Source code

Revision control

Other Tools

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