toolkit/components/jsdownloads/src/DownloadCore.jsm

branch
TOR_BUG_9701
changeset 14
925c144e1f1f
equal deleted inserted replaced
-1:000000000000 0:50eb42a2cce4
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
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/. */
6
7 /**
8 * This file includes the following constructors and global objects:
9 *
10 * Download
11 * Represents a single download, with associated state and actions. This object
12 * is transient, though it can be included in a DownloadList so that it can be
13 * managed by the user interface and persisted across sessions.
14 *
15 * DownloadSource
16 * Represents the source of a download, for example a document or an URI.
17 *
18 * DownloadTarget
19 * Represents the target of a download, for example a file in the global
20 * downloads directory, or a file in the system temporary directory.
21 *
22 * DownloadError
23 * Provides detailed information about a download failure.
24 *
25 * DownloadSaver
26 * Template for an object that actually transfers the data for the download.
27 *
28 * DownloadCopySaver
29 * Saver object that simply copies the entire source file to the target.
30 *
31 * DownloadLegacySaver
32 * Saver object that integrates with the legacy nsITransfer interface.
33 */
34
35 "use strict";
36
37 this.EXPORTED_SYMBOLS = [
38 "Download",
39 "DownloadSource",
40 "DownloadTarget",
41 "DownloadError",
42 "DownloadSaver",
43 "DownloadCopySaver",
44 "DownloadLegacySaver",
45 ];
46
47 ////////////////////////////////////////////////////////////////////////////////
48 //// Globals
49
50 const Cc = Components.classes;
51 const Ci = Components.interfaces;
52 const Cu = Components.utils;
53 const Cr = Components.results;
54
55 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
56
57 XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
58 "resource://gre/modules/DownloadIntegration.jsm");
59 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
60 "resource://gre/modules/FileUtils.jsm");
61 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
62 "resource://gre/modules/NetUtil.jsm");
63 XPCOMUtils.defineLazyModuleGetter(this, "OS",
64 "resource://gre/modules/osfile.jsm")
65 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
66 "resource://gre/modules/Promise.jsm");
67 XPCOMUtils.defineLazyModuleGetter(this, "Services",
68 "resource://gre/modules/Services.jsm");
69 XPCOMUtils.defineLazyModuleGetter(this, "Task",
70 "resource://gre/modules/Task.jsm");
71
72 XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
73 "@mozilla.org/browser/download-history;1",
74 Ci.nsIDownloadHistory);
75 XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher",
76 "@mozilla.org/uriloader/external-helper-app-service;1",
77 Ci.nsPIExternalAppLauncher);
78 XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
79 "@mozilla.org/uriloader/external-helper-app-service;1",
80 Ci.nsIExternalHelperAppService);
81
82 const BackgroundFileSaverStreamListener = Components.Constructor(
83 "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
84 "nsIBackgroundFileSaver");
85
86 /**
87 * Returns true if the given value is a primitive string or a String object.
88 */
89 function isString(aValue) {
90 // We cannot use the "instanceof" operator reliably across module boundaries.
91 return (typeof aValue == "string") ||
92 (typeof aValue == "object" && "charAt" in aValue);
93 }
94
95 /**
96 * Serialize the unknown properties of aObject into aSerializable.
97 */
98 function serializeUnknownProperties(aObject, aSerializable)
99 {
100 if (aObject._unknownProperties) {
101 for (let property in aObject._unknownProperties) {
102 aSerializable[property] = aObject._unknownProperties[property];
103 }
104 }
105 }
106
107 /**
108 * Check for any unknown properties in aSerializable and preserve those in the
109 * _unknownProperties field of aObject. aFilterFn is called for each property
110 * name of aObject and should return true only for unknown properties.
111 */
112 function deserializeUnknownProperties(aObject, aSerializable, aFilterFn)
113 {
114 for (let property in aSerializable) {
115 if (aFilterFn(property)) {
116 if (!aObject._unknownProperties) {
117 aObject._unknownProperties = { };
118 }
119
120 aObject._unknownProperties[property] = aSerializable[property];
121 }
122 }
123 }
124
125 /**
126 * This determines the minimum time interval between updates to the number of
127 * bytes transferred, and is a limiting factor to the sequence of readings used
128 * in calculating the speed of the download.
129 */
130 const kProgressUpdateIntervalMs = 400;
131
132 ////////////////////////////////////////////////////////////////////////////////
133 //// Download
134
135 /**
136 * Represents a single download, with associated state and actions. This object
137 * is transient, though it can be included in a DownloadList so that it can be
138 * managed by the user interface and persisted across sessions.
139 */
140 this.Download = function ()
141 {
142 this._deferSucceeded = Promise.defer();
143 }
144
145 this.Download.prototype = {
146 /**
147 * DownloadSource object associated with this download.
148 */
149 source: null,
150
151 /**
152 * DownloadTarget object associated with this download.
153 */
154 target: null,
155
156 /**
157 * DownloadSaver object associated with this download.
158 */
159 saver: null,
160
161 /**
162 * Indicates that the download never started, has been completed successfully,
163 * failed, or has been canceled. This property becomes false when a download
164 * is started for the first time, or when a failed or canceled download is
165 * restarted.
166 */
167 stopped: true,
168
169 /**
170 * Indicates that the download has been completed successfully.
171 */
172 succeeded: false,
173
174 /**
175 * Indicates that the download has been canceled. This property can become
176 * true, then it can be reset to false when a canceled download is restarted.
177 *
178 * This property becomes true as soon as the "cancel" method is called, though
179 * the "stopped" property might remain false until the cancellation request
180 * has been processed. Temporary files or part files may still exist even if
181 * they are expected to be deleted, until the "stopped" property becomes true.
182 */
183 canceled: false,
184
185 /**
186 * When the download fails, this is set to a DownloadError instance indicating
187 * the cause of the failure. If the download has been completed successfully
188 * or has been canceled, this property is null. This property is reset to
189 * null when a failed download is restarted.
190 */
191 error: null,
192
193 /**
194 * Indicates the start time of the download. When the download starts,
195 * this property is set to a valid Date object. The default value is null
196 * before the download starts.
197 */
198 startTime: null,
199
200 /**
201 * Indicates whether this download's "progress" property is able to report
202 * partial progress while the download proceeds, and whether the value in
203 * totalBytes is relevant. This depends on the saver and the download source.
204 */
205 hasProgress: false,
206
207 /**
208 * Progress percent, from 0 to 100. Intermediate values are reported only if
209 * hasProgress is true.
210 *
211 * @note You shouldn't rely on this property being equal to 100 to determine
212 * whether the download is completed. You should use the individual
213 * state properties instead.
214 */
215 progress: 0,
216
217 /**
218 * When hasProgress is true, indicates the total number of bytes to be
219 * transferred before the download finishes, that can be zero for empty files.
220 *
221 * When hasProgress is false, this property is always zero.
222 */
223 totalBytes: 0,
224
225 /**
226 * Number of bytes currently transferred. This value starts at zero, and may
227 * be updated regardless of the value of hasProgress.
228 *
229 * @note You shouldn't rely on this property being equal to totalBytes to
230 * determine whether the download is completed. You should use the
231 * individual state properties instead.
232 */
233 currentBytes: 0,
234
235 /**
236 * Fractional number representing the speed of the download, in bytes per
237 * second. This value is zero when the download is stopped, and may be
238 * updated regardless of the value of hasProgress.
239 */
240 speed: 0,
241
242 /**
243 * Indicates whether, at this time, there is any partially downloaded data
244 * that can be used when restarting a failed or canceled download.
245 *
246 * This property is relevant while the download is in progress, and also if it
247 * failed or has been canceled. If the download has been completed
248 * successfully, this property is always false.
249 *
250 * Whether partial data can actually be retained depends on the saver and the
251 * download source, and may not be known before the download is started.
252 */
253 hasPartialData: false,
254
255 /**
256 * This can be set to a function that is called after other properties change.
257 */
258 onchange: null,
259
260 /**
261 * This tells if the user has chosen to open/run the downloaded file after
262 * download has completed.
263 */
264 launchWhenSucceeded: false,
265
266 /**
267 * This represents the MIME type of the download.
268 */
269 contentType: null,
270
271 /**
272 * This indicates the path of the application to be used to launch the file,
273 * or null if the file should be launched with the default application.
274 */
275 launcherPath: null,
276
277 /**
278 * Raises the onchange notification.
279 */
280 _notifyChange: function D_notifyChange() {
281 try {
282 if (this.onchange) {
283 this.onchange();
284 }
285 } catch (ex) {
286 Cu.reportError(ex);
287 }
288 },
289
290 /**
291 * The download may be stopped and restarted multiple times before it
292 * completes successfully. This may happen if any of the download attempts is
293 * canceled or fails.
294 *
295 * This property contains a promise that is linked to the current attempt, or
296 * null if the download is either stopped or in the process of being canceled.
297 * If the download restarts, this property is replaced with a new promise.
298 *
299 * The promise is resolved if the attempt it represents finishes successfully,
300 * and rejected if the attempt fails.
301 */
302 _currentAttempt: null,
303
304 /**
305 * Starts the download for the first time, or restarts a download that failed
306 * or has been canceled.
307 *
308 * Calling this method when the download has been completed successfully has
309 * no effect, and the method returns a resolved promise. If the download is
310 * in progress, the method returns the same promise as the previous call.
311 *
312 * If the "cancel" method was called but the cancellation process has not
313 * finished yet, this method waits for the cancellation to finish, then
314 * restarts the download immediately.
315 *
316 * @note If you need to start a new download from the same source, rather than
317 * restarting a failed or canceled one, you should create a separate
318 * Download object with the same source as the current one.
319 *
320 * @return {Promise}
321 * @resolves When the download has finished successfully.
322 * @rejects JavaScript exception if the download failed.
323 */
324 start: function D_start()
325 {
326 // If the download succeeded, it's the final state, we have nothing to do.
327 if (this.succeeded) {
328 return Promise.resolve();
329 }
330
331 // If the download already started and hasn't failed or hasn't been
332 // canceled, return the same promise as the previous call, allowing the
333 // caller to wait for the current attempt to finish.
334 if (this._currentAttempt) {
335 return this._currentAttempt;
336 }
337
338 // While shutting down or disposing of this object, we prevent the download
339 // from returning to be in progress.
340 if (this._finalized) {
341 return Promise.reject(new DownloadError({
342 message: "Cannot start after finalization."}));
343 }
344
345 // Initialize all the status properties for a new or restarted download.
346 this.stopped = false;
347 this.canceled = false;
348 this.error = null;
349 this.hasProgress = false;
350 this.progress = 0;
351 this.totalBytes = 0;
352 this.currentBytes = 0;
353 this.startTime = new Date();
354
355 // Create a new deferred object and an associated promise before starting
356 // the actual download. We store it on the download as the current attempt.
357 let deferAttempt = Promise.defer();
358 let currentAttempt = deferAttempt.promise;
359 this._currentAttempt = currentAttempt;
360
361 // Restart the progress and speed calculations from scratch.
362 this._lastProgressTimeMs = 0;
363
364 // This function propagates progress from the DownloadSaver object, unless
365 // it comes in late from a download attempt that was replaced by a new one.
366 // If the cancellation process for the download has started, then the update
367 // is ignored.
368 function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData)
369 {
370 if (this._currentAttempt == currentAttempt) {
371 this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData);
372 }
373 }
374
375 // This function propagates download properties from the DownloadSaver
376 // object, unless it comes in late from a download attempt that was
377 // replaced by a new one. If the cancellation process for the download has
378 // started, then the update is ignored.
379 function DS_setProperties(aOptions)
380 {
381 if (this._currentAttempt != currentAttempt) {
382 return;
383 }
384
385 let changeMade = false;
386
387 if ("contentType" in aOptions &&
388 this.contentType != aOptions.contentType) {
389 this.contentType = aOptions.contentType;
390 changeMade = true;
391 }
392
393 if (changeMade) {
394 this._notifyChange();
395 }
396 }
397
398 // Now that we stored the promise in the download object, we can start the
399 // task that will actually execute the download.
400 deferAttempt.resolve(Task.spawn(function task_D_start() {
401 // Wait upon any pending operation before restarting.
402 if (this._promiseCanceled) {
403 yield this._promiseCanceled;
404 }
405 if (this._promiseRemovePartialData) {
406 try {
407 yield this._promiseRemovePartialData;
408 } catch (ex) {
409 // Ignore any errors, which are already reported by the original
410 // caller of the removePartialData method.
411 }
412 }
413
414 // In case the download was restarted while cancellation was in progress,
415 // but the previous attempt actually succeeded before cancellation could
416 // be processed, it is possible that the download has already finished.
417 if (this.succeeded) {
418 return;
419 }
420
421 try {
422 // Disallow download if parental controls service restricts it.
423 if (yield DownloadIntegration.shouldBlockForParentalControls(this)) {
424 throw new DownloadError({ becauseBlockedByParentalControls: true });
425 }
426
427 // We should check if we have been canceled in the meantime, after all
428 // the previous asynchronous operations have been executed and just
429 // before we call the "execute" method of the saver.
430 if (this._promiseCanceled) {
431 // The exception will become a cancellation in the "catch" block.
432 throw undefined;
433 }
434
435 // Execute the actual download through the saver object.
436 this._saverExecuting = true;
437 yield this.saver.execute(DS_setProgressBytes.bind(this),
438 DS_setProperties.bind(this));
439
440 // Check for application reputation, which requires the entire file to
441 // be downloaded. After that, check for the last time if the download
442 // has been canceled. Both cases require the target file to be deleted,
443 // thus we process both in the same block of code.
444 if ((yield DownloadIntegration.shouldBlockForReputationCheck(this)) ||
445 this._promiseCanceled) {
446 try {
447 yield OS.File.remove(this.target.path);
448 } catch (ex) {
449 Cu.reportError(ex);
450 }
451 // If this is actually a cancellation, this exception will be changed
452 // in the catch block below.
453 throw new DownloadError({ becauseBlockedByReputationCheck: true });
454 }
455
456 // Update the status properties for a successful download.
457 this.progress = 100;
458 this.succeeded = true;
459 this.hasPartialData = false;
460 } catch (ex) {
461 // Fail with a generic status code on cancellation, so that the caller
462 // is forced to actually check the status properties to see if the
463 // download was canceled or failed because of other reasons.
464 if (this._promiseCanceled) {
465 throw new DownloadError({ message: "Download canceled." });
466 }
467
468 // An HTTP 450 error code is used by Windows to indicate that a uri is
469 // blocked by parental controls. This will prevent the download from
470 // occuring, so an error needs to be raised. This is not performed
471 // during the parental controls check above as it requires the request
472 // to start.
473 if (this._blockedByParentalControls) {
474 ex = new DownloadError({ becauseBlockedByParentalControls: true });
475 }
476
477 // Update the download error, unless a new attempt already started. The
478 // change in the status property is notified in the finally block.
479 if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
480 this.error = ex;
481 }
482 throw ex;
483 } finally {
484 // Any cancellation request has now been processed.
485 this._saverExecuting = false;
486 this._promiseCanceled = null;
487
488 // Update the status properties, unless a new attempt already started.
489 if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
490 this._currentAttempt = null;
491 this.stopped = true;
492 this.speed = 0;
493 this._notifyChange();
494 if (this.succeeded) {
495 yield DownloadIntegration.downloadDone(this);
496
497 this._deferSucceeded.resolve();
498
499 if (this.launchWhenSucceeded) {
500 this.launch().then(null, Cu.reportError);
501
502 // Always schedule files to be deleted at the end of the private browsing
503 // mode, regardless of the value of the pref.
504 if (this.source.isPrivate) {
505 gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
506 new FileUtils.File(this.target.path));
507 } else if (Services.prefs.getBoolPref(
508 "browser.helperApps.deleteTempFileOnExit")) {
509 gExternalAppLauncher.deleteTemporaryFileOnExit(
510 new FileUtils.File(this.target.path));
511 }
512 }
513 }
514 }
515 }
516 }.bind(this)));
517
518 // Notify the new download state before returning.
519 this._notifyChange();
520 return currentAttempt;
521 },
522
523 /*
524 * Launches the file after download has completed. This can open
525 * the file with the default application for the target MIME type
526 * or file extension, or with a custom application if launcherPath
527 * is set.
528 *
529 * @return {Promise}
530 * @resolves When the instruction to launch the file has been
531 * successfully given to the operating system. Note that
532 * the OS might still take a while until the file is actually
533 * launched.
534 * @rejects JavaScript exception if there was an error trying to launch
535 * the file.
536 */
537 launch: function() {
538 if (!this.succeeded) {
539 return Promise.reject(
540 new Error("launch can only be called if the download succeeded")
541 );
542 }
543
544 return DownloadIntegration.launchDownload(this);
545 },
546
547 /*
548 * Shows the folder containing the target file, or where the target file
549 * will be saved. This may be called at any time, even if the download
550 * failed or is currently in progress.
551 *
552 * @return {Promise}
553 * @resolves When the instruction to open the containing folder has been
554 * successfully given to the operating system. Note that
555 * the OS might still take a while until the folder is actually
556 * opened.
557 * @rejects JavaScript exception if there was an error trying to open
558 * the containing folder.
559 */
560 showContainingDirectory: function D_showContainingDirectory() {
561 return DownloadIntegration.showContainingDirectory(this.target.path);
562 },
563
564 /**
565 * When a request to cancel the download is received, contains a promise that
566 * will be resolved when the cancellation request is processed. When the
567 * request is processed, this property becomes null again.
568 */
569 _promiseCanceled: null,
570
571 /**
572 * True between the call to the "execute" method of the saver and the
573 * completion of the current download attempt.
574 */
575 _saverExecuting: false,
576
577 /**
578 * Cancels the download.
579 *
580 * The cancellation request is asynchronous. Until the cancellation process
581 * finishes, temporary files or part files may still exist even if they are
582 * expected to be deleted.
583 *
584 * In case the download completes successfully before the cancellation request
585 * could be processed, this method has no effect, and it returns a resolved
586 * promise. You should check the properties of the download at the time the
587 * returned promise is resolved to determine if the download was cancelled.
588 *
589 * Calling this method when the download has been completed successfully,
590 * failed, or has been canceled has no effect, and the method returns a
591 * resolved promise. This behavior is designed for the case where the call
592 * to "cancel" happens asynchronously, and is consistent with the case where
593 * the cancellation request could not be processed in time.
594 *
595 * @return {Promise}
596 * @resolves When the cancellation process has finished.
597 * @rejects Never.
598 */
599 cancel: function D_cancel()
600 {
601 // If the download is currently stopped, we have nothing to do.
602 if (this.stopped) {
603 return Promise.resolve();
604 }
605
606 if (!this._promiseCanceled) {
607 // Start a new cancellation request.
608 let deferCanceled = Promise.defer();
609 this._currentAttempt.then(function () deferCanceled.resolve(),
610 function () deferCanceled.resolve());
611 this._promiseCanceled = deferCanceled.promise;
612
613 // The download can already be restarted.
614 this._currentAttempt = null;
615
616 // Notify that the cancellation request was received.
617 this.canceled = true;
618 this._notifyChange();
619
620 // Execute the actual cancellation through the saver object, in case it
621 // has already started. Otherwise, the cancellation will be handled just
622 // before the saver is started.
623 if (this._saverExecuting) {
624 this.saver.cancel();
625 }
626 }
627
628 return this._promiseCanceled;
629 },
630
631 /**
632 * Indicates whether any partially downloaded data should be retained, to use
633 * when restarting a failed or canceled download. The default is false.
634 *
635 * Whether partial data can actually be retained depends on the saver and the
636 * download source, and may not be known before the download is started.
637 *
638 * To have any effect, this property must be set before starting the download.
639 * Resetting this property to false after the download has already started
640 * will not remove any partial data.
641 *
642 * If this property is set to true, care should be taken that partial data is
643 * removed before the reference to the download is discarded. This can be
644 * done using the removePartialData or the "finalize" methods.
645 */
646 tryToKeepPartialData: false,
647
648 /**
649 * When a request to remove partially downloaded data is received, contains a
650 * promise that will be resolved when the removal request is processed. When
651 * the request is processed, this property becomes null again.
652 */
653 _promiseRemovePartialData: null,
654
655 /**
656 * Removes any partial data kept as part of a canceled or failed download.
657 *
658 * If the download is not canceled or failed, this method has no effect, and
659 * it returns a resolved promise. If the "cancel" method was called but the
660 * cancellation process has not finished yet, this method waits for the
661 * cancellation to finish, then removes the partial data.
662 *
663 * After this method has been called, if the tryToKeepPartialData property is
664 * still true when the download is restarted, partial data will be retained
665 * during the new download attempt.
666 *
667 * @return {Promise}
668 * @resolves When the partial data has been successfully removed.
669 * @rejects JavaScript exception if the operation could not be completed.
670 */
671 removePartialData: function ()
672 {
673 if (!this.canceled && !this.error) {
674 return Promise.resolve();
675 }
676
677 let promiseRemovePartialData = this._promiseRemovePartialData;
678
679 if (!promiseRemovePartialData) {
680 let deferRemovePartialData = Promise.defer();
681 promiseRemovePartialData = deferRemovePartialData.promise;
682 this._promiseRemovePartialData = promiseRemovePartialData;
683
684 deferRemovePartialData.resolve(
685 Task.spawn(function task_D_removePartialData() {
686 try {
687 // Wait upon any pending cancellation request.
688 if (this._promiseCanceled) {
689 yield this._promiseCanceled;
690 }
691 // Ask the saver object to remove any partial data.
692 yield this.saver.removePartialData();
693 // For completeness, clear the number of bytes transferred.
694 if (this.currentBytes != 0 || this.hasPartialData) {
695 this.currentBytes = 0;
696 this.hasPartialData = false;
697 this._notifyChange();
698 }
699 } finally {
700 this._promiseRemovePartialData = null;
701 }
702 }.bind(this)));
703 }
704
705 return promiseRemovePartialData;
706 },
707
708 /**
709 * This deferred object contains a promise that is resolved as soon as this
710 * download finishes successfully, and is never rejected. This property is
711 * initialized when the download is created, and never changes.
712 */
713 _deferSucceeded: null,
714
715 /**
716 * Returns a promise that is resolved as soon as this download finishes
717 * successfully, even if the download was stopped and restarted meanwhile.
718 *
719 * You can use this property for scheduling download completion actions in the
720 * current session, for downloads that are controlled interactively. If the
721 * download is not controlled interactively, you should use the promise
722 * returned by the "start" method instead, to check for success or failure.
723 *
724 * @return {Promise}
725 * @resolves When the download has finished successfully.
726 * @rejects Never.
727 */
728 whenSucceeded: function D_whenSucceeded()
729 {
730 return this._deferSucceeded.promise;
731 },
732
733 /**
734 * Updates the state of a finished, failed, or canceled download based on the
735 * current state in the file system. If the download is in progress or it has
736 * been finalized, this method has no effect, and it returns a resolved
737 * promise.
738 *
739 * This allows the properties of the download to be updated in case the user
740 * moved or deleted the target file or its associated ".part" file.
741 *
742 * @return {Promise}
743 * @resolves When the operation has completed.
744 * @rejects Never.
745 */
746 refresh: function ()
747 {
748 return Task.spawn(function () {
749 if (!this.stopped || this._finalized) {
750 return;
751 }
752
753 // Update the current progress from disk if we retained partial data.
754 if (this.hasPartialData && this.target.partFilePath) {
755 let stat = yield OS.File.stat(this.target.partFilePath);
756
757 // Ignore the result if the state has changed meanwhile.
758 if (!this.stopped || this._finalized) {
759 return;
760 }
761
762 // Update the bytes transferred and the related progress properties.
763 this.currentBytes = stat.size;
764 if (this.totalBytes > 0) {
765 this.hasProgress = true;
766 this.progress = Math.floor(this.currentBytes /
767 this.totalBytes * 100);
768 }
769 this._notifyChange();
770 }
771 }.bind(this)).then(null, Cu.reportError);
772 },
773
774 /**
775 * True if the "finalize" method has been called. This prevents the download
776 * from starting again after having been stopped.
777 */
778 _finalized: false,
779
780 /**
781 * Ensures that the download is stopped, and optionally removes any partial
782 * data kept as part of a canceled or failed download. After this method has
783 * been called, the download cannot be started again.
784 *
785 * This method should be used in place of "cancel" and removePartialData while
786 * shutting down or disposing of the download object, to prevent other callers
787 * from interfering with the operation. This is required because cancellation
788 * and other operations are asynchronous.
789 *
790 * @param aRemovePartialData
791 * Whether any partially downloaded data should be removed after the
792 * download has been stopped.
793 *
794 * @return {Promise}
795 * @resolves When the operation has finished successfully.
796 * @rejects JavaScript exception if an error occurred while removing the
797 * partially downloaded data.
798 */
799 finalize: function (aRemovePartialData)
800 {
801 // Prevents the download from starting again after having been stopped.
802 this._finalized = true;
803
804 if (aRemovePartialData) {
805 // Cancel the download, in case it is currently in progress, then remove
806 // any partially downloaded data. The removal operation waits for
807 // cancellation to be completed before resolving the promise it returns.
808 this.cancel();
809 return this.removePartialData();
810 } else {
811 // Just cancel the download, in case it is currently in progress.
812 return this.cancel();
813 }
814 },
815
816 /**
817 * Indicates the time of the last progress notification, expressed as the
818 * number of milliseconds since January 1, 1970, 00:00:00 UTC. This is zero
819 * until some bytes have actually been transferred.
820 */
821 _lastProgressTimeMs: 0,
822
823 /**
824 * Updates progress notifications based on the number of bytes transferred.
825 *
826 * The number of bytes transferred is not updated unless enough time passed
827 * since this function was last called. This limits the computation load, in
828 * particular when the listeners update the user interface in response.
829 *
830 * @param aCurrentBytes
831 * Number of bytes transferred until now.
832 * @param aTotalBytes
833 * Total number of bytes to be transferred, or -1 if unknown.
834 * @param aHasPartialData
835 * Indicates whether the partially downloaded data can be used when
836 * restarting the download if it fails or is canceled.
837 */
838 _setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
839 let changeMade = (this.hasPartialData != aHasPartialData);
840 this.hasPartialData = aHasPartialData;
841
842 // Unless aTotalBytes is -1, we can report partial download progress. In
843 // this case, notify when the related properties changed since last time.
844 if (aTotalBytes != -1 && (!this.hasProgress ||
845 this.totalBytes != aTotalBytes)) {
846 this.hasProgress = true;
847 this.totalBytes = aTotalBytes;
848 changeMade = true;
849 }
850
851 // Updating the progress and computing the speed require that enough time
852 // passed since the last update, or that we haven't started throttling yet.
853 let currentTimeMs = Date.now();
854 let intervalMs = currentTimeMs - this._lastProgressTimeMs;
855 if (intervalMs >= kProgressUpdateIntervalMs) {
856 // Don't compute the speed unless we started throttling notifications.
857 if (this._lastProgressTimeMs != 0) {
858 // Calculate the speed in bytes per second.
859 let rawSpeed = (aCurrentBytes - this.currentBytes) / intervalMs * 1000;
860 if (this.speed == 0) {
861 // When the previous speed is exactly zero instead of a fractional
862 // number, this can be considered the first element of the series.
863 this.speed = rawSpeed;
864 } else {
865 // Apply exponential smoothing, with a smoothing factor of 0.1.
866 this.speed = rawSpeed * 0.1 + this.speed * 0.9;
867 }
868 }
869
870 // Start throttling notifications only when we have actually received some
871 // bytes for the first time. The timing of the first part of the download
872 // is not reliable, due to possible latency in the initial notifications.
873 // This also allows automated tests to receive and verify the number of
874 // bytes initially transferred.
875 if (aCurrentBytes > 0) {
876 this._lastProgressTimeMs = currentTimeMs;
877
878 // Update the progress now that we don't need its previous value.
879 this.currentBytes = aCurrentBytes;
880 if (this.totalBytes > 0) {
881 this.progress = Math.floor(this.currentBytes / this.totalBytes * 100);
882 }
883 changeMade = true;
884 }
885 }
886
887 if (changeMade) {
888 this._notifyChange();
889 }
890 },
891
892 /**
893 * Returns a static representation of the current object state.
894 *
895 * @return A JavaScript object that can be serialized to JSON.
896 */
897 toSerializable: function ()
898 {
899 let serializable = {
900 source: this.source.toSerializable(),
901 target: this.target.toSerializable(),
902 };
903
904 // Simplify the representation for the most common saver type. If the saver
905 // is an object instead of a simple string, we can't simplify it because we
906 // need to persist all its properties, not only "type". This may happen for
907 // savers of type "copy" as well as other types.
908 let saver = this.saver.toSerializable();
909 if (saver !== "copy") {
910 serializable.saver = saver;
911 }
912
913 if (this.error && ("message" in this.error)) {
914 serializable.error = { message: this.error.message };
915 }
916
917 if (this.startTime) {
918 serializable.startTime = this.startTime.toJSON();
919 }
920
921 // These are serialized unless they are false, null, or empty strings.
922 for (let property of kSerializableDownloadProperties) {
923 if (property != "error" && property != "startTime" && this[property]) {
924 serializable[property] = this[property];
925 }
926 }
927
928 serializeUnknownProperties(this, serializable);
929
930 return serializable;
931 },
932
933 /**
934 * Returns a value that changes only when one of the properties of a Download
935 * object that should be saved into a file also change. This excludes
936 * properties whose value doesn't usually change during the download lifetime.
937 *
938 * This function is used to determine whether the download should be
939 * serialized after a property change notification has been received.
940 *
941 * @return String representing the relevant download state.
942 */
943 getSerializationHash: function ()
944 {
945 // The "succeeded", "canceled", "error", and startTime properties are not
946 // taken into account because they all change before the "stopped" property
947 // changes, and are not altered in other cases.
948 return this.stopped + "," + this.totalBytes + "," + this.hasPartialData +
949 "," + this.contentType;
950 },
951 };
952
953 /**
954 * Defines which properties of the Download object are serializable.
955 */
956 const kSerializableDownloadProperties = [
957 "succeeded",
958 "canceled",
959 "error",
960 "totalBytes",
961 "hasPartialData",
962 "tryToKeepPartialData",
963 "launcherPath",
964 "launchWhenSucceeded",
965 "contentType",
966 ];
967
968 /**
969 * Creates a new Download object from a serializable representation. This
970 * function is used by the createDownload method of Downloads.jsm when a new
971 * Download object is requested, thus some properties may refer to live objects
972 * in place of their serializable representations.
973 *
974 * @param aSerializable
975 * An object with the following fields:
976 * {
977 * source: DownloadSource object, or its serializable representation.
978 * See DownloadSource.fromSerializable for details.
979 * target: DownloadTarget object, or its serializable representation.
980 * See DownloadTarget.fromSerializable for details.
981 * saver: Serializable representation of a DownloadSaver object. See
982 * DownloadSaver.fromSerializable for details. If omitted,
983 * defaults to "copy".
984 * }
985 *
986 * @return The newly created Download object.
987 */
988 Download.fromSerializable = function (aSerializable) {
989 let download = new Download();
990 if (aSerializable.source instanceof DownloadSource) {
991 download.source = aSerializable.source;
992 } else {
993 download.source = DownloadSource.fromSerializable(aSerializable.source);
994 }
995 if (aSerializable.target instanceof DownloadTarget) {
996 download.target = aSerializable.target;
997 } else {
998 download.target = DownloadTarget.fromSerializable(aSerializable.target);
999 }
1000 if ("saver" in aSerializable) {
1001 download.saver = DownloadSaver.fromSerializable(aSerializable.saver);
1002 } else {
1003 download.saver = DownloadSaver.fromSerializable("copy");
1004 }
1005 download.saver.download = download;
1006
1007 if ("startTime" in aSerializable) {
1008 let time = aSerializable.startTime.getTime
1009 ? aSerializable.startTime.getTime()
1010 : aSerializable.startTime;
1011 download.startTime = new Date(time);
1012 }
1013
1014 for (let property of kSerializableDownloadProperties) {
1015 if (property in aSerializable) {
1016 download[property] = aSerializable[property];
1017 }
1018 }
1019
1020 deserializeUnknownProperties(download, aSerializable, property =>
1021 kSerializableDownloadProperties.indexOf(property) == -1 &&
1022 property != "startTime" &&
1023 property != "source" &&
1024 property != "target" &&
1025 property != "saver");
1026
1027 return download;
1028 };
1029
1030 ////////////////////////////////////////////////////////////////////////////////
1031 //// DownloadSource
1032
1033 /**
1034 * Represents the source of a download, for example a document or an URI.
1035 */
1036 this.DownloadSource = function () {}
1037
1038 this.DownloadSource.prototype = {
1039 /**
1040 * String containing the URI for the download source.
1041 */
1042 url: null,
1043
1044 /**
1045 * Indicates whether the download originated from a private window. This
1046 * determines the context of the network request that is made to retrieve the
1047 * resource.
1048 */
1049 isPrivate: false,
1050
1051 /**
1052 * String containing the referrer URI of the download source, or null if no
1053 * referrer should be sent or the download source is not HTTP.
1054 */
1055 referrer: null,
1056
1057 /**
1058 * Returns a static representation of the current object state.
1059 *
1060 * @return A JavaScript object that can be serialized to JSON.
1061 */
1062 toSerializable: function ()
1063 {
1064 // Simplify the representation if we don't have other details.
1065 if (!this.isPrivate && !this.referrer && !this._unknownProperties) {
1066 return this.url;
1067 }
1068
1069 let serializable = { url: this.url };
1070 if (this.isPrivate) {
1071 serializable.isPrivate = true;
1072 }
1073 if (this.referrer) {
1074 serializable.referrer = this.referrer;
1075 }
1076
1077 serializeUnknownProperties(this, serializable);
1078 return serializable;
1079 },
1080 };
1081
1082 /**
1083 * Creates a new DownloadSource object from its serializable representation.
1084 *
1085 * @param aSerializable
1086 * Serializable representation of a DownloadSource object. This may be a
1087 * string containing the URI for the download source, an nsIURI, or an
1088 * object with the following properties:
1089 * {
1090 * url: String containing the URI for the download source.
1091 * isPrivate: Indicates whether the download originated from a private
1092 * window. If omitted, the download is public.
1093 * referrer: String containing the referrer URI of the download source.
1094 * Can be omitted or null if no referrer should be sent or
1095 * the download source is not HTTP.
1096 * }
1097 *
1098 * @return The newly created DownloadSource object.
1099 */
1100 this.DownloadSource.fromSerializable = function (aSerializable) {
1101 let source = new DownloadSource();
1102 if (isString(aSerializable)) {
1103 // Convert String objects to primitive strings at this point.
1104 source.url = aSerializable.toString();
1105 } else if (aSerializable instanceof Ci.nsIURI) {
1106 source.url = aSerializable.spec;
1107 } else {
1108 // Convert String objects to primitive strings at this point.
1109 source.url = aSerializable.url.toString();
1110 if ("isPrivate" in aSerializable) {
1111 source.isPrivate = aSerializable.isPrivate;
1112 }
1113 if ("referrer" in aSerializable) {
1114 source.referrer = aSerializable.referrer;
1115 }
1116
1117 deserializeUnknownProperties(source, aSerializable, property =>
1118 property != "url" && property != "isPrivate" && property != "referrer");
1119 }
1120
1121 return source;
1122 };
1123
1124 ////////////////////////////////////////////////////////////////////////////////
1125 //// DownloadTarget
1126
1127 /**
1128 * Represents the target of a download, for example a file in the global
1129 * downloads directory, or a file in the system temporary directory.
1130 */
1131 this.DownloadTarget = function () {}
1132
1133 this.DownloadTarget.prototype = {
1134 /**
1135 * String containing the path of the target file.
1136 */
1137 path: null,
1138
1139 /**
1140 * String containing the path of the ".part" file containing the data
1141 * downloaded so far, or null to disable the use of a ".part" file to keep
1142 * partially downloaded data.
1143 */
1144 partFilePath: null,
1145
1146 /**
1147 * Returns a static representation of the current object state.
1148 *
1149 * @return A JavaScript object that can be serialized to JSON.
1150 */
1151 toSerializable: function ()
1152 {
1153 // Simplify the representation if we don't have other details.
1154 if (!this.partFilePath && !this._unknownProperties) {
1155 return this.path;
1156 }
1157
1158 let serializable = { path: this.path,
1159 partFilePath: this.partFilePath };
1160 serializeUnknownProperties(this, serializable);
1161 return serializable;
1162 },
1163 };
1164
1165 /**
1166 * Creates a new DownloadTarget object from its serializable representation.
1167 *
1168 * @param aSerializable
1169 * Serializable representation of a DownloadTarget object. This may be a
1170 * string containing the path of the target file, an nsIFile, or an
1171 * object with the following properties:
1172 * {
1173 * path: String containing the path of the target file.
1174 * partFilePath: optional string containing the part file path.
1175 * }
1176 *
1177 * @return The newly created DownloadTarget object.
1178 */
1179 this.DownloadTarget.fromSerializable = function (aSerializable) {
1180 let target = new DownloadTarget();
1181 if (isString(aSerializable)) {
1182 // Convert String objects to primitive strings at this point.
1183 target.path = aSerializable.toString();
1184 } else if (aSerializable instanceof Ci.nsIFile) {
1185 // Read the "path" property of nsIFile after checking the object type.
1186 target.path = aSerializable.path;
1187 } else {
1188 // Read the "path" property of the serializable DownloadTarget
1189 // representation, converting String objects to primitive strings.
1190 target.path = aSerializable.path.toString();
1191 if ("partFilePath" in aSerializable) {
1192 target.partFilePath = aSerializable.partFilePath;
1193 }
1194
1195 deserializeUnknownProperties(target, aSerializable, property =>
1196 property != "path" && property != "partFilePath");
1197 }
1198 return target;
1199 };
1200
1201 ////////////////////////////////////////////////////////////////////////////////
1202 //// DownloadError
1203
1204 /**
1205 * Provides detailed information about a download failure.
1206 *
1207 * @param aProperties
1208 * Object which may contain any of the following properties:
1209 * {
1210 * result: Result error code, defaulting to Cr.NS_ERROR_FAILURE
1211 * message: String error message to be displayed, or null to use the
1212 * message associated with the result code.
1213 * inferCause: If true, attempts to determine if the cause of the
1214 * download is a network failure or a local file failure,
1215 * based on a set of known values of the result code.
1216 * This is useful when the error is received by a
1217 * component that handles both aspects of the download.
1218 * }
1219 * The properties object may also contain any of the DownloadError's
1220 * because properties, which will be set accordingly in the error object.
1221 */
1222 this.DownloadError = function (aProperties)
1223 {
1224 const NS_ERROR_MODULE_BASE_OFFSET = 0x45;
1225 const NS_ERROR_MODULE_NETWORK = 6;
1226 const NS_ERROR_MODULE_FILES = 13;
1227
1228 // Set the error name used by the Error object prototype first.
1229 this.name = "DownloadError";
1230 this.result = aProperties.result || Cr.NS_ERROR_FAILURE;
1231 if (aProperties.message) {
1232 this.message = aProperties.message;
1233 } else if (aProperties.becauseBlocked ||
1234 aProperties.becauseBlockedByParentalControls ||
1235 aProperties.becauseBlockedByReputationCheck) {
1236 this.message = "Download blocked.";
1237 } else {
1238 let exception = new Components.Exception("", this.result);
1239 this.message = exception.toString();
1240 }
1241 if (aProperties.inferCause) {
1242 let module = ((this.result & 0x7FFF0000) >> 16) -
1243 NS_ERROR_MODULE_BASE_OFFSET;
1244 this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK);
1245 this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES);
1246 }
1247 else {
1248 if (aProperties.becauseSourceFailed) {
1249 this.becauseSourceFailed = true;
1250 }
1251 if (aProperties.becauseTargetFailed) {
1252 this.becauseTargetFailed = true;
1253 }
1254 }
1255
1256 if (aProperties.becauseBlockedByParentalControls) {
1257 this.becauseBlocked = true;
1258 this.becauseBlockedByParentalControls = true;
1259 } else if (aProperties.becauseBlockedByReputationCheck) {
1260 this.becauseBlocked = true;
1261 this.becauseBlockedByReputationCheck = true;
1262 } else if (aProperties.becauseBlocked) {
1263 this.becauseBlocked = true;
1264 }
1265
1266 this.stack = new Error().stack;
1267 }
1268
1269 this.DownloadError.prototype = {
1270 __proto__: Error.prototype,
1271
1272 /**
1273 * The result code associated with this error.
1274 */
1275 result: false,
1276
1277 /**
1278 * Indicates an error occurred while reading from the remote location.
1279 */
1280 becauseSourceFailed: false,
1281
1282 /**
1283 * Indicates an error occurred while writing to the local target.
1284 */
1285 becauseTargetFailed: false,
1286
1287 /**
1288 * Indicates the download failed because it was blocked. If the reason for
1289 * blocking is known, the corresponding property will be also set.
1290 */
1291 becauseBlocked: false,
1292
1293 /**
1294 * Indicates the download was blocked because downloads are globally
1295 * disallowed by the Parental Controls or Family Safety features on Windows.
1296 */
1297 becauseBlockedByParentalControls: false,
1298
1299 /**
1300 * Indicates the download was blocked because it failed the reputation check
1301 * and may be malware.
1302 */
1303 becauseBlockedByReputationCheck: false,
1304 };
1305
1306 ////////////////////////////////////////////////////////////////////////////////
1307 //// DownloadSaver
1308
1309 /**
1310 * Template for an object that actually transfers the data for the download.
1311 */
1312 this.DownloadSaver = function () {}
1313
1314 this.DownloadSaver.prototype = {
1315 /**
1316 * Download object for raising notifications and reading properties.
1317 *
1318 * If the tryToKeepPartialData property of the download object is false, the
1319 * saver should never try to keep partially downloaded data if the download
1320 * fails.
1321 */
1322 download: null,
1323
1324 /**
1325 * Executes the download.
1326 *
1327 * @param aSetProgressBytesFn
1328 * This function may be called by the saver to report progress. It
1329 * takes three arguments: the first is the number of bytes transferred
1330 * until now, the second is the total number of bytes to be
1331 * transferred (or -1 if unknown), the third indicates whether the
1332 * partially downloaded data can be used when restarting the download
1333 * if it fails or is canceled.
1334 * @param aSetPropertiesFn
1335 * This function may be called by the saver to report information
1336 * about new download properties discovered by the saver during the
1337 * download process. It takes an object where the keys represents
1338 * the names of the properties to set, and the value represents the
1339 * value to set.
1340 *
1341 * @return {Promise}
1342 * @resolves When the download has finished successfully.
1343 * @rejects JavaScript exception if the download failed.
1344 */
1345 execute: function DS_execute(aSetProgressBytesFn, aSetPropertiesFn)
1346 {
1347 throw new Error("Not implemented.");
1348 },
1349
1350 /**
1351 * Cancels the download.
1352 */
1353 cancel: function DS_cancel()
1354 {
1355 throw new Error("Not implemented.");
1356 },
1357
1358 /**
1359 * Removes any partial data kept as part of a canceled or failed download.
1360 *
1361 * This method is never called until the promise returned by "execute" is
1362 * either resolved or rejected, and the "execute" method is not called again
1363 * until the promise returned by this method is resolved or rejected.
1364 *
1365 * @return {Promise}
1366 * @resolves When the operation has finished successfully.
1367 * @rejects JavaScript exception.
1368 */
1369 removePartialData: function DS_removePartialData()
1370 {
1371 return Promise.resolve();
1372 },
1373
1374 /**
1375 * This can be called by the saver implementation when the download is already
1376 * started, to add it to the browsing history. This method has no effect if
1377 * the download is private.
1378 */
1379 addToHistory: function ()
1380 {
1381 if (this.download.source.isPrivate) {
1382 return;
1383 }
1384
1385 let sourceUri = NetUtil.newURI(this.download.source.url);
1386 let referrer = this.download.source.referrer;
1387 let referrerUri = referrer ? NetUtil.newURI(referrer) : null;
1388 let targetUri = NetUtil.newURI(new FileUtils.File(
1389 this.download.target.path));
1390
1391 // The start time is always available when we reach this point.
1392 let startPRTime = this.download.startTime.getTime() * 1000;
1393
1394 gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime,
1395 targetUri);
1396 },
1397
1398 /**
1399 * Returns a static representation of the current object state.
1400 *
1401 * @return A JavaScript object that can be serialized to JSON.
1402 */
1403 toSerializable: function ()
1404 {
1405 throw new Error("Not implemented.");
1406 },
1407
1408 /**
1409 * Returns the SHA-256 hash of the downloaded file, if it exists.
1410 */
1411 getSha256Hash: function ()
1412 {
1413 throw new Error("Not implemented.");
1414 },
1415
1416 getSignatureInfo: function ()
1417 {
1418 throw new Error("Not implemented.");
1419 },
1420 }; // DownloadSaver
1421
1422 /**
1423 * Creates a new DownloadSaver object from its serializable representation.
1424 *
1425 * @param aSerializable
1426 * Serializable representation of a DownloadSaver object. If no initial
1427 * state information for the saver object is needed, can be a string
1428 * representing the class of the download operation, for example "copy".
1429 *
1430 * @return The newly created DownloadSaver object.
1431 */
1432 this.DownloadSaver.fromSerializable = function (aSerializable) {
1433 let serializable = isString(aSerializable) ? { type: aSerializable }
1434 : aSerializable;
1435 let saver;
1436 switch (serializable.type) {
1437 case "copy":
1438 saver = DownloadCopySaver.fromSerializable(serializable);
1439 break;
1440 case "legacy":
1441 saver = DownloadLegacySaver.fromSerializable(serializable);
1442 break;
1443 default:
1444 throw new Error("Unrecoginzed download saver type.");
1445 }
1446 return saver;
1447 };
1448
1449 ////////////////////////////////////////////////////////////////////////////////
1450 //// DownloadCopySaver
1451
1452 /**
1453 * Saver object that simply copies the entire source file to the target.
1454 */
1455 this.DownloadCopySaver = function () {}
1456
1457 this.DownloadCopySaver.prototype = {
1458 __proto__: DownloadSaver.prototype,
1459
1460 /**
1461 * BackgroundFileSaver object currently handling the download.
1462 */
1463 _backgroundFileSaver: null,
1464
1465 /**
1466 * Indicates whether the "cancel" method has been called. This is used to
1467 * prevent the request from starting in case the operation is canceled before
1468 * the BackgroundFileSaver instance has been created.
1469 */
1470 _canceled: false,
1471
1472 /**
1473 * Save the SHA-256 hash in raw bytes of the downloaded file. This is null
1474 * unless BackgroundFileSaver has successfully completed saving the file.
1475 */
1476 _sha256Hash: null,
1477
1478 /**
1479 * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
1480 * if the file is signed. This is empty if the file is unsigned, and null
1481 * unless BackgroundFileSaver has successfully completed saving the file.
1482 */
1483 _signatureInfo: null,
1484
1485 /**
1486 * True if the associated download has already been added to browsing history.
1487 */
1488 alreadyAddedToHistory: false,
1489
1490 /**
1491 * String corresponding to the entityID property of the nsIResumableChannel
1492 * used to execute the download, or null if the channel was not resumable or
1493 * the saver was instructed not to keep partially downloaded data.
1494 */
1495 entityID: null,
1496
1497 /**
1498 * Implements "DownloadSaver.execute".
1499 */
1500 execute: function DCS_execute(aSetProgressBytesFn, aSetPropertiesFn)
1501 {
1502 let copySaver = this;
1503
1504 this._canceled = false;
1505
1506 let download = this.download;
1507 let targetPath = download.target.path;
1508 let partFilePath = download.target.partFilePath;
1509 let keepPartialData = download.tryToKeepPartialData;
1510
1511 return Task.spawn(function task_DCS_execute() {
1512 // Add the download to history the first time it is started in this
1513 // session. If the download is restarted in a different session, a new
1514 // history visit will be added. We do this just to avoid the complexity
1515 // of serializing this state between sessions, since adding a new visit
1516 // does not have any noticeable side effect.
1517 if (!this.alreadyAddedToHistory) {
1518 this.addToHistory();
1519 this.alreadyAddedToHistory = true;
1520 }
1521
1522 // To reduce the chance that other downloads reuse the same final target
1523 // file name, we should create a placeholder as soon as possible, before
1524 // starting the network request. The placeholder is also required in case
1525 // we are using a ".part" file instead of the final target while the
1526 // download is in progress.
1527 try {
1528 // If the file already exists, don't delete its contents yet.
1529 let file = yield OS.File.open(targetPath, { write: true });
1530 yield file.close();
1531 } catch (ex if ex instanceof OS.File.Error) {
1532 // Throw a DownloadError indicating that the operation failed because of
1533 // the target file. We cannot translate this into a specific result
1534 // code, but we preserve the original message using the toString method.
1535 let error = new DownloadError({ message: ex.toString() });
1536 error.becauseTargetFailed = true;
1537 throw error;
1538 }
1539
1540 try {
1541 let deferSaveComplete = Promise.defer();
1542
1543 if (this._canceled) {
1544 // Don't create the BackgroundFileSaver object if we have been
1545 // canceled meanwhile.
1546 throw new DownloadError({ message: "Saver canceled." });
1547 }
1548
1549 // Create the object that will save the file in a background thread.
1550 let backgroundFileSaver = new BackgroundFileSaverStreamListener();
1551 try {
1552 // When the operation completes, reflect the status in the promise
1553 // returned by this download execution function.
1554 backgroundFileSaver.observer = {
1555 onTargetChange: function () { },
1556 onSaveComplete: (aSaver, aStatus) => {
1557 // Send notifications now that we can restart if needed.
1558 if (Components.isSuccessCode(aStatus)) {
1559 // Save the hash before freeing backgroundFileSaver.
1560 this._sha256Hash = aSaver.sha256Hash;
1561 this._signatureInfo = aSaver.signatureInfo;
1562 deferSaveComplete.resolve();
1563 } else {
1564 // Infer the origin of the error from the failure code, because
1565 // BackgroundFileSaver does not provide more specific data.
1566 let properties = { result: aStatus, inferCause: true };
1567 deferSaveComplete.reject(new DownloadError(properties));
1568 }
1569 // Free the reference cycle, to release resources earlier.
1570 backgroundFileSaver.observer = null;
1571 this._backgroundFileSaver = null;
1572 },
1573 };
1574
1575 // Create a channel from the source, and listen to progress
1576 // notifications.
1577 let channel = NetUtil.newChannel(NetUtil.newURI(download.source.url));
1578 if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
1579 channel.setPrivate(download.source.isPrivate);
1580 }
1581 if (channel instanceof Ci.nsIHttpChannel &&
1582 download.source.referrer) {
1583 channel.referrer = NetUtil.newURI(download.source.referrer);
1584 }
1585
1586 // If we have data that we can use to resume the download from where
1587 // it stopped, try to use it.
1588 let resumeAttempted = false;
1589 let resumeFromBytes = 0;
1590 if (channel instanceof Ci.nsIResumableChannel && this.entityID &&
1591 partFilePath && keepPartialData) {
1592 try {
1593 let stat = yield OS.File.stat(partFilePath);
1594 channel.resumeAt(stat.size, this.entityID);
1595 resumeAttempted = true;
1596 resumeFromBytes = stat.size;
1597 } catch (ex if ex instanceof OS.File.Error &&
1598 ex.becauseNoSuchFile) { }
1599 }
1600
1601 channel.notificationCallbacks = {
1602 QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
1603 getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]),
1604 onProgress: function DCSE_onProgress(aRequest, aContext, aProgress,
1605 aProgressMax)
1606 {
1607 let currentBytes = resumeFromBytes + aProgress;
1608 let totalBytes = aProgressMax == -1 ? -1 : (resumeFromBytes +
1609 aProgressMax);
1610 aSetProgressBytesFn(currentBytes, totalBytes, aProgress > 0 &&
1611 partFilePath && keepPartialData);
1612 },
1613 onStatus: function () { },
1614 };
1615
1616 // Open the channel, directing output to the background file saver.
1617 backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
1618 channel.asyncOpen({
1619 onStartRequest: function (aRequest, aContext) {
1620 backgroundFileSaver.onStartRequest(aRequest, aContext);
1621
1622 // Check if the request's response has been blocked by Windows
1623 // Parental Controls with an HTTP 450 error code.
1624 if (aRequest instanceof Ci.nsIHttpChannel &&
1625 aRequest.responseStatus == 450) {
1626 // Set a flag that can be retrieved later when handling the
1627 // cancellation so that the proper error can be thrown.
1628 this.download._blockedByParentalControls = true;
1629 aRequest.cancel(Cr.NS_BINDING_ABORTED);
1630 return;
1631 }
1632
1633 aSetPropertiesFn({ contentType: channel.contentType });
1634
1635 // Ensure we report the value of "Content-Length", if available,
1636 // even if the download doesn't generate any progress events
1637 // later.
1638 if (channel.contentLength >= 0) {
1639 aSetProgressBytesFn(0, channel.contentLength);
1640 }
1641
1642 // If the URL we are downloading from includes a file extension
1643 // that matches the "Content-Encoding" header, for example ".gz"
1644 // with a "gzip" encoding, we should save the file in its encoded
1645 // form. In all other cases, we decode the body while saving.
1646 if (channel instanceof Ci.nsIEncodedChannel &&
1647 channel.contentEncodings) {
1648 let uri = channel.URI;
1649 if (uri instanceof Ci.nsIURL && uri.fileExtension) {
1650 // Only the first, outermost encoding is considered.
1651 let encoding = channel.contentEncodings.getNext();
1652 if (encoding) {
1653 channel.applyConversion =
1654 gExternalHelperAppService.applyDecodingForExtension(
1655 uri.fileExtension, encoding);
1656 }
1657 }
1658 }
1659
1660 if (keepPartialData) {
1661 // If the source is not resumable, don't keep partial data even
1662 // if we were asked to try and do it.
1663 if (aRequest instanceof Ci.nsIResumableChannel) {
1664 try {
1665 // If reading the ID succeeds, the source is resumable.
1666 this.entityID = aRequest.entityID;
1667 } catch (ex if ex instanceof Components.Exception &&
1668 ex.result == Cr.NS_ERROR_NOT_RESUMABLE) {
1669 keepPartialData = false;
1670 }
1671 } else {
1672 keepPartialData = false;
1673 }
1674 }
1675
1676 // Enable hashing and signature verification before setting the
1677 // target.
1678 backgroundFileSaver.enableSha256();
1679 backgroundFileSaver.enableSignatureInfo();
1680 if (partFilePath) {
1681 // If we actually resumed a request, append to the partial data.
1682 if (resumeAttempted) {
1683 // TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED
1684 backgroundFileSaver.enableAppend();
1685 }
1686
1687 // Use a part file, determining if we should keep it on failure.
1688 backgroundFileSaver.setTarget(new FileUtils.File(partFilePath),
1689 keepPartialData);
1690 } else {
1691 // Set the final target file, and delete it on failure.
1692 backgroundFileSaver.setTarget(new FileUtils.File(targetPath),
1693 false);
1694 }
1695 }.bind(copySaver),
1696
1697 onStopRequest: function (aRequest, aContext, aStatusCode) {
1698 try {
1699 backgroundFileSaver.onStopRequest(aRequest, aContext,
1700 aStatusCode);
1701 } finally {
1702 // If the data transfer completed successfully, indicate to the
1703 // background file saver that the operation can finish. If the
1704 // data transfer failed, the saver has been already stopped.
1705 if (Components.isSuccessCode(aStatusCode)) {
1706 if (partFilePath) {
1707 // Move to the final target if we were using a part file.
1708 backgroundFileSaver.setTarget(
1709 new FileUtils.File(targetPath), false);
1710 }
1711 backgroundFileSaver.finish(Cr.NS_OK);
1712 }
1713 }
1714 }.bind(copySaver),
1715
1716 onDataAvailable: function (aRequest, aContext, aInputStream,
1717 aOffset, aCount) {
1718 backgroundFileSaver.onDataAvailable(aRequest, aContext,
1719 aInputStream, aOffset,
1720 aCount);
1721 }.bind(copySaver),
1722 }, null);
1723
1724 // We should check if we have been canceled in the meantime, after
1725 // all the previous asynchronous operations have been executed and
1726 // just before we set the _backgroundFileSaver property.
1727 if (this._canceled) {
1728 throw new DownloadError({ message: "Saver canceled." });
1729 }
1730
1731 // If the operation succeeded, store the object to allow cancellation.
1732 this._backgroundFileSaver = backgroundFileSaver;
1733 } catch (ex) {
1734 // In case an error occurs while setting up the chain of objects for
1735 // the download, ensure that we release the resources of the saver.
1736 backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
1737 throw ex;
1738 }
1739
1740 // We will wait on this promise in case no error occurred while setting
1741 // up the chain of objects for the download.
1742 yield deferSaveComplete.promise;
1743 } catch (ex) {
1744 // Ensure we always remove the placeholder for the final target file on
1745 // failure, independently of which code path failed. In some cases, the
1746 // background file saver may have already removed the file.
1747 try {
1748 yield OS.File.remove(targetPath);
1749 } catch (e2) {
1750 // If we failed during the operation, we report the error but use the
1751 // original one as the failure reason of the download. Note that on
1752 // Windows we may get an access denied error instead of a no such file
1753 // error if the file existed before, and was recently deleted.
1754 if (!(e2 instanceof OS.File.Error &&
1755 (e2.becauseNoSuchFile || e2.becauseAccessDenied))) {
1756 Cu.reportError(e2);
1757 }
1758 }
1759 throw ex;
1760 }
1761 }.bind(this));
1762 },
1763
1764 /**
1765 * Implements "DownloadSaver.cancel".
1766 */
1767 cancel: function DCS_cancel()
1768 {
1769 this._canceled = true;
1770 if (this._backgroundFileSaver) {
1771 this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
1772 this._backgroundFileSaver = null;
1773 }
1774 },
1775
1776 /**
1777 * Implements "DownloadSaver.removePartialData".
1778 */
1779 removePartialData: function ()
1780 {
1781 return Task.spawn(function task_DCS_removePartialData() {
1782 if (this.download.target.partFilePath) {
1783 try {
1784 yield OS.File.remove(this.download.target.partFilePath);
1785 } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { }
1786 }
1787 }.bind(this));
1788 },
1789
1790 /**
1791 * Implements "DownloadSaver.toSerializable".
1792 */
1793 toSerializable: function ()
1794 {
1795 // Simplify the representation if we don't have other details.
1796 if (!this.entityID && !this._unknownProperties) {
1797 return "copy";
1798 }
1799
1800 let serializable = { type: "copy",
1801 entityID: this.entityID };
1802 serializeUnknownProperties(this, serializable);
1803 return serializable;
1804 },
1805
1806 /**
1807 * Implements "DownloadSaver.getSha256Hash"
1808 */
1809 getSha256Hash: function ()
1810 {
1811 return this._sha256Hash;
1812 },
1813
1814 /*
1815 * Implements DownloadSaver.getSignatureInfo.
1816 */
1817 getSignatureInfo: function ()
1818 {
1819 return this._signatureInfo;
1820 }
1821 };
1822
1823 /**
1824 * Creates a new DownloadCopySaver object, with its initial state derived from
1825 * its serializable representation.
1826 *
1827 * @param aSerializable
1828 * Serializable representation of a DownloadCopySaver object.
1829 *
1830 * @return The newly created DownloadCopySaver object.
1831 */
1832 this.DownloadCopySaver.fromSerializable = function (aSerializable) {
1833 let saver = new DownloadCopySaver();
1834 if ("entityID" in aSerializable) {
1835 saver.entityID = aSerializable.entityID;
1836 }
1837
1838 deserializeUnknownProperties(saver, aSerializable, property =>
1839 property != "entityID" && property != "type");
1840
1841 return saver;
1842 };
1843
1844 ////////////////////////////////////////////////////////////////////////////////
1845 //// DownloadLegacySaver
1846
1847 /**
1848 * Saver object that integrates with the legacy nsITransfer interface.
1849 *
1850 * For more background on the process, see the DownloadLegacyTransfer object.
1851 */
1852 this.DownloadLegacySaver = function()
1853 {
1854 this.deferExecuted = Promise.defer();
1855 this.deferCanceled = Promise.defer();
1856 }
1857
1858 this.DownloadLegacySaver.prototype = {
1859 __proto__: DownloadSaver.prototype,
1860
1861 /**
1862 * Save the SHA-256 hash in raw bytes of the downloaded file. This may be
1863 * null when nsExternalHelperAppService (and thus BackgroundFileSaver) is not
1864 * invoked.
1865 */
1866 _sha256Hash: null,
1867
1868 /**
1869 * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
1870 * if the file is signed. This is empty if the file is unsigned, and null
1871 * unless BackgroundFileSaver has successfully completed saving the file.
1872 */
1873 _signatureInfo: null,
1874
1875 /**
1876 * nsIRequest object associated to the status and progress updates we
1877 * received. This object is null before we receive the first status and
1878 * progress update, and is also reset to null when the download is stopped.
1879 */
1880 request: null,
1881
1882 /**
1883 * This deferred object contains a promise that is resolved as soon as this
1884 * download finishes successfully, and is rejected in case the download is
1885 * canceled or receives a failure notification through nsITransfer.
1886 */
1887 deferExecuted: null,
1888
1889 /**
1890 * This deferred object contains a promise that is resolved if the download
1891 * receives a cancellation request through the "cancel" method, and is never
1892 * rejected. The nsITransfer implementation will register a handler that
1893 * actually causes the download cancellation.
1894 */
1895 deferCanceled: null,
1896
1897 /**
1898 * This is populated with the value of the aSetProgressBytesFn argument of the
1899 * "execute" method, and is null before the method is called.
1900 */
1901 setProgressBytesFn: null,
1902
1903 /**
1904 * Called by the nsITransfer implementation while the download progresses.
1905 *
1906 * @param aCurrentBytes
1907 * Number of bytes transferred until now.
1908 * @param aTotalBytes
1909 * Total number of bytes to be transferred, or -1 if unknown.
1910 */
1911 onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes)
1912 {
1913 // Ignore progress notifications until we are ready to process them.
1914 if (!this.setProgressBytesFn) {
1915 return;
1916 }
1917
1918 let hasPartFile = !!this.download.target.partFilePath;
1919
1920 this.progressWasNotified = true;
1921 this.setProgressBytesFn(aCurrentBytes, aTotalBytes,
1922 aCurrentBytes > 0 && hasPartFile);
1923 },
1924
1925 /**
1926 * Whether the onProgressBytes function has been called at least once.
1927 */
1928 progressWasNotified: false,
1929
1930 /**
1931 * Called by the nsITransfer implementation when the request has started.
1932 *
1933 * @param aRequest
1934 * nsIRequest associated to the status update.
1935 * @param aAlreadyAddedToHistory
1936 * Indicates that the nsIExternalHelperAppService component already
1937 * added the download to the browsing history, unless it was started
1938 * from a private browsing window. When this parameter is false, the
1939 * download is added to the browsing history here. Private downloads
1940 * are never added to history even if this parameter is false.
1941 */
1942 onTransferStarted: function (aRequest, aAlreadyAddedToHistory)
1943 {
1944 // Store the entity ID to use for resuming if required.
1945 if (this.download.tryToKeepPartialData &&
1946 aRequest instanceof Ci.nsIResumableChannel) {
1947 try {
1948 // If reading the ID succeeds, the source is resumable.
1949 this.entityID = aRequest.entityID;
1950 } catch (ex if ex instanceof Components.Exception &&
1951 ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { }
1952 }
1953
1954 // For legacy downloads, we must update the referrer at this time.
1955 if (aRequest instanceof Ci.nsIHttpChannel && aRequest.referrer) {
1956 this.download.source.referrer = aRequest.referrer.spec;
1957 }
1958
1959 if (!aAlreadyAddedToHistory) {
1960 this.addToHistory();
1961 }
1962 },
1963
1964 /**
1965 * Called by the nsITransfer implementation when the request has finished.
1966 *
1967 * @param aRequest
1968 * nsIRequest associated to the status update.
1969 * @param aStatus
1970 * Status code received by the nsITransfer implementation.
1971 */
1972 onTransferFinished: function DLS_onTransferFinished(aRequest, aStatus)
1973 {
1974 // Store a reference to the request, used when handling completion.
1975 this.request = aRequest;
1976
1977 if (Components.isSuccessCode(aStatus)) {
1978 this.deferExecuted.resolve();
1979 } else {
1980 // Infer the origin of the error from the failure code, because more
1981 // specific data is not available through the nsITransfer implementation.
1982 let properties = { result: aStatus, inferCause: true };
1983 this.deferExecuted.reject(new DownloadError(properties));
1984 }
1985 },
1986
1987 /**
1988 * When the first execution of the download finished, it can be restarted by
1989 * using a DownloadCopySaver object instead of the original legacy component
1990 * that executed the download.
1991 */
1992 firstExecutionFinished: false,
1993
1994 /**
1995 * In case the download is restarted after the first execution finished, this
1996 * property contains a reference to the DownloadCopySaver that is executing
1997 * the new download attempt.
1998 */
1999 copySaver: null,
2000
2001 /**
2002 * String corresponding to the entityID property of the nsIResumableChannel
2003 * used to execute the download, or null if the channel was not resumable or
2004 * the saver was instructed not to keep partially downloaded data.
2005 */
2006 entityID: null,
2007
2008 /**
2009 * Implements "DownloadSaver.execute".
2010 */
2011 execute: function DLS_execute(aSetProgressBytesFn)
2012 {
2013 // Check if this is not the first execution of the download. The Download
2014 // object guarantees that this function is not re-entered during execution.
2015 if (this.firstExecutionFinished) {
2016 if (!this.copySaver) {
2017 this.copySaver = new DownloadCopySaver();
2018 this.copySaver.download = this.download;
2019 this.copySaver.entityID = this.entityID;
2020 this.copySaver.alreadyAddedToHistory = true;
2021 }
2022 return this.copySaver.execute.apply(this.copySaver, arguments);
2023 }
2024
2025 this.setProgressBytesFn = aSetProgressBytesFn;
2026
2027 return Task.spawn(function task_DLS_execute() {
2028 try {
2029 // Wait for the component that executes the download to finish.
2030 yield this.deferExecuted.promise;
2031
2032 // At this point, the "request" property has been populated. Ensure we
2033 // report the value of "Content-Length", if available, even if the
2034 // download didn't generate any progress events.
2035 if (!this.progressWasNotified &&
2036 this.request instanceof Ci.nsIChannel &&
2037 this.request.contentLength >= 0) {
2038 aSetProgressBytesFn(0, this.request.contentLength);
2039 }
2040
2041 // If the component executing the download provides the path of a
2042 // ".part" file, it means that it expects the listener to move the file
2043 // to its final target path when the download succeeds. In this case,
2044 // an empty ".part" file is created even if no data was received from
2045 // the source.
2046 if (this.download.target.partFilePath) {
2047 yield OS.File.move(this.download.target.partFilePath,
2048 this.download.target.path);
2049 } else {
2050 // The download implementation may not have created the target file if
2051 // no data was received from the source. In this case, ensure that an
2052 // empty file is created as expected.
2053 try {
2054 // This atomic operation is more efficient than an existence check.
2055 let file = yield OS.File.open(this.download.target.path,
2056 { create: true });
2057 yield file.close();
2058 } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { }
2059 }
2060 } catch (ex) {
2061 // Ensure we always remove the final target file on failure,
2062 // independently of which code path failed. In some cases, the
2063 // component executing the download may have already removed the file.
2064 try {
2065 yield OS.File.remove(this.download.target.path);
2066 } catch (e2) {
2067 // If we failed during the operation, we report the error but use the
2068 // original one as the failure reason of the download. Note that on
2069 // Windows we may get an access denied error instead of a no such file
2070 // error if the file existed before, and was recently deleted.
2071 if (!(e2 instanceof OS.File.Error &&
2072 (e2.becauseNoSuchFile || e2.becauseAccessDenied))) {
2073 Cu.reportError(e2);
2074 }
2075 }
2076 // In case the operation failed, ensure we stop downloading data. Since
2077 // we never re-enter this function, deferCanceled is always available.
2078 this.deferCanceled.resolve();
2079 throw ex;
2080 } finally {
2081 // We don't need the reference to the request anymore. We must also set
2082 // deferCanceled to null in order to free any indirect references it
2083 // may hold to the request.
2084 this.request = null;
2085 this.deferCanceled = null;
2086 // Allow the download to restart through a DownloadCopySaver.
2087 this.firstExecutionFinished = true;
2088 }
2089 }.bind(this));
2090 },
2091
2092 /**
2093 * Implements "DownloadSaver.cancel".
2094 */
2095 cancel: function DLS_cancel()
2096 {
2097 // We may be using a DownloadCopySaver to handle resuming.
2098 if (this.copySaver) {
2099 return this.copySaver.cancel.apply(this.copySaver, arguments);
2100 }
2101
2102 // If the download hasn't stopped already, resolve deferCanceled so that the
2103 // operation is canceled as soon as a cancellation handler is registered.
2104 // Note that the handler might not have been registered yet.
2105 if (this.deferCanceled) {
2106 this.deferCanceled.resolve();
2107 }
2108 },
2109
2110 /**
2111 * Implements "DownloadSaver.removePartialData".
2112 */
2113 removePartialData: function ()
2114 {
2115 // DownloadCopySaver and DownloadLeagcySaver use the same logic for removing
2116 // partially downloaded data, though this implementation isn't shared by
2117 // other saver types, thus it isn't found on their shared prototype.
2118 return DownloadCopySaver.prototype.removePartialData.call(this);
2119 },
2120
2121 /**
2122 * Implements "DownloadSaver.toSerializable".
2123 */
2124 toSerializable: function ()
2125 {
2126 // This object depends on legacy components that are created externally,
2127 // thus it cannot be rebuilt during deserialization. To support resuming
2128 // across different browser sessions, this object is transformed into a
2129 // DownloadCopySaver for the purpose of serialization.
2130 return DownloadCopySaver.prototype.toSerializable.call(this);
2131 },
2132
2133 /**
2134 * Implements "DownloadSaver.getSha256Hash".
2135 */
2136 getSha256Hash: function ()
2137 {
2138 if (this.copySaver) {
2139 return this.copySaver.getSha256Hash();
2140 }
2141 return this._sha256Hash;
2142 },
2143
2144 /**
2145 * Called by the nsITransfer implementation when the hash is available.
2146 */
2147 setSha256Hash: function (hash)
2148 {
2149 this._sha256Hash = hash;
2150 },
2151
2152 /**
2153 * Implements "DownloadSaver.getSignatureInfo".
2154 */
2155 getSignatureInfo: function ()
2156 {
2157 if (this.copySaver) {
2158 return this.copySaver.getSignatureInfo();
2159 }
2160 return this._signatureInfo;
2161 },
2162
2163 /**
2164 * Called by the nsITransfer implementation when the hash is available.
2165 */
2166 setSignatureInfo: function (signatureInfo)
2167 {
2168 this._signatureInfo = signatureInfo;
2169 },
2170 };
2171
2172 /**
2173 * Returns a new DownloadLegacySaver object. This saver type has a
2174 * deserializable form only when creating a new object in memory, because it
2175 * cannot be serialized to disk.
2176 */
2177 this.DownloadLegacySaver.fromSerializable = function () {
2178 return new DownloadLegacySaver();
2179 };

mercurial