Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /**
8 * Provides functions to integrate with the host application, handling for
9 * example the global prompts on shutdown.
10 */
12 "use strict";
14 this.EXPORTED_SYMBOLS = [
15 "DownloadIntegration",
16 ];
18 ////////////////////////////////////////////////////////////////////////////////
19 //// Globals
21 const Cc = Components.classes;
22 const Ci = Components.interfaces;
23 const Cu = Components.utils;
24 const Cr = Components.results;
26 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
28 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
29 "resource://gre/modules/DeferredTask.jsm");
30 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
31 "resource://gre/modules/Downloads.jsm");
32 XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
33 "resource://gre/modules/DownloadStore.jsm");
34 XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
35 "resource://gre/modules/DownloadImport.jsm");
36 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
37 "resource://gre/modules/DownloadUIHelper.jsm");
38 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
39 "resource://gre/modules/FileUtils.jsm");
40 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
41 "resource://gre/modules/NetUtil.jsm");
42 XPCOMUtils.defineLazyModuleGetter(this, "OS",
43 "resource://gre/modules/osfile.jsm");
44 #ifdef MOZ_PLACES
45 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
46 "resource://gre/modules/PlacesUtils.jsm");
47 #endif
48 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
49 "resource://gre/modules/Promise.jsm");
50 XPCOMUtils.defineLazyModuleGetter(this, "Services",
51 "resource://gre/modules/Services.jsm");
52 XPCOMUtils.defineLazyModuleGetter(this, "Task",
53 "resource://gre/modules/Task.jsm");
54 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
55 "resource://gre/modules/NetUtil.jsm");
57 XPCOMUtils.defineLazyServiceGetter(this, "gDownloadPlatform",
58 "@mozilla.org/toolkit/download-platform;1",
59 "mozIDownloadPlatform");
60 XPCOMUtils.defineLazyServiceGetter(this, "gEnvironment",
61 "@mozilla.org/process/environment;1",
62 "nsIEnvironment");
63 XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
64 "@mozilla.org/mime;1",
65 "nsIMIMEService");
66 XPCOMUtils.defineLazyServiceGetter(this, "gExternalProtocolService",
67 "@mozilla.org/uriloader/external-protocol-service;1",
68 "nsIExternalProtocolService");
70 XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
71 if ("@mozilla.org/parental-controls-service;1" in Cc) {
72 return Cc["@mozilla.org/parental-controls-service;1"]
73 .createInstance(Ci.nsIParentalControlsService);
74 }
75 return null;
76 });
78 XPCOMUtils.defineLazyServiceGetter(this, "gApplicationReputationService",
79 "@mozilla.org/downloads/application-reputation-service;1",
80 Ci.nsIApplicationReputationService);
82 XPCOMUtils.defineLazyServiceGetter(this, "volumeService",
83 "@mozilla.org/telephony/volume-service;1",
84 "nsIVolumeService");
86 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
87 "initWithCallback");
89 /**
90 * Indicates the delay between a change to the downloads data and the related
91 * save operation. This value is the result of a delicate trade-off, assuming
92 * the host application uses the browser history instead of the download store
93 * to save completed downloads.
94 *
95 * If a download takes less than this interval to complete (for example, saving
96 * a page that is already displayed), then no input/output is triggered by the
97 * download store except for an existence check, resulting in the best possible
98 * efficiency.
99 *
100 * Conversely, if the browser is closed before this interval has passed, the
101 * download will not be saved. This prevents it from being restored in the next
102 * session, and if there is partial data associated with it, then the ".part"
103 * file will not be deleted when the browser starts again.
104 *
105 * In all cases, for best efficiency, this value should be high enough that the
106 * input/output for opening or closing the target file does not overlap with the
107 * one for saving the list of downloads.
108 */
109 const kSaveDelayMs = 1500;
111 /**
112 * This pref indicates if we have already imported (or attempted to import)
113 * the downloads database from the previous SQLite storage.
114 */
115 const kPrefImportedFromSqlite = "browser.download.importedFromSqlite";
117 /**
118 * List of observers to listen against
119 */
120 const kObserverTopics = [
121 "quit-application-requested",
122 "offline-requested",
123 "last-pb-context-exiting",
124 "last-pb-context-exited",
125 "sleep_notification",
126 "suspend_process_notification",
127 "wake_notification",
128 "resume_process_notification",
129 "network:offline-about-to-go-offline",
130 "network:offline-status-changed",
131 "xpcom-will-shutdown",
132 ];
134 ////////////////////////////////////////////////////////////////////////////////
135 //// DownloadIntegration
137 /**
138 * Provides functions to integrate with the host application, handling for
139 * example the global prompts on shutdown.
140 */
141 this.DownloadIntegration = {
142 // For testing only
143 _testMode: false,
144 testPromptDownloads: 0,
145 dontLoadList: false,
146 dontLoadObservers: false,
147 dontCheckParentalControls: false,
148 shouldBlockInTest: false,
149 #ifdef MOZ_URL_CLASSIFIER
150 dontCheckApplicationReputation: false,
151 #else
152 dontCheckApplicationReputation: true,
153 #endif
154 shouldBlockInTestForApplicationReputation: false,
155 dontOpenFileAndFolder: false,
156 downloadDoneCalled: false,
157 _deferTestOpenFile: null,
158 _deferTestShowDir: null,
159 _deferTestClearPrivateList: null,
161 /**
162 * Main DownloadStore object for loading and saving the list of persistent
163 * downloads, or null if the download list was never requested and thus it
164 * doesn't need to be persisted.
165 */
166 _store: null,
168 /**
169 * Gets and sets test mode
170 */
171 get testMode() this._testMode,
172 set testMode(mode) {
173 this._downloadsDirectory = null;
174 return (this._testMode = mode);
175 },
177 /**
178 * Performs initialization of the list of persistent downloads, before its
179 * first use by the host application. This function may be called only once
180 * during the entire lifetime of the application.
181 *
182 * @param aList
183 * DownloadList object to be populated with the download objects
184 * serialized from the previous session. This list will be persisted
185 * to disk during the session lifetime.
186 *
187 * @return {Promise}
188 * @resolves When the list has been populated.
189 * @rejects JavaScript exception.
190 */
191 initializePublicDownloadList: function(aList) {
192 return Task.spawn(function task_DI_initializePublicDownloadList() {
193 if (this.dontLoadList) {
194 // In tests, only register the history observer. This object is kept
195 // alive by the history service, so we don't keep a reference to it.
196 new DownloadHistoryObserver(aList);
197 return;
198 }
200 if (this._store) {
201 throw new Error("initializePublicDownloadList may be called only once.");
202 }
204 this._store = new DownloadStore(aList, OS.Path.join(
205 OS.Constants.Path.profileDir,
206 "downloads.json"));
207 this._store.onsaveitem = this.shouldPersistDownload.bind(this);
209 if (this._importedFromSqlite) {
210 try {
211 yield this._store.load();
212 } catch (ex) {
213 Cu.reportError(ex);
214 }
215 } else {
216 let sqliteDBpath = OS.Path.join(OS.Constants.Path.profileDir,
217 "downloads.sqlite");
219 if (yield OS.File.exists(sqliteDBpath)) {
220 let sqliteImport = new DownloadImport(aList, sqliteDBpath);
221 yield sqliteImport.import();
223 let importCount = (yield aList.getAll()).length;
224 if (importCount > 0) {
225 try {
226 yield this._store.save();
227 } catch (ex) { }
228 }
230 // No need to wait for the file removal.
231 OS.File.remove(sqliteDBpath).then(null, Cu.reportError);
232 }
234 Services.prefs.setBoolPref(kPrefImportedFromSqlite, true);
236 // Don't even report error here because this file is pre Firefox 3
237 // and most likely doesn't exist.
238 OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
239 "downloads.rdf"));
241 }
243 // After the list of persistent downloads has been loaded, add the
244 // DownloadAutoSaveView and the DownloadHistoryObserver (even if the load
245 // operation failed). These objects are kept alive by the underlying
246 // DownloadList and by the history service respectively. We wait for a
247 // complete initialization of the view used for detecting changes to
248 // downloads to be persisted, before other callers get a chance to modify
249 // the list without being detected.
250 yield new DownloadAutoSaveView(aList, this._store).initialize();
251 new DownloadHistoryObserver(aList);
252 }.bind(this));
253 },
255 #ifdef MOZ_WIDGET_GONK
256 /**
257 * Finds the default download directory which can be either in the
258 * internal storage or on the sdcard.
259 *
260 * @return {Promise}
261 * @resolves The downloads directory string path.
262 */
263 _getDefaultDownloadDirectory: function() {
264 return Task.spawn(function() {
265 let directoryPath;
266 let win = Services.wm.getMostRecentWindow("navigator:browser");
267 let storages = win.navigator.getDeviceStorages("sdcard");
268 let preferredStorageName;
269 // Use the first one or the default storage.
270 storages.forEach((aStorage) => {
271 if (aStorage.default || !preferredStorageName) {
272 preferredStorageName = aStorage.storageName;
273 }
274 });
276 // Now get the path for this storage area.
277 if (preferredStorageName) {
278 let volume = volumeService.getVolumeByName(preferredStorageName);
279 if (volume &&
280 volume.isMediaPresent &&
281 !volume.isMountLocked &&
282 !volume.isSharing) {
283 directoryPath = OS.Path.join(volume.mountPoint, "downloads");
284 yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
285 }
286 }
287 if (directoryPath) {
288 throw new Task.Result(directoryPath);
289 } else {
290 throw new Components.Exception("No suitable storage for downloads.",
291 Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
292 }
293 });
294 },
295 #endif
297 /**
298 * Determines if a Download object from the list of persistent downloads
299 * should be saved into a file, so that it can be restored across sessions.
300 *
301 * This function allows filtering out downloads that the host application is
302 * not interested in persisting across sessions, for example downloads that
303 * finished successfully.
304 *
305 * @param aDownload
306 * The Download object to be inspected. This is originally taken from
307 * the global DownloadList object for downloads that were not started
308 * from a private browsing window. The item may have been removed
309 * from the list since the save operation started, though in this case
310 * the save operation will be repeated later.
311 *
312 * @return True to save the download, false otherwise.
313 */
314 shouldPersistDownload: function (aDownload)
315 {
316 // In the default implementation, we save all the downloads currently in
317 // progress, as well as stopped downloads for which we retained partially
318 // downloaded data. Stopped downloads for which we don't need to track the
319 // presence of a ".part" file are only retained in the browser history.
320 // On b2g, we keep a few days of history.
321 #ifdef MOZ_B2G
322 let maxTime = Date.now() -
323 Services.prefs.getIntPref("dom.downloads.max_retention_days") * 24 * 60 * 60 * 1000;
324 return (aDownload.startTime > maxTime) ||
325 aDownload.hasPartialData ||
326 !aDownload.stopped;
327 #else
328 return aDownload.hasPartialData || !aDownload.stopped;
329 #endif
330 },
332 /**
333 * Returns the system downloads directory asynchronously.
334 *
335 * @return {Promise}
336 * @resolves The downloads directory string path.
337 */
338 getSystemDownloadsDirectory: function DI_getSystemDownloadsDirectory() {
339 return Task.spawn(function() {
340 if (this._downloadsDirectory) {
341 // This explicitly makes this function a generator for Task.jsm. We
342 // need this because calls to the "yield" operator below may be
343 // preprocessed out on some platforms.
344 yield undefined;
345 throw new Task.Result(this._downloadsDirectory);
346 }
348 let directoryPath = null;
349 #ifdef XP_MACOSX
350 directoryPath = this._getDirectory("DfltDwnld");
351 #elifdef XP_WIN
352 // For XP/2K, use My Documents/Downloads. Other version uses
353 // the default Downloads directory.
354 let version = parseFloat(Services.sysinfo.getProperty("version"));
355 if (version < 6) {
356 directoryPath = yield this._createDownloadsDirectory("Pers");
357 } else {
358 directoryPath = this._getDirectory("DfltDwnld");
359 }
360 #elifdef XP_UNIX
361 #ifdef MOZ_WIDGET_ANDROID
362 // Android doesn't have a $HOME directory, and by default we only have
363 // write access to /data/data/org.mozilla.{$APP} and /sdcard
364 directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
365 if (!directoryPath) {
366 throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
367 Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
368 }
369 #elifdef MOZ_WIDGET_GONK
370 directoryPath = this._getDefaultDownloadDirectory();
371 #else
372 // For Linux, use XDG download dir, with a fallback to Home/Downloads
373 // if the XDG user dirs are disabled.
374 try {
375 directoryPath = this._getDirectory("DfltDwnld");
376 } catch(e) {
377 directoryPath = yield this._createDownloadsDirectory("Home");
378 }
379 #endif
380 #else
381 directoryPath = yield this._createDownloadsDirectory("Home");
382 #endif
383 this._downloadsDirectory = directoryPath;
384 throw new Task.Result(this._downloadsDirectory);
385 }.bind(this));
386 },
387 _downloadsDirectory: null,
389 /**
390 * Returns the user downloads directory asynchronously.
391 *
392 * @return {Promise}
393 * @resolves The downloads directory string path.
394 */
395 getPreferredDownloadsDirectory: function DI_getPreferredDownloadsDirectory() {
396 return Task.spawn(function() {
397 let directoryPath = null;
398 #ifdef MOZ_WIDGET_GONK
399 directoryPath = this._getDefaultDownloadDirectory();
400 #else
401 let prefValue = 1;
403 try {
404 prefValue = Services.prefs.getIntPref("browser.download.folderList");
405 } catch(e) {}
407 switch(prefValue) {
408 case 0: // Desktop
409 directoryPath = this._getDirectory("Desk");
410 break;
411 case 1: // Downloads
412 directoryPath = yield this.getSystemDownloadsDirectory();
413 break;
414 case 2: // Custom
415 try {
416 let directory = Services.prefs.getComplexValue("browser.download.dir",
417 Ci.nsIFile);
418 directoryPath = directory.path;
419 yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
420 } catch(ex) {
421 // Either the preference isn't set or the directory cannot be created.
422 directoryPath = yield this.getSystemDownloadsDirectory();
423 }
424 break;
425 default:
426 directoryPath = yield this.getSystemDownloadsDirectory();
427 }
428 #endif
429 throw new Task.Result(directoryPath);
430 }.bind(this));
431 },
433 /**
434 * Returns the temporary downloads directory asynchronously.
435 *
436 * @return {Promise}
437 * @resolves The downloads directory string path.
438 */
439 getTemporaryDownloadsDirectory: function DI_getTemporaryDownloadsDirectory() {
440 return Task.spawn(function() {
441 let directoryPath = null;
442 #ifdef XP_MACOSX
443 directoryPath = yield this.getPreferredDownloadsDirectory();
444 #elifdef MOZ_WIDGET_ANDROID
445 directoryPath = yield this.getSystemDownloadsDirectory();
446 #elifdef MOZ_WIDGET_GONK
447 directoryPath = yield this.getSystemDownloadsDirectory();
448 #else
449 // For Metro mode on Windows 8, we want searchability for documents
450 // that the user chose to open with an external application.
451 if (Services.metro && Services.metro.immersive) {
452 directoryPath = yield this.getSystemDownloadsDirectory();
453 } else {
454 directoryPath = this._getDirectory("TmpD");
455 }
456 #endif
457 throw new Task.Result(directoryPath);
458 }.bind(this));
459 },
461 /**
462 * Checks to determine whether to block downloads for parental controls.
463 *
464 * aParam aDownload
465 * The download object.
466 *
467 * @return {Promise}
468 * @resolves The boolean indicates to block downloads or not.
469 */
470 shouldBlockForParentalControls: function DI_shouldBlockForParentalControls(aDownload) {
471 if (this.dontCheckParentalControls) {
472 return Promise.resolve(this.shouldBlockInTest);
473 }
475 let isEnabled = gParentalControlsService &&
476 gParentalControlsService.parentalControlsEnabled;
477 let shouldBlock = isEnabled &&
478 gParentalControlsService.blockFileDownloadsEnabled;
480 // Log the event if required by parental controls settings.
481 if (isEnabled && gParentalControlsService.loggingEnabled) {
482 gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload,
483 shouldBlock,
484 NetUtil.newURI(aDownload.source.url), null);
485 }
487 return Promise.resolve(shouldBlock);
488 },
490 /**
491 * Checks to determine whether to block downloads because they might be
492 * malware, based on application reputation checks.
493 *
494 * aParam aDownload
495 * The download object.
496 *
497 * @return {Promise}
498 * @resolves The boolean indicates to block downloads or not.
499 */
500 shouldBlockForReputationCheck: function (aDownload) {
501 if (this.dontCheckApplicationReputation) {
502 return Promise.resolve(this.shouldBlockInTestForApplicationReputation);
503 }
504 let hash;
505 let sigInfo;
506 try {
507 hash = aDownload.saver.getSha256Hash();
508 sigInfo = aDownload.saver.getSignatureInfo();
509 } catch (ex) {
510 // Bail if DownloadSaver doesn't have a hash.
511 return Promise.resolve(false);
512 }
513 if (!hash || !sigInfo) {
514 return Promise.resolve(false);
515 }
516 let deferred = Promise.defer();
517 let aReferrer = null;
518 if (aDownload.source.referrer) {
519 aReferrer: NetUtil.newURI(aDownload.source.referrer);
520 }
521 gApplicationReputationService.queryReputation({
522 sourceURI: NetUtil.newURI(aDownload.source.url),
523 referrerURI: aReferrer,
524 fileSize: aDownload.currentBytes,
525 sha256Hash: hash,
526 signatureInfo: sigInfo },
527 function onComplete(aShouldBlock, aRv) {
528 deferred.resolve(aShouldBlock);
529 });
530 return deferred.promise;
531 },
533 #ifdef XP_WIN
534 /**
535 * Checks whether downloaded files should be marked as coming from
536 * Internet Zone.
537 *
538 * @return true if files should be marked
539 */
540 _shouldSaveZoneInformation: function() {
541 let key = Cc["@mozilla.org/windows-registry-key;1"]
542 .createInstance(Ci.nsIWindowsRegKey);
543 try {
544 key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
545 "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments",
546 Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE);
547 try {
548 return key.readIntValue("SaveZoneInformation") != 1;
549 } finally {
550 key.close();
551 }
552 } catch (ex) {
553 // If the key is not present, files should be marked by default.
554 return true;
555 }
556 },
557 #endif
559 /**
560 * Performs platform-specific operations when a download is done.
561 *
562 * aParam aDownload
563 * The Download object.
564 *
565 * @return {Promise}
566 * @resolves When all the operations completed successfully.
567 * @rejects JavaScript exception if any of the operations failed.
568 */
569 downloadDone: function(aDownload) {
570 return Task.spawn(function () {
571 #ifdef XP_WIN
572 // On Windows, we mark any file saved to the NTFS file system as coming
573 // from the Internet security zone unless Group Policy disables the
574 // feature. We do this by writing to the "Zone.Identifier" Alternate
575 // Data Stream directly, because the Save method of the
576 // IAttachmentExecute interface would trigger operations that may cause
577 // the application to hang, or other performance issues.
578 // The stream created in this way is forward-compatible with all the
579 // current and future versions of Windows.
580 if (this._shouldSaveZoneInformation()) {
581 let zone;
582 try {
583 zone = gDownloadPlatform.mapUrlToZone(aDownload.source.url);
584 } catch (e) {
585 // Default to Internet Zone if mapUrlToZone failed for
586 // whatever reason.
587 zone = Ci.mozIDownloadPlatform.ZONE_INTERNET;
588 }
589 try {
590 // Don't write zone IDs for Local, Intranet, or Trusted sites
591 // to match Windows behavior.
592 if (zone >= Ci.mozIDownloadPlatform.ZONE_INTERNET) {
593 let streamPath = aDownload.target.path + ":Zone.Identifier";
594 let stream = yield OS.File.open(streamPath, { create: true });
595 try {
596 yield stream.write(new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=" + zone + "\r\n"));
597 } finally {
598 yield stream.close();
599 }
600 }
601 } catch (ex) {
602 // If writing to the stream fails, we ignore the error and continue.
603 // The Windows API error 123 (ERROR_INVALID_NAME) is expected to
604 // occur when working on a file system that does not support
605 // Alternate Data Streams, like FAT32, thus we don't report this
606 // specific error.
607 if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) {
608 Cu.reportError(ex);
609 }
610 }
611 }
612 #endif
614 // Now that the file is completely downloaded, mark it
615 // accessible by other users on this system, if the user's
616 // global preferences so indicate. (On Unix, this applies the
617 // umask. On Windows, currently does nothing.)
618 // Errors should be reported, but are not fatal.
619 try {
620 yield OS.File.setPermissions(aDownload.target.path);
621 } catch (ex) {
622 Cu.reportError(ex);
623 }
625 gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),
626 new FileUtils.File(aDownload.target.path),
627 aDownload.contentType,
628 aDownload.source.isPrivate);
629 this.downloadDoneCalled = true;
630 }.bind(this));
631 },
633 /*
634 * Launches a file represented by the target of a download. This can
635 * open the file with the default application for the target MIME type
636 * or file extension, or with a custom application if
637 * aDownload.launcherPath is set.
638 *
639 * @param aDownload
640 * A Download object that contains the necessary information
641 * to launch the file. The relevant properties are: the target
642 * file, the contentType and the custom application chosen
643 * to launch it.
644 *
645 * @return {Promise}
646 * @resolves When the instruction to launch the file has been
647 * successfully given to the operating system. Note that
648 * the OS might still take a while until the file is actually
649 * launched.
650 * @rejects JavaScript exception if there was an error trying to launch
651 * the file.
652 */
653 launchDownload: function (aDownload) {
654 let deferred = Task.spawn(function DI_launchDownload_task() {
655 let file = new FileUtils.File(aDownload.target.path);
657 #ifndef XP_WIN
658 // Ask for confirmation if the file is executable, except on Windows where
659 // the operating system will show the prompt based on the security zone.
660 // We do this here, instead of letting the caller handle the prompt
661 // separately in the user interface layer, for two reasons. The first is
662 // because of its security nature, so that add-ons cannot forget to do
663 // this check. The second is that the system-level security prompt would
664 // be displayed at launch time in any case.
665 if (file.isExecutable() && !this.dontOpenFileAndFolder) {
666 // We don't anchor the prompt to a specific window intentionally, not
667 // only because this is the same behavior as the system-level prompt,
668 // but also because the most recently active window is the right choice
669 // in basically all cases.
670 let shouldLaunch = yield DownloadUIHelper.getPrompter()
671 .confirmLaunchExecutable(file.path);
672 if (!shouldLaunch) {
673 return;
674 }
675 }
676 #endif
678 // In case of a double extension, like ".tar.gz", we only
679 // consider the last one, because the MIME service cannot
680 // handle multiple extensions.
681 let fileExtension = null, mimeInfo = null;
682 let match = file.leafName.match(/\.([^.]+)$/);
683 if (match) {
684 fileExtension = match[1];
685 }
687 try {
688 // The MIME service might throw if contentType == "" and it can't find
689 // a MIME type for the given extension, so we'll treat this case as
690 // an unknown mimetype.
691 mimeInfo = gMIMEService.getFromTypeAndExtension(aDownload.contentType,
692 fileExtension);
693 } catch (e) { }
695 if (aDownload.launcherPath) {
696 if (!mimeInfo) {
697 // This should not happen on normal circumstances because launcherPath
698 // is only set when we had an instance of nsIMIMEInfo to retrieve
699 // the custom application chosen by the user.
700 throw new Error(
701 "Unable to create nsIMIMEInfo to launch a custom application");
702 }
704 // Custom application chosen
705 let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
706 .createInstance(Ci.nsILocalHandlerApp);
707 localHandlerApp.executable = new FileUtils.File(aDownload.launcherPath);
709 mimeInfo.preferredApplicationHandler = localHandlerApp;
710 mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
712 // In test mode, allow the test to verify the nsIMIMEInfo instance.
713 if (this.dontOpenFileAndFolder) {
714 throw new Task.Result(mimeInfo);
715 }
717 mimeInfo.launchWithFile(file);
718 return;
719 }
721 // No custom application chosen, let's launch the file with the default
722 // handler. In test mode, we indicate this with a null value.
723 if (this.dontOpenFileAndFolder) {
724 throw new Task.Result(null);
725 }
727 // First let's try to launch it through the MIME service application
728 // handler
729 if (mimeInfo) {
730 mimeInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
732 try {
733 mimeInfo.launchWithFile(file);
734 return;
735 } catch (ex) { }
736 }
738 // If it didn't work or if there was no MIME info available,
739 // let's try to directly launch the file.
740 try {
741 file.launch();
742 return;
743 } catch (ex) { }
745 // If our previous attempts failed, try sending it through
746 // the system's external "file:" URL handler.
747 gExternalProtocolService.loadUrl(NetUtil.newURI(file));
748 yield undefined;
749 }.bind(this));
751 if (this.dontOpenFileAndFolder) {
752 deferred.then((value) => { this._deferTestOpenFile.resolve(value); },
753 (error) => { this._deferTestOpenFile.reject(error); });
754 }
756 return deferred;
757 },
759 /*
760 * Shows the containing folder of a file.
761 *
762 * @param aFilePath
763 * The path to the file.
764 *
765 * @return {Promise}
766 * @resolves When the instruction to open the containing folder has been
767 * successfully given to the operating system. Note that
768 * the OS might still take a while until the folder is actually
769 * opened.
770 * @rejects JavaScript exception if there was an error trying to open
771 * the containing folder.
772 */
773 showContainingDirectory: function (aFilePath) {
774 let deferred = Task.spawn(function DI_showContainingDirectory_task() {
775 let file = new FileUtils.File(aFilePath);
777 if (this.dontOpenFileAndFolder) {
778 return;
779 }
781 try {
782 // Show the directory containing the file and select the file.
783 file.reveal();
784 return;
785 } catch (ex) { }
787 // If reveal fails for some reason (e.g., it's not implemented on unix
788 // or the file doesn't exist), try using the parent if we have it.
789 let parent = file.parent;
790 if (!parent) {
791 throw new Error(
792 "Unexpected reference to a top-level directory instead of a file");
793 }
795 try {
796 // Open the parent directory to show where the file should be.
797 parent.launch();
798 return;
799 } catch (ex) { }
801 // If launch also fails (probably because it's not implemented), let
802 // the OS handler try to open the parent.
803 gExternalProtocolService.loadUrl(NetUtil.newURI(parent));
804 yield undefined;
805 }.bind(this));
807 if (this.dontOpenFileAndFolder) {
808 deferred.then((value) => { this._deferTestShowDir.resolve("success"); },
809 (error) => {
810 // Ensure that _deferTestShowDir has at least one consumer
811 // for the error, otherwise the error will be reported as
812 // uncaught.
813 this._deferTestShowDir.promise.then(null, function() {});
814 this._deferTestShowDir.reject(error);
815 });
816 }
818 return deferred;
819 },
821 /**
822 * Calls the directory service, create a downloads directory and returns an
823 * nsIFile for the downloads directory.
824 *
825 * @return {Promise}
826 * @resolves The directory string path.
827 */
828 _createDownloadsDirectory: function DI_createDownloadsDirectory(aName) {
829 // We read the name of the directory from the list of translated strings
830 // that is kept by the UI helper module, even if this string is not strictly
831 // displayed in the user interface.
832 let directoryPath = OS.Path.join(this._getDirectory(aName),
833 DownloadUIHelper.strings.downloadsFolder);
835 // Create the Downloads folder and ignore if it already exists.
836 return OS.File.makeDir(directoryPath, { ignoreExisting: true }).
837 then(function() {
838 return directoryPath;
839 });
840 },
842 /**
843 * Calls the directory service and returns an nsIFile for the requested
844 * location name.
845 *
846 * @return The directory string path.
847 */
848 _getDirectory: function DI_getDirectory(aName) {
849 return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile).path;
850 },
852 /**
853 * Register the downloads interruption observers.
854 *
855 * @param aList
856 * The public or private downloads list.
857 * @param aIsPrivate
858 * True if the list is private, false otherwise.
859 *
860 * @return {Promise}
861 * @resolves When the views and observers are added.
862 */
863 addListObservers: function DI_addListObservers(aList, aIsPrivate) {
864 if (this.dontLoadObservers) {
865 return Promise.resolve();
866 }
868 DownloadObserver.registerView(aList, aIsPrivate);
869 if (!DownloadObserver.observersAdded) {
870 DownloadObserver.observersAdded = true;
871 for (let topic of kObserverTopics) {
872 Services.obs.addObserver(DownloadObserver, topic, false);
873 }
874 }
875 return Promise.resolve();
876 },
878 /**
879 * Checks if we have already imported (or attempted to import)
880 * the downloads database from the previous SQLite storage.
881 *
882 * @return boolean True if we the previous DB was imported.
883 */
884 get _importedFromSqlite() {
885 try {
886 return Services.prefs.getBoolPref(kPrefImportedFromSqlite);
887 } catch (ex) {
888 return false;
889 }
890 },
891 };
893 ////////////////////////////////////////////////////////////////////////////////
894 //// DownloadObserver
896 this.DownloadObserver = {
897 /**
898 * Flag to determine if the observers have been added previously.
899 */
900 observersAdded: false,
902 /**
903 * Timer used to delay restarting canceled downloads upon waking and returning
904 * online.
905 */
906 _wakeTimer: null,
908 /**
909 * Set that contains the in progress publics downloads.
910 * It's kept updated when a public download is added, removed or changes its
911 * properties.
912 */
913 _publicInProgressDownloads: new Set(),
915 /**
916 * Set that contains the in progress private downloads.
917 * It's kept updated when a private download is added, removed or changes its
918 * properties.
919 */
920 _privateInProgressDownloads: new Set(),
922 /**
923 * Set that contains the downloads that have been canceled when going offline
924 * or to sleep. These are started again when returning online or waking. This
925 * list is not persisted so when exiting and restarting, the downloads will not
926 * be started again.
927 */
928 _canceledOfflineDownloads: new Set(),
930 /**
931 * Registers a view that updates the corresponding downloads state set, based
932 * on the aIsPrivate argument. The set is updated when a download is added,
933 * removed or changes its properties.
934 *
935 * @param aList
936 * The public or private downloads list.
937 * @param aIsPrivate
938 * True if the list is private, false otherwise.
939 */
940 registerView: function DO_registerView(aList, aIsPrivate) {
941 let downloadsSet = aIsPrivate ? this._privateInProgressDownloads
942 : this._publicInProgressDownloads;
943 let downloadsView = {
944 onDownloadAdded: aDownload => {
945 if (!aDownload.stopped) {
946 downloadsSet.add(aDownload);
947 }
948 },
949 onDownloadChanged: aDownload => {
950 if (aDownload.stopped) {
951 downloadsSet.delete(aDownload);
952 } else {
953 downloadsSet.add(aDownload);
954 }
955 },
956 onDownloadRemoved: aDownload => {
957 downloadsSet.delete(aDownload);
958 // The download must also be removed from the canceled when offline set.
959 this._canceledOfflineDownloads.delete(aDownload);
960 }
961 };
963 // We register the view asynchronously.
964 aList.addView(downloadsView).then(null, Cu.reportError);
965 },
967 /**
968 * Wrapper that handles the test mode before calling the prompt that display
969 * a warning message box that informs that there are active downloads,
970 * and asks whether the user wants to cancel them or not.
971 *
972 * @param aCancel
973 * The observer notification subject.
974 * @param aDownloadsCount
975 * The current downloads count.
976 * @param aPrompter
977 * The prompter object that shows the confirm dialog.
978 * @param aPromptType
979 * The type of prompt notification depending on the observer.
980 */
981 _confirmCancelDownloads: function DO_confirmCancelDownload(
982 aCancel, aDownloadsCount, aPrompter, aPromptType) {
983 // If user has already dismissed the request, then do nothing.
984 if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) {
985 return;
986 }
987 // Handle test mode
988 if (DownloadIntegration.testMode) {
989 DownloadIntegration.testPromptDownloads = aDownloadsCount;
990 return;
991 }
993 aCancel.data = aPrompter.confirmCancelDownloads(aDownloadsCount, aPromptType);
994 },
996 /**
997 * Resume all downloads that were paused when going offline, used when waking
998 * from sleep or returning from being offline.
999 */
1000 _resumeOfflineDownloads: function DO_resumeOfflineDownloads() {
1001 this._wakeTimer = null;
1003 for (let download of this._canceledOfflineDownloads) {
1004 download.start();
1005 }
1006 },
1008 ////////////////////////////////////////////////////////////////////////////
1009 //// nsIObserver
1011 observe: function DO_observe(aSubject, aTopic, aData) {
1012 let downloadsCount;
1013 let p = DownloadUIHelper.getPrompter();
1014 switch (aTopic) {
1015 case "quit-application-requested":
1016 downloadsCount = this._publicInProgressDownloads.size +
1017 this._privateInProgressDownloads.size;
1018 this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_QUIT);
1019 break;
1020 case "offline-requested":
1021 downloadsCount = this._publicInProgressDownloads.size +
1022 this._privateInProgressDownloads.size;
1023 this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_OFFLINE);
1024 break;
1025 case "last-pb-context-exiting":
1026 downloadsCount = this._privateInProgressDownloads.size;
1027 this._confirmCancelDownloads(aSubject, downloadsCount, p,
1028 p.ON_LEAVE_PRIVATE_BROWSING);
1029 break;
1030 case "last-pb-context-exited":
1031 let deferred = Task.spawn(function() {
1032 let list = yield Downloads.getList(Downloads.PRIVATE);
1033 let downloads = yield list.getAll();
1035 // We can remove the downloads and finalize them in parallel.
1036 for (let download of downloads) {
1037 list.remove(download).then(null, Cu.reportError);
1038 download.finalize(true).then(null, Cu.reportError);
1039 }
1040 });
1041 // Handle test mode
1042 if (DownloadIntegration.testMode) {
1043 deferred.then((value) => { DownloadIntegration._deferTestClearPrivateList.resolve("success"); },
1044 (error) => { DownloadIntegration._deferTestClearPrivateList.reject(error); });
1045 }
1046 break;
1047 case "sleep_notification":
1048 case "suspend_process_notification":
1049 case "network:offline-about-to-go-offline":
1050 for (let download of this._publicInProgressDownloads) {
1051 download.cancel();
1052 this._canceledOfflineDownloads.add(download);
1053 }
1054 for (let download of this._privateInProgressDownloads) {
1055 download.cancel();
1056 this._canceledOfflineDownloads.add(download);
1057 }
1058 break;
1059 case "wake_notification":
1060 case "resume_process_notification":
1061 let wakeDelay = 10000;
1062 try {
1063 wakeDelay = Services.prefs.getIntPref("browser.download.manager.resumeOnWakeDelay");
1064 } catch(e) {}
1066 if (wakeDelay >= 0) {
1067 this._wakeTimer = new Timer(this._resumeOfflineDownloads.bind(this), wakeDelay,
1068 Ci.nsITimer.TYPE_ONE_SHOT);
1069 }
1070 break;
1071 case "network:offline-status-changed":
1072 if (aData == "online") {
1073 this._resumeOfflineDownloads();
1074 }
1075 break;
1076 // We need to unregister observers explicitly before we reach the
1077 // "xpcom-shutdown" phase, otherwise observers may be notified when some
1078 // required services are not available anymore. We can't unregister
1079 // observers on "quit-application", because this module is also loaded
1080 // during "make package" automation, and the quit notification is not sent
1081 // in that execution environment (bug 973637).
1082 case "xpcom-will-shutdown":
1083 for (let topic of kObserverTopics) {
1084 Services.obs.removeObserver(this, topic);
1085 }
1086 break;
1087 }
1088 },
1090 ////////////////////////////////////////////////////////////////////////////
1091 //// nsISupports
1093 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
1094 };
1096 ////////////////////////////////////////////////////////////////////////////////
1097 //// DownloadHistoryObserver
1099 #ifdef MOZ_PLACES
1100 /**
1101 * Registers a Places observer so that operations on download history are
1102 * reflected on the provided list of downloads.
1103 *
1104 * You do not need to keep a reference to this object in order to keep it alive,
1105 * because the history service already keeps a strong reference to it.
1106 *
1107 * @param aList
1108 * DownloadList object linked to this observer.
1109 */
1110 this.DownloadHistoryObserver = function (aList)
1111 {
1112 this._list = aList;
1113 PlacesUtils.history.addObserver(this, false);
1114 }
1116 this.DownloadHistoryObserver.prototype = {
1117 /**
1118 * DownloadList object linked to this observer.
1119 */
1120 _list: null,
1122 ////////////////////////////////////////////////////////////////////////////
1123 //// nsISupports
1125 QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
1127 ////////////////////////////////////////////////////////////////////////////
1128 //// nsINavHistoryObserver
1130 onDeleteURI: function DL_onDeleteURI(aURI, aGUID) {
1131 this._list.removeFinished(download => aURI.equals(NetUtil.newURI(
1132 download.source.url)));
1133 },
1135 onClearHistory: function DL_onClearHistory() {
1136 this._list.removeFinished();
1137 },
1139 onTitleChanged: function () {},
1140 onBeginUpdateBatch: function () {},
1141 onEndUpdateBatch: function () {},
1142 onVisit: function () {},
1143 onPageChanged: function () {},
1144 onDeleteVisits: function () {},
1145 };
1146 #else
1147 /**
1148 * Empty implementation when we have no Places support, for example on B2G.
1149 */
1150 this.DownloadHistoryObserver = function (aList) {}
1151 #endif
1153 ////////////////////////////////////////////////////////////////////////////////
1154 //// DownloadAutoSaveView
1156 /**
1157 * This view can be added to a DownloadList object to trigger a save operation
1158 * in the given DownloadStore object when a relevant change occurs. You should
1159 * call the "initialize" method in order to register the view and load the
1160 * current state from disk.
1161 *
1162 * You do not need to keep a reference to this object in order to keep it alive,
1163 * because the DownloadList object already keeps a strong reference to it.
1164 *
1165 * @param aList
1166 * The DownloadList object on which the view should be registered.
1167 * @param aStore
1168 * The DownloadStore object used for saving.
1169 */
1170 this.DownloadAutoSaveView = function (aList, aStore)
1171 {
1172 this._list = aList;
1173 this._store = aStore;
1174 this._downloadsMap = new Map();
1175 this._writer = new DeferredTask(() => this._store.save(), kSaveDelayMs);
1176 }
1178 this.DownloadAutoSaveView.prototype = {
1179 /**
1180 * DownloadList object linked to this view.
1181 */
1182 _list: null,
1184 /**
1185 * The DownloadStore object used for saving.
1186 */
1187 _store: null,
1189 /**
1190 * True when the initial state of the downloads has been loaded.
1191 */
1192 _initialized: false,
1194 /**
1195 * Registers the view and loads the current state from disk.
1196 *
1197 * @return {Promise}
1198 * @resolves When the view has been registered.
1199 * @rejects JavaScript exception.
1200 */
1201 initialize: function ()
1202 {
1203 // We set _initialized to true after adding the view, so that
1204 // onDownloadAdded doesn't cause a save to occur.
1205 return this._list.addView(this).then(() => this._initialized = true);
1206 },
1208 /**
1209 * This map contains only Download objects that should be saved to disk, and
1210 * associates them with the result of their getSerializationHash function, for
1211 * the purpose of detecting changes to the relevant properties.
1212 */
1213 _downloadsMap: null,
1215 /**
1216 * DeferredTask for the save operation.
1217 */
1218 _writer: null,
1220 /**
1221 * Called when the list of downloads changed, this triggers the asynchronous
1222 * serialization of the list of downloads.
1223 */
1224 saveSoon: function ()
1225 {
1226 this._writer.arm();
1227 },
1229 //////////////////////////////////////////////////////////////////////////////
1230 //// DownloadList view
1232 onDownloadAdded: function (aDownload)
1233 {
1234 if (DownloadIntegration.shouldPersistDownload(aDownload)) {
1235 this._downloadsMap.set(aDownload, aDownload.getSerializationHash());
1236 if (this._initialized) {
1237 this.saveSoon();
1238 }
1239 }
1240 },
1242 onDownloadChanged: function (aDownload)
1243 {
1244 if (!DownloadIntegration.shouldPersistDownload(aDownload)) {
1245 if (this._downloadsMap.has(aDownload)) {
1246 this._downloadsMap.delete(aDownload);
1247 this.saveSoon();
1248 }
1249 return;
1250 }
1252 let hash = aDownload.getSerializationHash();
1253 if (this._downloadsMap.get(aDownload) != hash) {
1254 this._downloadsMap.set(aDownload, hash);
1255 this.saveSoon();
1256 }
1257 },
1259 onDownloadRemoved: function (aDownload)
1260 {
1261 if (this._downloadsMap.has(aDownload)) {
1262 this._downloadsMap.delete(aDownload);
1263 this.saveSoon();
1264 }
1265 },
1266 };