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
* Provides functions to integrate with the host application, handling for
7
* example the global prompts on shutdown.
8
*/
9
10
"use strict";
11
12
var EXPORTED_SYMBOLS = [
13
"DownloadIntegration",
14
];
15
16
const {Integration} = ChromeUtils.import("resource://gre/modules/Integration.jsm");
17
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
18
19
ChromeUtils.defineModuleGetter(this, "AsyncShutdown",
21
ChromeUtils.defineModuleGetter(this, "AppConstants",
23
ChromeUtils.defineModuleGetter(this, "DeferredTask",
25
ChromeUtils.defineModuleGetter(this, "Downloads",
27
ChromeUtils.defineModuleGetter(this, "DownloadStore",
29
ChromeUtils.defineModuleGetter(this, "DownloadUIHelper",
31
ChromeUtils.defineModuleGetter(this, "FileUtils",
33
ChromeUtils.defineModuleGetter(this, "NetUtil",
35
ChromeUtils.defineModuleGetter(this, "OS",
37
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
39
ChromeUtils.defineModuleGetter(this, "Services",
41
ChromeUtils.defineModuleGetter(this, "NetUtil",
43
ChromeUtils.defineModuleGetter(this, "CloudStorage",
45
46
XPCOMUtils.defineLazyServiceGetter(this, "gDownloadPlatform",
47
"@mozilla.org/toolkit/download-platform;1",
48
"mozIDownloadPlatform");
49
XPCOMUtils.defineLazyServiceGetter(this, "gEnvironment",
50
"@mozilla.org/process/environment;1",
51
"nsIEnvironment");
52
XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
53
"@mozilla.org/mime;1",
54
"nsIMIMEService");
55
XPCOMUtils.defineLazyServiceGetter(this, "gExternalProtocolService",
56
"@mozilla.org/uriloader/external-protocol-service;1",
57
"nsIExternalProtocolService");
58
ChromeUtils.defineModuleGetter(this, "RuntimePermissions",
60
61
XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
62
if ("@mozilla.org/parental-controls-service;1" in Cc) {
63
return Cc["@mozilla.org/parental-controls-service;1"]
64
.createInstance(Ci.nsIParentalControlsService);
65
}
66
return null;
67
});
68
69
XPCOMUtils.defineLazyServiceGetter(this, "gApplicationReputationService",
70
"@mozilla.org/reputationservice/application-reputation-service;1",
71
Ci.nsIApplicationReputationService);
72
73
// We have to use the gCombinedDownloadIntegration identifier because, in this
74
// module only, the DownloadIntegration identifier refers to the base version.
75
/* global gCombinedDownloadIntegration:false */
76
Integration.downloads.defineModuleGetter(this, "gCombinedDownloadIntegration",
78
"DownloadIntegration");
79
80
const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
81
"initWithCallback");
82
83
/**
84
* Indicates the delay between a change to the downloads data and the related
85
* save operation.
86
*
87
* For best efficiency, this value should be high enough that the input/output
88
* for opening or closing the target file does not overlap with the one for
89
* saving the list of downloads.
90
*/
91
const kSaveDelayMs = 1500;
92
93
/**
94
* List of observers to listen against
95
*/
96
const kObserverTopics = [
97
"quit-application-requested",
98
"offline-requested",
99
"last-pb-context-exiting",
100
"last-pb-context-exited",
101
"sleep_notification",
102
"suspend_process_notification",
103
"wake_notification",
104
"resume_process_notification",
105
"network:offline-about-to-go-offline",
106
"network:offline-status-changed",
107
"xpcom-will-shutdown",
108
];
109
110
/**
111
* Maps nsIApplicationReputationService verdicts with the DownloadError ones.
112
*/
113
const kVerdictMap = {
114
[Ci.nsIApplicationReputationService.VERDICT_DANGEROUS]:
115
Downloads.Error.BLOCK_VERDICT_MALWARE,
116
[Ci.nsIApplicationReputationService.VERDICT_UNCOMMON]:
117
Downloads.Error.BLOCK_VERDICT_UNCOMMON,
118
[Ci.nsIApplicationReputationService.VERDICT_POTENTIALLY_UNWANTED]:
119
Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
120
[Ci.nsIApplicationReputationService.VERDICT_DANGEROUS_HOST]:
121
Downloads.Error.BLOCK_VERDICT_MALWARE,
122
};
123
124
/**
125
* Provides functions to integrate with the host application, handling for
126
* example the global prompts on shutdown.
127
*/
128
var DownloadIntegration = {
129
/**
130
* Main DownloadStore object for loading and saving the list of persistent
131
* downloads, or null if the download list was never requested and thus it
132
* doesn't need to be persisted.
133
*/
134
_store: null,
135
136
/**
137
* Returns whether data for blocked downloads should be kept on disk.
138
* Implementations which support unblocking downloads may return true to
139
* keep the blocked download on disk until its fate is decided.
140
*
141
* If a download is blocked and the partial data is kept the Download's
142
* 'hasBlockedData' property will be true. In this state Download.unblock()
143
* or Download.confirmBlock() may be used to either unblock the download or
144
* remove the downloaded data respectively.
145
*
146
* Even if shouldKeepBlockedData returns true, if the download did not use a
147
* partFile the blocked data will be removed - preventing the complete
148
* download from existing on disk with its final filename.
149
*
150
* @return boolean True if data should be kept.
151
*/
152
shouldKeepBlockedData() {
153
const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
154
return Services.appinfo.ID == FIREFOX_ID;
155
},
156
157
/**
158
* Performs initialization of the list of persistent downloads, before its
159
* first use by the host application. This function may be called only once
160
* during the entire lifetime of the application.
161
*
162
* @param list
163
* DownloadList object to be initialized.
164
*
165
* @return {Promise}
166
* @resolves When the list has been initialized.
167
* @rejects JavaScript exception.
168
*/
169
async initializePublicDownloadList(list) {
170
try {
171
await this.loadPublicDownloadListFromStore(list);
172
} catch (ex) {
173
Cu.reportError(ex);
174
}
175
176
if (AppConstants.MOZ_PLACES) {
177
// After the list of persistent downloads has been loaded, we can add the
178
// history observers, even if the load operation failed. This object is kept
179
// alive by the history service.
180
new DownloadHistoryObserver(list);
181
}
182
},
183
184
/**
185
* Called by initializePublicDownloadList to load the list of persistent
186
* downloads, before its first use by the host application. This function may
187
* be called only once during the entire lifetime of the application.
188
*
189
* @param list
190
* DownloadList object to be populated with the download objects
191
* serialized from the previous session. This list will be persisted
192
* to disk during the session lifetime.
193
*
194
* @return {Promise}
195
* @resolves When the list has been populated.
196
* @rejects JavaScript exception.
197
*/
198
async loadPublicDownloadListFromStore(list) {
199
if (this._store) {
200
throw new Error("Initialization may be performed only once.");
201
}
202
203
this._store = new DownloadStore(list, OS.Path.join(
204
OS.Constants.Path.profileDir,
205
"downloads.json"));
206
this._store.onsaveitem = this.shouldPersistDownload.bind(this);
207
208
try {
209
await this._store.load();
210
} catch (ex) {
211
Cu.reportError(ex);
212
}
213
214
// Add the view used for detecting changes to downloads to be persisted.
215
// We must do this after the list of persistent downloads has been loaded,
216
// even if the load operation failed. We wait for a complete initialization
217
// so other callers cannot modify the list without being detected. The
218
// DownloadAutoSaveView is kept alive by the underlying DownloadList.
219
await new DownloadAutoSaveView(list, this._store).initialize();
220
},
221
222
/**
223
* Determines if a Download object from the list of persistent downloads
224
* should be saved into a file, so that it can be restored across sessions.
225
*
226
* This function allows filtering out downloads that the host application is
227
* not interested in persisting across sessions, for example downloads that
228
* finished successfully.
229
*
230
* @param aDownload
231
* The Download object to be inspected. This is originally taken from
232
* the global DownloadList object for downloads that were not started
233
* from a private browsing window. The item may have been removed
234
* from the list since the save operation started, though in this case
235
* the save operation will be repeated later.
236
*
237
* @return True to save the download, false otherwise.
238
*/
239
shouldPersistDownload(aDownload) {
240
// On all platforms, we save all the downloads currently in progress, as
241
// well as stopped downloads for which we retained partially downloaded
242
// data or we have blocked data.
243
// On Android we store all history; on Desktop, stopped downloads for which
244
// we don't need to track the presence of a ".part" file are only retained
245
// in the browser history.
246
return !aDownload.stopped || aDownload.hasPartialData ||
247
aDownload.hasBlockedData || AppConstants.platform == "android";
248
},
249
250
/**
251
* Returns the system downloads directory asynchronously.
252
*
253
* @return {Promise}
254
* @resolves The downloads directory string path.
255
*/
256
async getSystemDownloadsDirectory() {
257
if (this._downloadsDirectory) {
258
return this._downloadsDirectory;
259
}
260
261
if (AppConstants.platform == "android") {
262
// Android doesn't have a $HOME directory, and by default we only have
263
// write access to /data/data/org.mozilla.{$APP} and /sdcard
264
this._downloadsDirectory = gEnvironment.get("DOWNLOADS_DIRECTORY");
265
if (!this._downloadsDirectory) {
266
throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
267
Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
268
}
269
} else {
270
try {
271
this._downloadsDirectory = this._getDirectory("DfltDwnld");
272
} catch (e) {
273
this._downloadsDirectory = await this._createDownloadsDirectory("Home");
274
}
275
}
276
277
return this._downloadsDirectory;
278
},
279
_downloadsDirectory: null,
280
281
/**
282
* Returns the user downloads directory asynchronously.
283
*
284
* @return {Promise}
285
* @resolves The downloads directory string path.
286
*/
287
async getPreferredDownloadsDirectory() {
288
let directoryPath = null;
289
let prefValue = Services.prefs.getIntPref("browser.download.folderList", 1);
290
291
switch (prefValue) {
292
case 0: // Desktop
293
directoryPath = this._getDirectory("Desk");
294
break;
295
case 1: // Downloads
296
directoryPath = await this.getSystemDownloadsDirectory();
297
break;
298
case 2: // Custom
299
try {
300
let directory = Services.prefs.getComplexValue("browser.download.dir",
301
Ci.nsIFile);
302
directoryPath = directory.path;
303
await OS.File.makeDir(directoryPath, { ignoreExisting: true });
304
} catch (ex) {
305
// Either the preference isn't set or the directory cannot be created.
306
directoryPath = await this.getSystemDownloadsDirectory();
307
}
308
break;
309
case 3: // Cloud Storage
310
try {
311
directoryPath = await CloudStorage.getDownloadFolder();
312
} catch (ex) {}
313
if (!directoryPath) {
314
directoryPath = await this.getSystemDownloadsDirectory();
315
}
316
break;
317
default:
318
directoryPath = await this.getSystemDownloadsDirectory();
319
}
320
return directoryPath;
321
},
322
323
/**
324
* Returns the temporary downloads directory asynchronously.
325
*
326
* @return {Promise}
327
* @resolves The downloads directory string path.
328
*/
329
async getTemporaryDownloadsDirectory() {
330
let directoryPath = null;
331
if (AppConstants.platform == "macosx") {
332
directoryPath = await this.getPreferredDownloadsDirectory();
333
} else if (AppConstants.platform == "android") {
334
directoryPath = await this.getSystemDownloadsDirectory();
335
} else {
336
directoryPath = this._getDirectory("TmpD");
337
}
338
return directoryPath;
339
},
340
341
/**
342
* Checks to determine whether to block downloads for parental controls.
343
*
344
* aParam aDownload
345
* The download object.
346
*
347
* @return {Promise}
348
* @resolves The boolean indicates to block downloads or not.
349
*/
350
shouldBlockForParentalControls(aDownload) {
351
let isEnabled = gParentalControlsService &&
352
gParentalControlsService.parentalControlsEnabled;
353
let shouldBlock = isEnabled &&
354
gParentalControlsService.blockFileDownloadsEnabled;
355
356
// Log the event if required by parental controls settings.
357
if (isEnabled && gParentalControlsService.loggingEnabled) {
358
gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload,
359
shouldBlock,
360
NetUtil.newURI(aDownload.source.url), null);
361
}
362
363
return Promise.resolve(shouldBlock);
364
},
365
366
/**
367
* Checks to determine whether to block downloads for not granted runtime permissions.
368
*
369
* @return {Promise}
370
* @resolves The boolean indicates to block downloads or not.
371
*/
372
async shouldBlockForRuntimePermissions() {
373
return AppConstants.platform == "android" &&
374
!(await RuntimePermissions.waitForPermissions(
375
RuntimePermissions.WRITE_EXTERNAL_STORAGE));
376
},
377
378
/**
379
* Checks to determine whether to block downloads because they might be
380
* malware, based on application reputation checks.
381
*
382
* aParam aDownload
383
* The download object.
384
*
385
* @return {Promise}
386
* @resolves Object with the following properties:
387
* {
388
* shouldBlock: Whether the download should be blocked.
389
* verdict: Detailed reason for the block, according to the
390
* "Downloads.Error.BLOCK_VERDICT_" constants, or empty
391
* string if the reason is unknown.
392
* }
393
*/
394
shouldBlockForReputationCheck(aDownload) {
395
let hash;
396
let sigInfo;
397
let channelRedirects;
398
try {
399
hash = aDownload.saver.getSha256Hash();
400
sigInfo = aDownload.saver.getSignatureInfo();
401
channelRedirects = aDownload.saver.getRedirects();
402
} catch (ex) {
403
// Bail if DownloadSaver doesn't have a hash or signature info.
404
return Promise.resolve({
405
shouldBlock: false,
406
verdict: "",
407
});
408
}
409
if (!hash || !sigInfo) {
410
return Promise.resolve({
411
shouldBlock: false,
412
verdict: "",
413
});
414
}
415
return new Promise(resolve => {
416
let aReferrer = null;
417
if (aDownload.source.referrer) {
418
aReferrer = NetUtil.newURI(aDownload.source.referrer);
419
}
420
gApplicationReputationService.queryReputation({
421
sourceURI: NetUtil.newURI(aDownload.source.url),
422
referrerURI: aReferrer,
423
fileSize: aDownload.currentBytes,
424
sha256Hash: hash,
425
suggestedFileName: OS.Path.basename(aDownload.target.path),
426
signatureInfo: sigInfo,
427
redirects: channelRedirects },
428
function onComplete(aShouldBlock, aRv, aVerdict) {
429
resolve({
430
shouldBlock: aShouldBlock,
431
verdict: (aShouldBlock && kVerdictMap[aVerdict]) || "",
432
});
433
});
434
});
435
},
436
437
/**
438
* Checks whether downloaded files should be marked as coming from
439
* Internet Zone.
440
*
441
* @return true if files should be marked
442
*/
443
_shouldSaveZoneInformation() {
444
let key = Cc["@mozilla.org/windows-registry-key;1"]
445
.createInstance(Ci.nsIWindowsRegKey);
446
try {
447
key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
448
"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments",
449
Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE);
450
try {
451
return key.readIntValue("SaveZoneInformation") != 1;
452
} finally {
453
key.close();
454
}
455
} catch (ex) {
456
// If the key is not present, files should be marked by default.
457
return true;
458
}
459
},
460
461
/**
462
* Builds a key and URL value pair for the "Zone.Identifier" Alternate Data
463
* Stream.
464
*
465
* @param aKey
466
* String to write before the "=" sign. This is not validated.
467
* @param aUrl
468
* URL string to write after the "=" sign. Only the "http(s)" and
469
* "ftp" schemes are allowed, and usernames and passwords are
470
* stripped.
471
* @param [optional] aFallback
472
* Value to place after the "=" sign in case the URL scheme is not
473
* allowed. If unspecified, an empty string is returned when the
474
* scheme is not allowed.
475
*
476
* @return Line to add to the stream, including the final CRLF, or an empty
477
* string if the validation failed.
478
*/
479
_zoneIdKey(aKey, aUrl, aFallback) {
480
try {
481
let url;
482
const uri = NetUtil.newURI(aUrl);
483
if (["http", "https", "ftp"].includes(uri.scheme)) {
484
url = uri.mutate().setUserPass("").finalize().spec;
485
} else if (aFallback) {
486
url = aFallback;
487
} else {
488
return "";
489
}
490
return aKey + "=" + url + "\r\n";
491
} catch (e) {
492
return "";
493
}
494
},
495
496
/**
497
* Performs platform-specific operations when a download is done.
498
*
499
* aParam aDownload
500
* The Download object.
501
*
502
* @return {Promise}
503
* @resolves When all the operations completed successfully.
504
* @rejects JavaScript exception if any of the operations failed.
505
*/
506
async downloadDone(aDownload) {
507
// On Windows, we mark any file saved to the NTFS file system as coming
508
// from the Internet security zone unless Group Policy disables the
509
// feature. We do this by writing to the "Zone.Identifier" Alternate
510
// Data Stream directly, because the Save method of the
511
// IAttachmentExecute interface would trigger operations that may cause
512
// the application to hang, or other performance issues.
513
// The stream created in this way is forward-compatible with all the
514
// current and future versions of Windows.
515
if (AppConstants.platform == "win" && this._shouldSaveZoneInformation()) {
516
let zone;
517
try {
518
zone = gDownloadPlatform.mapUrlToZone(aDownload.source.url);
519
} catch (e) {
520
// Default to Internet Zone if mapUrlToZone failed for
521
// whatever reason.
522
zone = Ci.mozIDownloadPlatform.ZONE_INTERNET;
523
}
524
try {
525
// Don't write zone IDs for Local, Intranet, or Trusted sites
526
// to match Windows behavior.
527
if (zone >= Ci.mozIDownloadPlatform.ZONE_INTERNET) {
528
let streamPath = aDownload.target.path + ":Zone.Identifier";
529
let stream = await OS.File.open(
530
streamPath,
531
{ create: true },
532
{ winAllowLengthBeyondMaxPathWithCaveats: true }
533
);
534
try {
535
let zoneId = "[ZoneTransfer]\r\nZoneId=" + zone + "\r\n";
536
if (!aDownload.source.isPrivate) {
537
zoneId +=
538
this._zoneIdKey("ReferrerUrl", aDownload.source.referrer) +
539
this._zoneIdKey("HostUrl", aDownload.source.url, "about:internet");
540
}
541
await stream.write(new TextEncoder().encode(zoneId));
542
} finally {
543
await stream.close();
544
}
545
}
546
} catch (ex) {
547
// If writing to the stream fails, we ignore the error and continue.
548
// The Windows API error 123 (ERROR_INVALID_NAME) is expected to
549
// occur when working on a file system that does not support
550
// Alternate Data Streams, like FAT32, thus we don't report this
551
// specific error.
552
if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) {
553
Cu.reportError(ex);
554
}
555
}
556
}
557
558
// The file with the partially downloaded data has restrictive permissions
559
// that don't allow other users on the system to access it. Now that the
560
// download is completed, we need to adjust permissions based on whether
561
// this is a permanently downloaded file or a temporary download to be
562
// opened read-only with an external application.
563
try {
564
// The following logic to determine whether this is a temporary download
565
// is due to the fact that "deleteTempFileOnExit" is false on Mac, where
566
// downloads to be opened with external applications are preserved in
567
// the "Downloads" folder like normal downloads.
568
let isTemporaryDownload =
569
aDownload.launchWhenSucceeded && (aDownload.source.isPrivate ||
570
Services.prefs.getBoolPref("browser.helperApps.deleteTempFileOnExit"));
571
// Permanently downloaded files are made accessible by other users on
572
// this system, while temporary downloads are marked as read-only.
573
let options = {};
574
if (isTemporaryDownload) {
575
options.unixMode = 0o400;
576
options.winAttributes = {readOnly: true};
577
} else {
578
options.unixMode = 0o666;
579
}
580
// On Unix, the umask of the process is respected.
581
await OS.File.setPermissions(aDownload.target.path, options);
582
} catch (ex) {
583
// We should report errors with making the permissions less restrictive
584
// or marking the file as read-only on Unix and Mac, but this should not
585
// prevent the download from completing.
586
// The setPermissions API error EPERM is expected to occur when working
587
// on a file system that does not support file permissions, like FAT32,
588
// thus we don't report this error.
589
if (!(ex instanceof OS.File.Error) || ex.unixErrno != OS.Constants.libc.EPERM) {
590
Cu.reportError(ex);
591
}
592
}
593
594
let aReferrer = null;
595
if (aDownload.source.referrer) {
596
aReferrer = NetUtil.newURI(aDownload.source.referrer);
597
}
598
599
await gDownloadPlatform.downloadDone(
600
NetUtil.newURI(aDownload.source.url),
601
aReferrer,
602
new FileUtils.File(aDownload.target.path),
603
aDownload.contentType,
604
aDownload.source.isPrivate
605
);
606
},
607
608
/**
609
* Launches a file represented by the target of a download. This can
610
* open the file with the default application for the target MIME type
611
* or file extension, or with a custom application if
612
* aDownload.launcherPath is set.
613
*
614
* @param aDownload
615
* A Download object that contains the necessary information
616
* to launch the file. The relevant properties are: the target
617
* file, the contentType and the custom application chosen
618
* to launch it.
619
*
620
* @return {Promise}
621
* @resolves When the instruction to launch the file has been
622
* successfully given to the operating system. Note that
623
* the OS might still take a while until the file is actually
624
* launched.
625
* @rejects JavaScript exception if there was an error trying to launch
626
* the file.
627
*/
628
async launchDownload(aDownload) {
629
let file = new FileUtils.File(aDownload.target.path);
630
631
// In case of a double extension, like ".tar.gz", we only
632
// consider the last one, because the MIME service cannot
633
// handle multiple extensions.
634
let fileExtension = null, mimeInfo = null;
635
let match = file.leafName.match(/\.([^.]+)$/);
636
if (match) {
637
fileExtension = match[1];
638
}
639
640
let isWindowsExe = AppConstants.platform == "win" &&
641
fileExtension && fileExtension.toLowerCase() == "exe";
642
643
// Ask for confirmation if the file is executable, except for .exe on
644
// Windows where the operating system will show the prompt based on the
645
// security zone. We do this here, instead of letting the caller handle
646
// the prompt separately in the user interface layer, for two reasons. The
647
// first is because of its security nature, so that add-ons cannot forget
648
// to do this check. The second is that the system-level security prompt
649
// would be displayed at launch time in any case.
650
if (file.isExecutable() && !isWindowsExe &&
651
!(await this.confirmLaunchExecutable(file.path))) {
652
return;
653
}
654
655
try {
656
// The MIME service might throw if contentType == "" and it can't find
657
// a MIME type for the given extension, so we'll treat this case as
658
// an unknown mimetype.
659
mimeInfo = gMIMEService.getFromTypeAndExtension(aDownload.contentType,
660
fileExtension);
661
} catch (e) { }
662
663
if (aDownload.launcherPath) {
664
if (!mimeInfo) {
665
// This should not happen on normal circumstances because launcherPath
666
// is only set when we had an instance of nsIMIMEInfo to retrieve
667
// the custom application chosen by the user.
668
throw new Error(
669
"Unable to create nsIMIMEInfo to launch a custom application");
670
}
671
672
// Custom application chosen
673
let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
674
.createInstance(Ci.nsILocalHandlerApp);
675
localHandlerApp.executable = new FileUtils.File(aDownload.launcherPath);
676
677
mimeInfo.preferredApplicationHandler = localHandlerApp;
678
mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
679
680
this.launchFile(file, mimeInfo);
681
return;
682
}
683
684
// No custom application chosen, let's launch the file with the default
685
// handler. First, let's try to launch it through the MIME service.
686
if (mimeInfo) {
687
mimeInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
688
689
try {
690
this.launchFile(file, mimeInfo);
691
return;
692
} catch (ex) { }
693
}
694
695
// If it didn't work or if there was no MIME info available,
696
// let's try to directly launch the file.
697
try {
698
this.launchFile(file);
699
return;
700
} catch (ex) { }
701
702
// If our previous attempts failed, try sending it through
703
// the system's external "file:" URL handler.
704
gExternalProtocolService.loadURI(NetUtil.newURI(file));
705
},
706
707
/**
708
* Asks for confirmation for launching the specified executable file. This
709
* can be overridden by regression tests to avoid the interactive prompt.
710
*/
711
async confirmLaunchExecutable(path) {
712
// We don't anchor the prompt to a specific window intentionally, not
713
// only because this is the same behavior as the system-level prompt,
714
// but also because the most recently active window is the right choice
715
// in basically all cases.
716
return DownloadUIHelper.getPrompter().confirmLaunchExecutable(path);
717
},
718
719
/**
720
* Launches the specified file, unless overridden by regression tests.
721
*/
722
launchFile(file, mimeInfo) {
723
if (mimeInfo) {
724
mimeInfo.launchWithFile(file);
725
} else {
726
file.launch();
727
}
728
},
729
730
/**
731
* Shows the containing folder of a file.
732
*
733
* @param aFilePath
734
* The path to the file.
735
*
736
* @return {Promise}
737
* @resolves When the instruction to open the containing folder has been
738
* successfully given to the operating system. Note that
739
* the OS might still take a while until the folder is actually
740
* opened.
741
* @rejects JavaScript exception if there was an error trying to open
742
* the containing folder.
743
*/
744
async showContainingDirectory(aFilePath) {
745
let file = new FileUtils.File(aFilePath);
746
747
try {
748
// Show the directory containing the file and select the file.
749
file.reveal();
750
return;
751
} catch (ex) { }
752
753
// If reveal fails for some reason (e.g., it's not implemented on unix
754
// or the file doesn't exist), try using the parent if we have it.
755
let parent = file.parent;
756
if (!parent) {
757
throw new Error(
758
"Unexpected reference to a top-level directory instead of a file");
759
}
760
761
try {
762
// Open the parent directory to show where the file should be.
763
parent.launch();
764
return;
765
} catch (ex) { }
766
767
// If launch also fails (probably because it's not implemented), let
768
// the OS handler try to open the parent.
769
gExternalProtocolService.loadURI(NetUtil.newURI(parent));
770
},
771
772
/**
773
* Calls the directory service, create a downloads directory and returns an
774
* nsIFile for the downloads directory.
775
*
776
* @return {Promise}
777
* @resolves The directory string path.
778
*/
779
_createDownloadsDirectory(aName) {
780
// We read the name of the directory from the list of translated strings
781
// that is kept by the UI helper module, even if this string is not strictly
782
// displayed in the user interface.
783
let directoryPath = OS.Path.join(this._getDirectory(aName),
784
DownloadUIHelper.strings.downloadsFolder);
785
786
// Create the Downloads folder and ignore if it already exists.
787
return OS.File.makeDir(directoryPath, { ignoreExisting: true })
788
.then(() => directoryPath);
789
},
790
791
/**
792
* Returns the string path for the given directory service location name. This
793
* can be overridden by regression tests to return the path of the system
794
* temporary directory in all cases.
795
*/
796
_getDirectory(name) {
797
return Services.dirsvc.get(name, Ci.nsIFile).path;
798
},
799
800
/**
801
* Register the downloads interruption observers.
802
*
803
* @param aList
804
* The public or private downloads list.
805
* @param aIsPrivate
806
* True if the list is private, false otherwise.
807
*
808
* @return {Promise}
809
* @resolves When the views and observers are added.
810
*/
811
addListObservers(aList, aIsPrivate) {
812
DownloadObserver.registerView(aList, aIsPrivate);
813
if (!DownloadObserver.observersAdded) {
814
DownloadObserver.observersAdded = true;
815
for (let topic of kObserverTopics) {
816
Services.obs.addObserver(DownloadObserver, topic);
817
}
818
}
819
return Promise.resolve();
820
},
821
822
/**
823
* Force a save on _store if it exists. Used to ensure downloads do not
824
* persist after being sanitized on Android.
825
*
826
* @return {Promise}
827
* @resolves When _store.save() completes.
828
*/
829
forceSave() {
830
if (this._store) {
831
return this._store.save();
832
}
833
return Promise.resolve();
834
},
835
};
836
837
var DownloadObserver = {
838
/**
839
* Flag to determine if the observers have been added previously.
840
*/
841
observersAdded: false,
842
843
/**
844
* Timer used to delay restarting canceled downloads upon waking and returning
845
* online.
846
*/
847
_wakeTimer: null,
848
849
/**
850
* Set that contains the in progress publics downloads.
851
* It's kept updated when a public download is added, removed or changes its
852
* properties.
853
*/
854
_publicInProgressDownloads: new Set(),
855
856
/**
857
* Set that contains the in progress private downloads.
858
* It's kept updated when a private download is added, removed or changes its
859
* properties.
860
*/
861
_privateInProgressDownloads: new Set(),
862
863
/**
864
* Set that contains the downloads that have been canceled when going offline
865
* or to sleep. These are started again when returning online or waking. This
866
* list is not persisted so when exiting and restarting, the downloads will not
867
* be started again.
868
*/
869
_canceledOfflineDownloads: new Set(),
870
871
/**
872
* Registers a view that updates the corresponding downloads state set, based
873
* on the aIsPrivate argument. The set is updated when a download is added,
874
* removed or changes its properties.
875
*
876
* @param aList
877
* The public or private downloads list.
878
* @param aIsPrivate
879
* True if the list is private, false otherwise.
880
*/
881
registerView: function DO_registerView(aList, aIsPrivate) {
882
let downloadsSet = aIsPrivate ? this._privateInProgressDownloads
883
: this._publicInProgressDownloads;
884
let downloadsView = {
885
onDownloadAdded: aDownload => {
886
if (!aDownload.stopped) {
887
downloadsSet.add(aDownload);
888
}
889
},
890
onDownloadChanged: aDownload => {
891
if (aDownload.stopped) {
892
downloadsSet.delete(aDownload);
893
} else {
894
downloadsSet.add(aDownload);
895
}
896
},
897
onDownloadRemoved: aDownload => {
898
downloadsSet.delete(aDownload);
899
// The download must also be removed from the canceled when offline set.
900
this._canceledOfflineDownloads.delete(aDownload);
901
},
902
};
903
904
// We register the view asynchronously.
905
aList.addView(downloadsView).catch(Cu.reportError);
906
},
907
908
/**
909
* Wrapper that handles the test mode before calling the prompt that display
910
* a warning message box that informs that there are active downloads,
911
* and asks whether the user wants to cancel them or not.
912
*
913
* @param aCancel
914
* The observer notification subject.
915
* @param aDownloadsCount
916
* The current downloads count.
917
* @param aPrompter
918
* The prompter object that shows the confirm dialog.
919
* @param aPromptType
920
* The type of prompt notification depending on the observer.
921
*/
922
_confirmCancelDownloads: function DO_confirmCancelDownload(
923
aCancel, aDownloadsCount, aPromptType) {
924
// Handle test mode
925
if (gCombinedDownloadIntegration._testPromptDownloads) {
926
gCombinedDownloadIntegration._testPromptDownloads = aDownloadsCount;
927
return;
928
}
929
930
if (!aDownloadsCount) {
931
return;
932
}
933
934
// If user has already dismissed the request, then do nothing.
935
if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) {
936
return;
937
}
938
939
let prompter = DownloadUIHelper.getPrompter();
940
aCancel.data = prompter.confirmCancelDownloads(aDownloadsCount,
941
prompter[aPromptType]);
942
},
943
944
/**
945
* Resume all downloads that were paused when going offline, used when waking
946
* from sleep or returning from being offline.
947
*/
948
_resumeOfflineDownloads: function DO_resumeOfflineDownloads() {
949
this._wakeTimer = null;
950
951
for (let download of this._canceledOfflineDownloads) {
952
download.start().catch(() => {});
953
}
954
},
955
956
// nsIObserver
957
observe: function DO_observe(aSubject, aTopic, aData) {
958
let downloadsCount;
959
switch (aTopic) {
960
case "quit-application-requested":
961
downloadsCount = this._publicInProgressDownloads.size +
962
this._privateInProgressDownloads.size;
963
this._confirmCancelDownloads(aSubject, downloadsCount, "ON_QUIT");
964
break;
965
case "offline-requested":
966
downloadsCount = this._publicInProgressDownloads.size +
967
this._privateInProgressDownloads.size;
968
this._confirmCancelDownloads(aSubject, downloadsCount, "ON_OFFLINE");
969
break;
970
case "last-pb-context-exiting":
971
downloadsCount = this._privateInProgressDownloads.size;
972
this._confirmCancelDownloads(aSubject, downloadsCount,
973
"ON_LEAVE_PRIVATE_BROWSING");
974
break;
975
case "last-pb-context-exited":
976
let promise = (async function() {
977
let list = await Downloads.getList(Downloads.PRIVATE);
978
let downloads = await list.getAll();
979
980
// We can remove the downloads and finalize them in parallel.
981
for (let download of downloads) {
982
list.remove(download).catch(Cu.reportError);
983
download.finalize(true).catch(Cu.reportError);
984
}
985
})();
986
// Handle test mode
987
if (gCombinedDownloadIntegration._testResolveClearPrivateList) {
988
gCombinedDownloadIntegration._testResolveClearPrivateList(promise);
989
} else {
990
promise.catch(ex => Cu.reportError(ex));
991
}
992
break;
993
case "sleep_notification":
994
case "suspend_process_notification":
995
case "network:offline-about-to-go-offline":
996
for (let download of this._publicInProgressDownloads) {
997
download.cancel();
998
this._canceledOfflineDownloads.add(download);
999
}
1000
for (let download of this._privateInProgressDownloads) {
1001
download.cancel();
1002
this._canceledOfflineDownloads.add(download);
1003
}
1004
break;
1005
case "wake_notification":
1006
case "resume_process_notification":
1007
let wakeDelay =
1008
Services.prefs.getIntPref("browser.download.manager.resumeOnWakeDelay", 10000);
1009
1010
if (wakeDelay >= 0) {
1011
this._wakeTimer = new Timer(this._resumeOfflineDownloads.bind(this), wakeDelay,
1012
Ci.nsITimer.TYPE_ONE_SHOT);
1013
}
1014
break;
1015
case "network:offline-status-changed":
1016
if (aData == "online") {
1017
this._resumeOfflineDownloads();
1018
}
1019
break;
1020
// We need to unregister observers explicitly before we reach the
1021
// "xpcom-shutdown" phase, otherwise observers may be notified when some
1022
// required services are not available anymore. We can't unregister
1023
// observers on "quit-application", because this module is also loaded
1024
// during "make package" automation, and the quit notification is not sent
1025
// in that execution environment (bug 973637).
1026
case "xpcom-will-shutdown":
1027
for (let topic of kObserverTopics) {
1028
Services.obs.removeObserver(this, topic);
1029
}
1030
break;
1031
}
1032
},
1033
1034
QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
1035
};
1036
1037
/**
1038
* Registers a Places observer so that operations on download history are
1039
* reflected on the provided list of downloads.
1040
*
1041
* You do not need to keep a reference to this object in order to keep it alive,
1042
* because the history service already keeps a strong reference to it.
1043
*
1044
* @param aList
1045
* DownloadList object linked to this observer.
1046
*/
1047
var DownloadHistoryObserver = function(aList) {
1048
this._list = aList;
1049
PlacesUtils.history.addObserver(this);
1050
};
1051
1052
this.DownloadHistoryObserver.prototype = {
1053
/**
1054
* DownloadList object linked to this observer.
1055
*/
1056
_list: null,
1057
1058
QueryInterface: ChromeUtils.generateQI([Ci.nsINavHistoryObserver]),
1059
1060
// nsINavHistoryObserver
1061
onDeleteURI: function DL_onDeleteURI(aURI, aGUID) {
1062
this._list.removeFinished(download => aURI.equals(NetUtil.newURI(
1063
download.source.url)));
1064
},
1065
1066
// nsINavHistoryObserver
1067
onClearHistory: function DL_onClearHistory() {
1068
this._list.removeFinished();
1069
},
1070
1071
onTitleChanged() {},
1072
onBeginUpdateBatch() {},
1073
onEndUpdateBatch() {},
1074
onPageChanged() {},
1075
onDeleteVisits() {},
1076
};
1077
1078
/**
1079
* This view can be added to a DownloadList object to trigger a save operation
1080
* in the given DownloadStore object when a relevant change occurs. You should
1081
* call the "initialize" method in order to register the view and load the
1082
* current state from disk.
1083
*
1084
* You do not need to keep a reference to this object in order to keep it alive,
1085
* because the DownloadList object already keeps a strong reference to it.
1086
*
1087
* @param aList
1088
* The DownloadList object on which the view should be registered.
1089
* @param aStore
1090
* The DownloadStore object used for saving.
1091
*/
1092
var DownloadAutoSaveView = function(aList, aStore) {
1093
this._list = aList;
1094
this._store = aStore;
1095
this._downloadsMap = new Map();
1096
this._writer = new DeferredTask(() => this._store.save(), kSaveDelayMs);
1097
AsyncShutdown.profileBeforeChange.addBlocker("DownloadAutoSaveView: writing data",
1098
() => this._writer.finalize());
1099
};
1100
1101
this.DownloadAutoSaveView.prototype = {
1102
/**
1103
* DownloadList object linked to this view.
1104
*/
1105
_list: null,
1106
1107
/**
1108
* The DownloadStore object used for saving.
1109
*/
1110
_store: null,
1111
1112
/**
1113
* True when the initial state of the downloads has been loaded.
1114
*/
1115
_initialized: false,
1116
1117
/**
1118
* Registers the view and loads the current state from disk.
1119
*
1120
* @return {Promise}
1121
* @resolves When the view has been registered.
1122
* @rejects JavaScript exception.
1123
*/
1124
initialize() {
1125
// We set _initialized to true after adding the view, so that
1126
// onDownloadAdded doesn't cause a save to occur.
1127
return this._list.addView(this).then(() => this._initialized = true);
1128
},
1129
1130
/**
1131
* This map contains only Download objects that should be saved to disk, and
1132
* associates them with the result of their getSerializationHash function, for
1133
* the purpose of detecting changes to the relevant properties.
1134
*/
1135
_downloadsMap: null,
1136
1137
/**
1138
* DeferredTask for the save operation.
1139
*/
1140
_writer: null,
1141
1142
/**
1143
* Called when the list of downloads changed, this triggers the asynchronous
1144
* serialization of the list of downloads.
1145
*/
1146
saveSoon() {
1147
this._writer.arm();
1148
},
1149
1150
// DownloadList callback
1151
onDownloadAdded(aDownload) {
1152
if (gCombinedDownloadIntegration.shouldPersistDownload(aDownload)) {
1153
this._downloadsMap.set(aDownload, aDownload.getSerializationHash());
1154
if (this._initialized) {
1155
this.saveSoon();
1156
}
1157
}
1158
},
1159
1160
// DownloadList callback
1161
onDownloadChanged(aDownload) {
1162
if (!gCombinedDownloadIntegration.shouldPersistDownload(aDownload)) {
1163
if (this._downloadsMap.has(aDownload)) {
1164
this._downloadsMap.delete(aDownload);
1165
this.saveSoon();
1166
}
1167
return;
1168
}
1169
1170
let hash = aDownload.getSerializationHash();
1171
if (this._downloadsMap.get(aDownload) != hash) {
1172
this._downloadsMap.set(aDownload, hash);
1173
this.saveSoon();
1174
}
1175
},
1176
1177
// DownloadList callback
1178
onDownloadRemoved(aDownload) {
1179
if (this._downloadsMap.has(aDownload)) {
1180
this._downloadsMap.delete(aDownload);
1181
this.saveSoon();
1182
}
1183
},
1184
};