toolkit/components/jsdownloads/src/DownloadIntegration.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial