toolkit/components/jsdownloads/src/DownloadIntegration.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1266 @@
     1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +/**
    1.11 + * Provides functions to integrate with the host application, handling for
    1.12 + * example the global prompts on shutdown.
    1.13 + */
    1.14 +
    1.15 +"use strict";
    1.16 +
    1.17 +this.EXPORTED_SYMBOLS = [
    1.18 +  "DownloadIntegration",
    1.19 +];
    1.20 +
    1.21 +////////////////////////////////////////////////////////////////////////////////
    1.22 +//// Globals
    1.23 +
    1.24 +const Cc = Components.classes;
    1.25 +const Ci = Components.interfaces;
    1.26 +const Cu = Components.utils;
    1.27 +const Cr = Components.results;
    1.28 +
    1.29 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.30 +
    1.31 +XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
    1.32 +                                  "resource://gre/modules/DeferredTask.jsm");
    1.33 +XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
    1.34 +                                  "resource://gre/modules/Downloads.jsm");
    1.35 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
    1.36 +                                  "resource://gre/modules/DownloadStore.jsm");
    1.37 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
    1.38 +                                  "resource://gre/modules/DownloadImport.jsm");
    1.39 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
    1.40 +                                  "resource://gre/modules/DownloadUIHelper.jsm");
    1.41 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    1.42 +                                  "resource://gre/modules/FileUtils.jsm");
    1.43 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    1.44 +                                  "resource://gre/modules/NetUtil.jsm");
    1.45 +XPCOMUtils.defineLazyModuleGetter(this, "OS",
    1.46 +                                  "resource://gre/modules/osfile.jsm");
    1.47 +#ifdef MOZ_PLACES
    1.48 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    1.49 +                                  "resource://gre/modules/PlacesUtils.jsm");
    1.50 +#endif
    1.51 +XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    1.52 +                                  "resource://gre/modules/Promise.jsm");
    1.53 +XPCOMUtils.defineLazyModuleGetter(this, "Services",
    1.54 +                                  "resource://gre/modules/Services.jsm");
    1.55 +XPCOMUtils.defineLazyModuleGetter(this, "Task",
    1.56 +                                  "resource://gre/modules/Task.jsm");
    1.57 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    1.58 +                                  "resource://gre/modules/NetUtil.jsm");
    1.59 +
    1.60 +XPCOMUtils.defineLazyServiceGetter(this, "gDownloadPlatform",
    1.61 +                                   "@mozilla.org/toolkit/download-platform;1",
    1.62 +                                   "mozIDownloadPlatform");
    1.63 +XPCOMUtils.defineLazyServiceGetter(this, "gEnvironment",
    1.64 +                                   "@mozilla.org/process/environment;1",
    1.65 +                                   "nsIEnvironment");
    1.66 +XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
    1.67 +                                   "@mozilla.org/mime;1",
    1.68 +                                   "nsIMIMEService");
    1.69 +XPCOMUtils.defineLazyServiceGetter(this, "gExternalProtocolService",
    1.70 +                                   "@mozilla.org/uriloader/external-protocol-service;1",
    1.71 +                                   "nsIExternalProtocolService");
    1.72 +
    1.73 +XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
    1.74 +  if ("@mozilla.org/parental-controls-service;1" in Cc) {
    1.75 +    return Cc["@mozilla.org/parental-controls-service;1"]
    1.76 +      .createInstance(Ci.nsIParentalControlsService);
    1.77 +  }
    1.78 +  return null;
    1.79 +});
    1.80 +
    1.81 +XPCOMUtils.defineLazyServiceGetter(this, "gApplicationReputationService",
    1.82 +           "@mozilla.org/downloads/application-reputation-service;1",
    1.83 +           Ci.nsIApplicationReputationService);
    1.84 +
    1.85 +XPCOMUtils.defineLazyServiceGetter(this, "volumeService",
    1.86 +                                   "@mozilla.org/telephony/volume-service;1",
    1.87 +                                   "nsIVolumeService");
    1.88 +
    1.89 +const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
    1.90 +                                     "initWithCallback");
    1.91 +
    1.92 +/**
    1.93 + * Indicates the delay between a change to the downloads data and the related
    1.94 + * save operation.  This value is the result of a delicate trade-off, assuming
    1.95 + * the host application uses the browser history instead of the download store
    1.96 + * to save completed downloads.
    1.97 + *
    1.98 + * If a download takes less than this interval to complete (for example, saving
    1.99 + * a page that is already displayed), then no input/output is triggered by the
   1.100 + * download store except for an existence check, resulting in the best possible
   1.101 + * efficiency.
   1.102 + *
   1.103 + * Conversely, if the browser is closed before this interval has passed, the
   1.104 + * download will not be saved.  This prevents it from being restored in the next
   1.105 + * session, and if there is partial data associated with it, then the ".part"
   1.106 + * file will not be deleted when the browser starts again.
   1.107 + *
   1.108 + * In all cases, for best efficiency, this value should be high enough that the
   1.109 + * input/output for opening or closing the target file does not overlap with the
   1.110 + * one for saving the list of downloads.
   1.111 + */
   1.112 +const kSaveDelayMs = 1500;
   1.113 +
   1.114 +/**
   1.115 + * This pref indicates if we have already imported (or attempted to import)
   1.116 + * the downloads database from the previous SQLite storage.
   1.117 + */
   1.118 +const kPrefImportedFromSqlite = "browser.download.importedFromSqlite";
   1.119 +
   1.120 +/**
   1.121 + * List of observers to listen against
   1.122 + */
   1.123 +const kObserverTopics = [
   1.124 +  "quit-application-requested",
   1.125 +  "offline-requested",
   1.126 +  "last-pb-context-exiting",
   1.127 +  "last-pb-context-exited",
   1.128 +  "sleep_notification",
   1.129 +  "suspend_process_notification",
   1.130 +  "wake_notification",
   1.131 +  "resume_process_notification",
   1.132 +  "network:offline-about-to-go-offline",
   1.133 +  "network:offline-status-changed",
   1.134 +  "xpcom-will-shutdown",
   1.135 +];
   1.136 +
   1.137 +////////////////////////////////////////////////////////////////////////////////
   1.138 +//// DownloadIntegration
   1.139 +
   1.140 +/**
   1.141 + * Provides functions to integrate with the host application, handling for
   1.142 + * example the global prompts on shutdown.
   1.143 + */
   1.144 +this.DownloadIntegration = {
   1.145 +  // For testing only
   1.146 +  _testMode: false,
   1.147 +  testPromptDownloads: 0,
   1.148 +  dontLoadList: false,
   1.149 +  dontLoadObservers: false,
   1.150 +  dontCheckParentalControls: false,
   1.151 +  shouldBlockInTest: false,
   1.152 +#ifdef MOZ_URL_CLASSIFIER
   1.153 +  dontCheckApplicationReputation: false,
   1.154 +#else
   1.155 +  dontCheckApplicationReputation: true,
   1.156 +#endif
   1.157 +  shouldBlockInTestForApplicationReputation: false,
   1.158 +  dontOpenFileAndFolder: false,
   1.159 +  downloadDoneCalled: false,
   1.160 +  _deferTestOpenFile: null,
   1.161 +  _deferTestShowDir: null,
   1.162 +  _deferTestClearPrivateList: null,
   1.163 +
   1.164 +  /**
   1.165 +   * Main DownloadStore object for loading and saving the list of persistent
   1.166 +   * downloads, or null if the download list was never requested and thus it
   1.167 +   * doesn't need to be persisted.
   1.168 +   */
   1.169 +  _store: null,
   1.170 +
   1.171 +  /**
   1.172 +   * Gets and sets test mode
   1.173 +   */
   1.174 +  get testMode() this._testMode,
   1.175 +  set testMode(mode) {
   1.176 +    this._downloadsDirectory = null;
   1.177 +    return (this._testMode = mode);
   1.178 +  },
   1.179 +
   1.180 +  /**
   1.181 +   * Performs initialization of the list of persistent downloads, before its
   1.182 +   * first use by the host application.  This function may be called only once
   1.183 +   * during the entire lifetime of the application.
   1.184 +   *
   1.185 +   * @param aList
   1.186 +   *        DownloadList object to be populated with the download objects
   1.187 +   *        serialized from the previous session.  This list will be persisted
   1.188 +   *        to disk during the session lifetime.
   1.189 +   *
   1.190 +   * @return {Promise}
   1.191 +   * @resolves When the list has been populated.
   1.192 +   * @rejects JavaScript exception.
   1.193 +   */
   1.194 +  initializePublicDownloadList: function(aList) {
   1.195 +    return Task.spawn(function task_DI_initializePublicDownloadList() {
   1.196 +      if (this.dontLoadList) {
   1.197 +        // In tests, only register the history observer.  This object is kept
   1.198 +        // alive by the history service, so we don't keep a reference to it.
   1.199 +        new DownloadHistoryObserver(aList);
   1.200 +        return;
   1.201 +      }
   1.202 +
   1.203 +      if (this._store) {
   1.204 +        throw new Error("initializePublicDownloadList may be called only once.");
   1.205 +      }
   1.206 +
   1.207 +      this._store = new DownloadStore(aList, OS.Path.join(
   1.208 +                                                OS.Constants.Path.profileDir,
   1.209 +                                                "downloads.json"));
   1.210 +      this._store.onsaveitem = this.shouldPersistDownload.bind(this);
   1.211 +
   1.212 +      if (this._importedFromSqlite) {
   1.213 +        try {
   1.214 +          yield this._store.load();
   1.215 +        } catch (ex) {
   1.216 +          Cu.reportError(ex);
   1.217 +        }
   1.218 +      } else {
   1.219 +        let sqliteDBpath = OS.Path.join(OS.Constants.Path.profileDir,
   1.220 +                                        "downloads.sqlite");
   1.221 +
   1.222 +        if (yield OS.File.exists(sqliteDBpath)) {
   1.223 +          let sqliteImport = new DownloadImport(aList, sqliteDBpath);
   1.224 +          yield sqliteImport.import();
   1.225 +
   1.226 +          let importCount = (yield aList.getAll()).length;
   1.227 +          if (importCount > 0) {
   1.228 +            try {
   1.229 +              yield this._store.save();
   1.230 +            } catch (ex) { }
   1.231 +          }
   1.232 +
   1.233 +          // No need to wait for the file removal.
   1.234 +          OS.File.remove(sqliteDBpath).then(null, Cu.reportError);
   1.235 +        }
   1.236 +
   1.237 +        Services.prefs.setBoolPref(kPrefImportedFromSqlite, true);
   1.238 +
   1.239 +        // Don't even report error here because this file is pre Firefox 3
   1.240 +        // and most likely doesn't exist.
   1.241 +        OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
   1.242 +                                    "downloads.rdf"));
   1.243 +
   1.244 +      }
   1.245 +
   1.246 +      // After the list of persistent downloads has been loaded, add the
   1.247 +      // DownloadAutoSaveView and the DownloadHistoryObserver (even if the load
   1.248 +      // operation failed).  These objects are kept alive by the underlying
   1.249 +      // DownloadList and by the history service respectively.  We wait for a
   1.250 +      // complete initialization of the view used for detecting changes to
   1.251 +      // downloads to be persisted, before other callers get a chance to modify
   1.252 +      // the list without being detected.
   1.253 +      yield new DownloadAutoSaveView(aList, this._store).initialize();
   1.254 +      new DownloadHistoryObserver(aList);
   1.255 +    }.bind(this));
   1.256 +  },
   1.257 +
   1.258 +#ifdef MOZ_WIDGET_GONK
   1.259 +  /**
   1.260 +    * Finds the default download directory which can be either in the
   1.261 +    * internal storage or on the sdcard.
   1.262 +    *
   1.263 +    * @return {Promise}
   1.264 +    * @resolves The downloads directory string path.
   1.265 +    */
   1.266 +  _getDefaultDownloadDirectory: function() {
   1.267 +    return Task.spawn(function() {
   1.268 +      let directoryPath;
   1.269 +      let win = Services.wm.getMostRecentWindow("navigator:browser");
   1.270 +      let storages = win.navigator.getDeviceStorages("sdcard");
   1.271 +      let preferredStorageName;
   1.272 +      // Use the first one or the default storage.
   1.273 +      storages.forEach((aStorage) => {
   1.274 +        if (aStorage.default || !preferredStorageName) {
   1.275 +          preferredStorageName = aStorage.storageName;
   1.276 +        }
   1.277 +      });
   1.278 +
   1.279 +      // Now get the path for this storage area.
   1.280 +      if (preferredStorageName) {
   1.281 +        let volume = volumeService.getVolumeByName(preferredStorageName);
   1.282 +        if (volume &&
   1.283 +            volume.isMediaPresent &&
   1.284 +            !volume.isMountLocked &&
   1.285 +            !volume.isSharing) {
   1.286 +          directoryPath = OS.Path.join(volume.mountPoint, "downloads");
   1.287 +          yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
   1.288 +        }
   1.289 +      }
   1.290 +      if (directoryPath) {
   1.291 +        throw new Task.Result(directoryPath);
   1.292 +      } else {
   1.293 +        throw new Components.Exception("No suitable storage for downloads.",
   1.294 +                                       Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
   1.295 +      }
   1.296 +    });
   1.297 +  },
   1.298 +#endif
   1.299 +
   1.300 +  /**
   1.301 +   * Determines if a Download object from the list of persistent downloads
   1.302 +   * should be saved into a file, so that it can be restored across sessions.
   1.303 +   *
   1.304 +   * This function allows filtering out downloads that the host application is
   1.305 +   * not interested in persisting across sessions, for example downloads that
   1.306 +   * finished successfully.
   1.307 +   *
   1.308 +   * @param aDownload
   1.309 +   *        The Download object to be inspected.  This is originally taken from
   1.310 +   *        the global DownloadList object for downloads that were not started
   1.311 +   *        from a private browsing window.  The item may have been removed
   1.312 +   *        from the list since the save operation started, though in this case
   1.313 +   *        the save operation will be repeated later.
   1.314 +   *
   1.315 +   * @return True to save the download, false otherwise.
   1.316 +   */
   1.317 +  shouldPersistDownload: function (aDownload)
   1.318 +  {
   1.319 +    // In the default implementation, we save all the downloads currently in
   1.320 +    // progress, as well as stopped downloads for which we retained partially
   1.321 +    // downloaded data.  Stopped downloads for which we don't need to track the
   1.322 +    // presence of a ".part" file are only retained in the browser history.
   1.323 +    // On b2g, we keep a few days of history.
   1.324 +#ifdef MOZ_B2G
   1.325 +    let maxTime = Date.now() -
   1.326 +      Services.prefs.getIntPref("dom.downloads.max_retention_days") * 24 * 60 * 60 * 1000;
   1.327 +    return (aDownload.startTime > maxTime) ||
   1.328 +           aDownload.hasPartialData ||
   1.329 +           !aDownload.stopped;
   1.330 +#else
   1.331 +    return aDownload.hasPartialData || !aDownload.stopped;
   1.332 +#endif
   1.333 +  },
   1.334 +
   1.335 +  /**
   1.336 +   * Returns the system downloads directory asynchronously.
   1.337 +   *
   1.338 +   * @return {Promise}
   1.339 +   * @resolves The downloads directory string path.
   1.340 +   */
   1.341 +  getSystemDownloadsDirectory: function DI_getSystemDownloadsDirectory() {
   1.342 +    return Task.spawn(function() {
   1.343 +      if (this._downloadsDirectory) {
   1.344 +        // This explicitly makes this function a generator for Task.jsm. We
   1.345 +        // need this because calls to the "yield" operator below may be
   1.346 +        // preprocessed out on some platforms.
   1.347 +        yield undefined;
   1.348 +        throw new Task.Result(this._downloadsDirectory);
   1.349 +      }
   1.350 +
   1.351 +      let directoryPath = null;
   1.352 +#ifdef XP_MACOSX
   1.353 +      directoryPath = this._getDirectory("DfltDwnld");
   1.354 +#elifdef XP_WIN
   1.355 +      // For XP/2K, use My Documents/Downloads. Other version uses
   1.356 +      // the default Downloads directory.
   1.357 +      let version = parseFloat(Services.sysinfo.getProperty("version"));
   1.358 +      if (version < 6) {
   1.359 +        directoryPath = yield this._createDownloadsDirectory("Pers");
   1.360 +      } else {
   1.361 +        directoryPath = this._getDirectory("DfltDwnld");
   1.362 +      }
   1.363 +#elifdef XP_UNIX
   1.364 +#ifdef MOZ_WIDGET_ANDROID
   1.365 +      // Android doesn't have a $HOME directory, and by default we only have
   1.366 +      // write access to /data/data/org.mozilla.{$APP} and /sdcard
   1.367 +      directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
   1.368 +      if (!directoryPath) {
   1.369 +        throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
   1.370 +                                       Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
   1.371 +      }
   1.372 +#elifdef MOZ_WIDGET_GONK
   1.373 +      directoryPath = this._getDefaultDownloadDirectory();
   1.374 +#else
   1.375 +      // For Linux, use XDG download dir, with a fallback to Home/Downloads
   1.376 +      // if the XDG user dirs are disabled.
   1.377 +      try {
   1.378 +        directoryPath = this._getDirectory("DfltDwnld");
   1.379 +      } catch(e) {
   1.380 +        directoryPath = yield this._createDownloadsDirectory("Home");
   1.381 +      }
   1.382 +#endif
   1.383 +#else
   1.384 +      directoryPath = yield this._createDownloadsDirectory("Home");
   1.385 +#endif
   1.386 +      this._downloadsDirectory = directoryPath;
   1.387 +      throw new Task.Result(this._downloadsDirectory);
   1.388 +    }.bind(this));
   1.389 +  },
   1.390 +  _downloadsDirectory: null,
   1.391 +
   1.392 +  /**
   1.393 +   * Returns the user downloads directory asynchronously.
   1.394 +   *
   1.395 +   * @return {Promise}
   1.396 +   * @resolves The downloads directory string path.
   1.397 +   */
   1.398 +  getPreferredDownloadsDirectory: function DI_getPreferredDownloadsDirectory() {
   1.399 +    return Task.spawn(function() {
   1.400 +      let directoryPath = null;
   1.401 +#ifdef MOZ_WIDGET_GONK
   1.402 +      directoryPath = this._getDefaultDownloadDirectory();
   1.403 +#else
   1.404 +      let prefValue = 1;
   1.405 +
   1.406 +      try {
   1.407 +        prefValue = Services.prefs.getIntPref("browser.download.folderList");
   1.408 +      } catch(e) {}
   1.409 +
   1.410 +      switch(prefValue) {
   1.411 +        case 0: // Desktop
   1.412 +          directoryPath = this._getDirectory("Desk");
   1.413 +          break;
   1.414 +        case 1: // Downloads
   1.415 +          directoryPath = yield this.getSystemDownloadsDirectory();
   1.416 +          break;
   1.417 +        case 2: // Custom
   1.418 +          try {
   1.419 +            let directory = Services.prefs.getComplexValue("browser.download.dir",
   1.420 +                                                           Ci.nsIFile);
   1.421 +            directoryPath = directory.path;
   1.422 +            yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
   1.423 +          } catch(ex) {
   1.424 +            // Either the preference isn't set or the directory cannot be created.
   1.425 +            directoryPath = yield this.getSystemDownloadsDirectory();
   1.426 +          }
   1.427 +          break;
   1.428 +        default:
   1.429 +          directoryPath = yield this.getSystemDownloadsDirectory();
   1.430 +      }
   1.431 +#endif
   1.432 +      throw new Task.Result(directoryPath);
   1.433 +    }.bind(this));
   1.434 +  },
   1.435 +
   1.436 +  /**
   1.437 +   * Returns the temporary downloads directory asynchronously.
   1.438 +   *
   1.439 +   * @return {Promise}
   1.440 +   * @resolves The downloads directory string path.
   1.441 +   */
   1.442 +  getTemporaryDownloadsDirectory: function DI_getTemporaryDownloadsDirectory() {
   1.443 +    return Task.spawn(function() {
   1.444 +      let directoryPath = null;
   1.445 +#ifdef XP_MACOSX
   1.446 +      directoryPath = yield this.getPreferredDownloadsDirectory();
   1.447 +#elifdef MOZ_WIDGET_ANDROID
   1.448 +      directoryPath = yield this.getSystemDownloadsDirectory();
   1.449 +#elifdef MOZ_WIDGET_GONK
   1.450 +      directoryPath = yield this.getSystemDownloadsDirectory();
   1.451 +#else
   1.452 +      // For Metro mode on Windows 8,  we want searchability for documents
   1.453 +      // that the user chose to open with an external application.
   1.454 +      if (Services.metro && Services.metro.immersive) {
   1.455 +        directoryPath = yield this.getSystemDownloadsDirectory();
   1.456 +      } else {
   1.457 +        directoryPath = this._getDirectory("TmpD");
   1.458 +      }
   1.459 +#endif
   1.460 +      throw new Task.Result(directoryPath);
   1.461 +    }.bind(this));
   1.462 +  },
   1.463 +
   1.464 +  /**
   1.465 +   * Checks to determine whether to block downloads for parental controls.
   1.466 +   *
   1.467 +   * aParam aDownload
   1.468 +   *        The download object.
   1.469 +   *
   1.470 +   * @return {Promise}
   1.471 +   * @resolves The boolean indicates to block downloads or not.
   1.472 +   */
   1.473 +  shouldBlockForParentalControls: function DI_shouldBlockForParentalControls(aDownload) {
   1.474 +    if (this.dontCheckParentalControls) {
   1.475 +      return Promise.resolve(this.shouldBlockInTest);
   1.476 +    }
   1.477 +
   1.478 +    let isEnabled = gParentalControlsService &&
   1.479 +                    gParentalControlsService.parentalControlsEnabled;
   1.480 +    let shouldBlock = isEnabled &&
   1.481 +                      gParentalControlsService.blockFileDownloadsEnabled;
   1.482 +
   1.483 +    // Log the event if required by parental controls settings.
   1.484 +    if (isEnabled && gParentalControlsService.loggingEnabled) {
   1.485 +      gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload,
   1.486 +                                   shouldBlock,
   1.487 +                                   NetUtil.newURI(aDownload.source.url), null);
   1.488 +    }
   1.489 +
   1.490 +    return Promise.resolve(shouldBlock);
   1.491 +  },
   1.492 +
   1.493 +  /**
   1.494 +   * Checks to determine whether to block downloads because they might be
   1.495 +   * malware, based on application reputation checks.
   1.496 +   *
   1.497 +   * aParam aDownload
   1.498 +   *        The download object.
   1.499 +   *
   1.500 +   * @return {Promise}
   1.501 +   * @resolves The boolean indicates to block downloads or not.
   1.502 +   */
   1.503 +  shouldBlockForReputationCheck: function (aDownload) {
   1.504 +    if (this.dontCheckApplicationReputation) {
   1.505 +      return Promise.resolve(this.shouldBlockInTestForApplicationReputation);
   1.506 +    }
   1.507 +    let hash;
   1.508 +    let sigInfo;
   1.509 +    try {
   1.510 +      hash = aDownload.saver.getSha256Hash();
   1.511 +      sigInfo = aDownload.saver.getSignatureInfo();
   1.512 +    } catch (ex) {
   1.513 +      // Bail if DownloadSaver doesn't have a hash.
   1.514 +      return Promise.resolve(false);
   1.515 +    }
   1.516 +    if (!hash || !sigInfo) {
   1.517 +      return Promise.resolve(false);
   1.518 +    }
   1.519 +    let deferred = Promise.defer();
   1.520 +    let aReferrer = null;
   1.521 +    if (aDownload.source.referrer) {
   1.522 +      aReferrer: NetUtil.newURI(aDownload.source.referrer);
   1.523 +    }
   1.524 +    gApplicationReputationService.queryReputation({
   1.525 +      sourceURI: NetUtil.newURI(aDownload.source.url),
   1.526 +      referrerURI: aReferrer,
   1.527 +      fileSize: aDownload.currentBytes,
   1.528 +      sha256Hash: hash,
   1.529 +      signatureInfo: sigInfo },
   1.530 +      function onComplete(aShouldBlock, aRv) {
   1.531 +        deferred.resolve(aShouldBlock);
   1.532 +      });
   1.533 +    return deferred.promise;
   1.534 +  },
   1.535 +
   1.536 +#ifdef XP_WIN
   1.537 +  /**
   1.538 +   * Checks whether downloaded files should be marked as coming from
   1.539 +   * Internet Zone.
   1.540 +   *
   1.541 +   * @return true if files should be marked
   1.542 +   */
   1.543 +  _shouldSaveZoneInformation: function() {
   1.544 +    let key = Cc["@mozilla.org/windows-registry-key;1"]
   1.545 +                .createInstance(Ci.nsIWindowsRegKey);
   1.546 +    try {
   1.547 +      key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
   1.548 +               "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments",
   1.549 +               Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE);
   1.550 +      try {
   1.551 +        return key.readIntValue("SaveZoneInformation") != 1;
   1.552 +      } finally {
   1.553 +        key.close();
   1.554 +      }
   1.555 +    } catch (ex) {
   1.556 +      // If the key is not present, files should be marked by default.
   1.557 +      return true;
   1.558 +    }
   1.559 +  },
   1.560 +#endif
   1.561 +
   1.562 +  /**
   1.563 +   * Performs platform-specific operations when a download is done.
   1.564 +   *
   1.565 +   * aParam aDownload
   1.566 +   *        The Download object.
   1.567 +   *
   1.568 +   * @return {Promise}
   1.569 +   * @resolves When all the operations completed successfully.
   1.570 +   * @rejects JavaScript exception if any of the operations failed.
   1.571 +   */
   1.572 +  downloadDone: function(aDownload) {
   1.573 +    return Task.spawn(function () {
   1.574 +#ifdef XP_WIN
   1.575 +      // On Windows, we mark any file saved to the NTFS file system as coming
   1.576 +      // from the Internet security zone unless Group Policy disables the
   1.577 +      // feature.  We do this by writing to the "Zone.Identifier" Alternate
   1.578 +      // Data Stream directly, because the Save method of the
   1.579 +      // IAttachmentExecute interface would trigger operations that may cause
   1.580 +      // the application to hang, or other performance issues.
   1.581 +      // The stream created in this way is forward-compatible with all the
   1.582 +      // current and future versions of Windows.
   1.583 +      if (this._shouldSaveZoneInformation()) {
   1.584 +        let zone;
   1.585 +        try {
   1.586 +          zone = gDownloadPlatform.mapUrlToZone(aDownload.source.url);
   1.587 +        } catch (e) {
   1.588 +          // Default to Internet Zone if mapUrlToZone failed for
   1.589 +          // whatever reason.
   1.590 +          zone = Ci.mozIDownloadPlatform.ZONE_INTERNET;
   1.591 +        }
   1.592 +        try {
   1.593 +          // Don't write zone IDs for Local, Intranet, or Trusted sites
   1.594 +          // to match Windows behavior.
   1.595 +          if (zone >= Ci.mozIDownloadPlatform.ZONE_INTERNET) {
   1.596 +            let streamPath = aDownload.target.path + ":Zone.Identifier";
   1.597 +            let stream = yield OS.File.open(streamPath, { create: true });
   1.598 +            try {
   1.599 +              yield stream.write(new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=" + zone + "\r\n"));
   1.600 +            } finally {
   1.601 +              yield stream.close();
   1.602 +            }
   1.603 +          }
   1.604 +        } catch (ex) {
   1.605 +          // If writing to the stream fails, we ignore the error and continue.
   1.606 +          // The Windows API error 123 (ERROR_INVALID_NAME) is expected to
   1.607 +          // occur when working on a file system that does not support
   1.608 +          // Alternate Data Streams, like FAT32, thus we don't report this
   1.609 +          // specific error.
   1.610 +          if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) {
   1.611 +            Cu.reportError(ex);
   1.612 +          }
   1.613 +        }
   1.614 +      }
   1.615 +#endif
   1.616 +
   1.617 +      // Now that the file is completely downloaded, mark it
   1.618 +      // accessible by other users on this system, if the user's
   1.619 +      // global preferences so indicate.  (On Unix, this applies the
   1.620 +      // umask.  On Windows, currently does nothing.)
   1.621 +      // Errors should be reported, but are not fatal.
   1.622 +      try {
   1.623 +        yield OS.File.setPermissions(aDownload.target.path);
   1.624 +      } catch (ex) {
   1.625 +        Cu.reportError(ex);
   1.626 +      }
   1.627 +
   1.628 +      gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),
   1.629 +                                     new FileUtils.File(aDownload.target.path),
   1.630 +                                     aDownload.contentType,
   1.631 +                                     aDownload.source.isPrivate);
   1.632 +      this.downloadDoneCalled = true;
   1.633 +    }.bind(this));
   1.634 +  },
   1.635 +
   1.636 +  /*
   1.637 +   * Launches a file represented by the target of a download. This can
   1.638 +   * open the file with the default application for the target MIME type
   1.639 +   * or file extension, or with a custom application if
   1.640 +   * aDownload.launcherPath is set.
   1.641 +   *
   1.642 +   * @param    aDownload
   1.643 +   *           A Download object that contains the necessary information
   1.644 +   *           to launch the file. The relevant properties are: the target
   1.645 +   *           file, the contentType and the custom application chosen
   1.646 +   *           to launch it.
   1.647 +   *
   1.648 +   * @return {Promise}
   1.649 +   * @resolves When the instruction to launch the file has been
   1.650 +   *           successfully given to the operating system. Note that
   1.651 +   *           the OS might still take a while until the file is actually
   1.652 +   *           launched.
   1.653 +   * @rejects  JavaScript exception if there was an error trying to launch
   1.654 +   *           the file.
   1.655 +   */
   1.656 +  launchDownload: function (aDownload) {
   1.657 +    let deferred = Task.spawn(function DI_launchDownload_task() {
   1.658 +      let file = new FileUtils.File(aDownload.target.path);
   1.659 +
   1.660 +#ifndef XP_WIN
   1.661 +      // Ask for confirmation if the file is executable, except on Windows where
   1.662 +      // the operating system will show the prompt based on the security zone.
   1.663 +      // We do this here, instead of letting the caller handle the prompt
   1.664 +      // separately in the user interface layer, for two reasons.  The first is
   1.665 +      // because of its security nature, so that add-ons cannot forget to do
   1.666 +      // this check.  The second is that the system-level security prompt would
   1.667 +      // be displayed at launch time in any case.
   1.668 +      if (file.isExecutable() && !this.dontOpenFileAndFolder) {
   1.669 +        // We don't anchor the prompt to a specific window intentionally, not
   1.670 +        // only because this is the same behavior as the system-level prompt,
   1.671 +        // but also because the most recently active window is the right choice
   1.672 +        // in basically all cases.
   1.673 +        let shouldLaunch = yield DownloadUIHelper.getPrompter()
   1.674 +                                   .confirmLaunchExecutable(file.path);
   1.675 +        if (!shouldLaunch) {
   1.676 +          return;
   1.677 +        }
   1.678 +      }
   1.679 +#endif
   1.680 +
   1.681 +      // In case of a double extension, like ".tar.gz", we only
   1.682 +      // consider the last one, because the MIME service cannot
   1.683 +      // handle multiple extensions.
   1.684 +      let fileExtension = null, mimeInfo = null;
   1.685 +      let match = file.leafName.match(/\.([^.]+)$/);
   1.686 +      if (match) {
   1.687 +        fileExtension = match[1];
   1.688 +      }
   1.689 +
   1.690 +      try {
   1.691 +        // The MIME service might throw if contentType == "" and it can't find
   1.692 +        // a MIME type for the given extension, so we'll treat this case as
   1.693 +        // an unknown mimetype.
   1.694 +        mimeInfo = gMIMEService.getFromTypeAndExtension(aDownload.contentType,
   1.695 +                                                        fileExtension);
   1.696 +      } catch (e) { }
   1.697 +
   1.698 +      if (aDownload.launcherPath) {
   1.699 +        if (!mimeInfo) {
   1.700 +          // This should not happen on normal circumstances because launcherPath
   1.701 +          // is only set when we had an instance of nsIMIMEInfo to retrieve
   1.702 +          // the custom application chosen by the user.
   1.703 +          throw new Error(
   1.704 +            "Unable to create nsIMIMEInfo to launch a custom application");
   1.705 +        }
   1.706 +
   1.707 +        // Custom application chosen
   1.708 +        let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
   1.709 +                                .createInstance(Ci.nsILocalHandlerApp);
   1.710 +        localHandlerApp.executable = new FileUtils.File(aDownload.launcherPath);
   1.711 +
   1.712 +        mimeInfo.preferredApplicationHandler = localHandlerApp;
   1.713 +        mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
   1.714 +
   1.715 +        // In test mode, allow the test to verify the nsIMIMEInfo instance.
   1.716 +        if (this.dontOpenFileAndFolder) {
   1.717 +          throw new Task.Result(mimeInfo);
   1.718 +        }
   1.719 +
   1.720 +        mimeInfo.launchWithFile(file);
   1.721 +        return;
   1.722 +      }
   1.723 +
   1.724 +      // No custom application chosen, let's launch the file with the default
   1.725 +      // handler.  In test mode, we indicate this with a null value.
   1.726 +      if (this.dontOpenFileAndFolder) {
   1.727 +        throw new Task.Result(null);
   1.728 +      }
   1.729 +
   1.730 +      // First let's try to launch it through the MIME service application
   1.731 +      // handler
   1.732 +      if (mimeInfo) {
   1.733 +        mimeInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
   1.734 +
   1.735 +        try {
   1.736 +          mimeInfo.launchWithFile(file);
   1.737 +          return;
   1.738 +        } catch (ex) { }
   1.739 +      }
   1.740 +
   1.741 +      // If it didn't work or if there was no MIME info available,
   1.742 +      // let's try to directly launch the file.
   1.743 +      try {
   1.744 +        file.launch();
   1.745 +        return;
   1.746 +      } catch (ex) { }
   1.747 +
   1.748 +      // If our previous attempts failed, try sending it through
   1.749 +      // the system's external "file:" URL handler.
   1.750 +      gExternalProtocolService.loadUrl(NetUtil.newURI(file));
   1.751 +      yield undefined;
   1.752 +    }.bind(this));
   1.753 +
   1.754 +    if (this.dontOpenFileAndFolder) {
   1.755 +      deferred.then((value) => { this._deferTestOpenFile.resolve(value); },
   1.756 +                    (error) => { this._deferTestOpenFile.reject(error); });
   1.757 +    }
   1.758 +
   1.759 +    return deferred;
   1.760 +  },
   1.761 +
   1.762 +  /*
   1.763 +   * Shows the containing folder of a file.
   1.764 +   *
   1.765 +   * @param    aFilePath
   1.766 +   *           The path to the file.
   1.767 +   *
   1.768 +   * @return {Promise}
   1.769 +   * @resolves When the instruction to open the containing folder has been
   1.770 +   *           successfully given to the operating system. Note that
   1.771 +   *           the OS might still take a while until the folder is actually
   1.772 +   *           opened.
   1.773 +   * @rejects  JavaScript exception if there was an error trying to open
   1.774 +   *           the containing folder.
   1.775 +   */
   1.776 +  showContainingDirectory: function (aFilePath) {
   1.777 +    let deferred = Task.spawn(function DI_showContainingDirectory_task() {
   1.778 +      let file = new FileUtils.File(aFilePath);
   1.779 +
   1.780 +      if (this.dontOpenFileAndFolder) {
   1.781 +        return;
   1.782 +      }
   1.783 +
   1.784 +      try {
   1.785 +        // Show the directory containing the file and select the file.
   1.786 +        file.reveal();
   1.787 +        return;
   1.788 +      } catch (ex) { }
   1.789 +
   1.790 +      // If reveal fails for some reason (e.g., it's not implemented on unix
   1.791 +      // or the file doesn't exist), try using the parent if we have it.
   1.792 +      let parent = file.parent;
   1.793 +      if (!parent) {
   1.794 +        throw new Error(
   1.795 +          "Unexpected reference to a top-level directory instead of a file");
   1.796 +      }
   1.797 +
   1.798 +      try {
   1.799 +        // Open the parent directory to show where the file should be.
   1.800 +        parent.launch();
   1.801 +        return;
   1.802 +      } catch (ex) { }
   1.803 +
   1.804 +      // If launch also fails (probably because it's not implemented), let
   1.805 +      // the OS handler try to open the parent.
   1.806 +      gExternalProtocolService.loadUrl(NetUtil.newURI(parent));
   1.807 +      yield undefined;
   1.808 +    }.bind(this));
   1.809 +
   1.810 +    if (this.dontOpenFileAndFolder) {
   1.811 +      deferred.then((value) => { this._deferTestShowDir.resolve("success"); },
   1.812 +                    (error) => {
   1.813 +                      // Ensure that _deferTestShowDir has at least one consumer
   1.814 +                      // for the error, otherwise the error will be reported as
   1.815 +                      // uncaught.
   1.816 +                      this._deferTestShowDir.promise.then(null, function() {});
   1.817 +                      this._deferTestShowDir.reject(error);
   1.818 +                    });
   1.819 +    }
   1.820 +
   1.821 +    return deferred;
   1.822 +  },
   1.823 +
   1.824 +  /**
   1.825 +   * Calls the directory service, create a downloads directory and returns an
   1.826 +   * nsIFile for the downloads directory.
   1.827 +   *
   1.828 +   * @return {Promise}
   1.829 +   * @resolves The directory string path.
   1.830 +   */
   1.831 +  _createDownloadsDirectory: function DI_createDownloadsDirectory(aName) {
   1.832 +    // We read the name of the directory from the list of translated strings
   1.833 +    // that is kept by the UI helper module, even if this string is not strictly
   1.834 +    // displayed in the user interface.
   1.835 +    let directoryPath = OS.Path.join(this._getDirectory(aName),
   1.836 +                                     DownloadUIHelper.strings.downloadsFolder);
   1.837 +
   1.838 +    // Create the Downloads folder and ignore if it already exists.
   1.839 +    return OS.File.makeDir(directoryPath, { ignoreExisting: true }).
   1.840 +             then(function() {
   1.841 +               return directoryPath;
   1.842 +             });
   1.843 +  },
   1.844 +
   1.845 +  /**
   1.846 +   * Calls the directory service and returns an nsIFile for the requested
   1.847 +   * location name.
   1.848 +   *
   1.849 +   * @return The directory string path.
   1.850 +   */
   1.851 +  _getDirectory: function DI_getDirectory(aName) {
   1.852 +    return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile).path;
   1.853 +  },
   1.854 +
   1.855 +  /**
   1.856 +   * Register the downloads interruption observers.
   1.857 +   *
   1.858 +   * @param aList
   1.859 +   *        The public or private downloads list.
   1.860 +   * @param aIsPrivate
   1.861 +   *        True if the list is private, false otherwise.
   1.862 +   *
   1.863 +   * @return {Promise}
   1.864 +   * @resolves When the views and observers are added.
   1.865 +   */
   1.866 +  addListObservers: function DI_addListObservers(aList, aIsPrivate) {
   1.867 +    if (this.dontLoadObservers) {
   1.868 +      return Promise.resolve();
   1.869 +    }
   1.870 +
   1.871 +    DownloadObserver.registerView(aList, aIsPrivate);
   1.872 +    if (!DownloadObserver.observersAdded) {
   1.873 +      DownloadObserver.observersAdded = true;
   1.874 +      for (let topic of kObserverTopics) {
   1.875 +        Services.obs.addObserver(DownloadObserver, topic, false);
   1.876 +      }
   1.877 +    }
   1.878 +    return Promise.resolve();
   1.879 +  },
   1.880 +
   1.881 +  /**
   1.882 +   * Checks if we have already imported (or attempted to import)
   1.883 +   * the downloads database from the previous SQLite storage.
   1.884 +   *
   1.885 +   * @return boolean True if we the previous DB was imported.
   1.886 +   */
   1.887 +  get _importedFromSqlite() {
   1.888 +    try {
   1.889 +      return Services.prefs.getBoolPref(kPrefImportedFromSqlite);
   1.890 +    } catch (ex) {
   1.891 +      return false;
   1.892 +    }
   1.893 +  },
   1.894 +};
   1.895 +
   1.896 +////////////////////////////////////////////////////////////////////////////////
   1.897 +//// DownloadObserver
   1.898 +
   1.899 +this.DownloadObserver = {
   1.900 +  /**
   1.901 +   * Flag to determine if the observers have been added previously.
   1.902 +   */
   1.903 +  observersAdded: false,
   1.904 +
   1.905 +  /**
   1.906 +   * Timer used to delay restarting canceled downloads upon waking and returning
   1.907 +   * online.
   1.908 +   */
   1.909 +  _wakeTimer: null,
   1.910 +
   1.911 +  /**
   1.912 +   * Set that contains the in progress publics downloads.
   1.913 +   * It's kept updated when a public download is added, removed or changes its
   1.914 +   * properties.
   1.915 +   */
   1.916 +  _publicInProgressDownloads: new Set(),
   1.917 +
   1.918 +  /**
   1.919 +   * Set that contains the in progress private downloads.
   1.920 +   * It's kept updated when a private download is added, removed or changes its
   1.921 +   * properties.
   1.922 +   */
   1.923 +  _privateInProgressDownloads: new Set(),
   1.924 +
   1.925 +  /**
   1.926 +   * Set that contains the downloads that have been canceled when going offline
   1.927 +   * or to sleep. These are started again when returning online or waking. This
   1.928 +   * list is not persisted so when exiting and restarting, the downloads will not
   1.929 +   * be started again.
   1.930 +   */
   1.931 +  _canceledOfflineDownloads: new Set(),
   1.932 +
   1.933 +  /**
   1.934 +   * Registers a view that updates the corresponding downloads state set, based
   1.935 +   * on the aIsPrivate argument. The set is updated when a download is added,
   1.936 +   * removed or changes its properties.
   1.937 +   *
   1.938 +   * @param aList
   1.939 +   *        The public or private downloads list.
   1.940 +   * @param aIsPrivate
   1.941 +   *        True if the list is private, false otherwise.
   1.942 +   */
   1.943 +  registerView: function DO_registerView(aList, aIsPrivate) {
   1.944 +    let downloadsSet = aIsPrivate ? this._privateInProgressDownloads
   1.945 +                                  : this._publicInProgressDownloads;
   1.946 +    let downloadsView = {
   1.947 +      onDownloadAdded: aDownload => {
   1.948 +        if (!aDownload.stopped) {
   1.949 +          downloadsSet.add(aDownload);
   1.950 +        }
   1.951 +      },
   1.952 +      onDownloadChanged: aDownload => {
   1.953 +        if (aDownload.stopped) {
   1.954 +          downloadsSet.delete(aDownload);
   1.955 +        } else {
   1.956 +          downloadsSet.add(aDownload);
   1.957 +        }
   1.958 +      },
   1.959 +      onDownloadRemoved: aDownload => {
   1.960 +        downloadsSet.delete(aDownload);
   1.961 +        // The download must also be removed from the canceled when offline set.
   1.962 +        this._canceledOfflineDownloads.delete(aDownload);
   1.963 +      }
   1.964 +    };
   1.965 +
   1.966 +    // We register the view asynchronously.
   1.967 +    aList.addView(downloadsView).then(null, Cu.reportError);
   1.968 +  },
   1.969 +
   1.970 +  /**
   1.971 +   * Wrapper that handles the test mode before calling the prompt that display
   1.972 +   * a warning message box that informs that there are active downloads,
   1.973 +   * and asks whether the user wants to cancel them or not.
   1.974 +   *
   1.975 +   * @param aCancel
   1.976 +   *        The observer notification subject.
   1.977 +   * @param aDownloadsCount
   1.978 +   *        The current downloads count.
   1.979 +   * @param aPrompter
   1.980 +   *        The prompter object that shows the confirm dialog.
   1.981 +   * @param aPromptType
   1.982 +   *        The type of prompt notification depending on the observer.
   1.983 +   */
   1.984 +  _confirmCancelDownloads: function DO_confirmCancelDownload(
   1.985 +    aCancel, aDownloadsCount, aPrompter, aPromptType) {
   1.986 +    // If user has already dismissed the request, then do nothing.
   1.987 +    if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) {
   1.988 +      return;
   1.989 +    }
   1.990 +    // Handle test mode
   1.991 +    if (DownloadIntegration.testMode) {
   1.992 +      DownloadIntegration.testPromptDownloads = aDownloadsCount;
   1.993 +      return;
   1.994 +    }
   1.995 +
   1.996 +    aCancel.data = aPrompter.confirmCancelDownloads(aDownloadsCount, aPromptType);
   1.997 +  },
   1.998 +
   1.999 +  /**
  1.1000 +   * Resume all downloads that were paused when going offline, used when waking
  1.1001 +   * from sleep or returning from being offline.
  1.1002 +   */
  1.1003 +  _resumeOfflineDownloads: function DO_resumeOfflineDownloads() {
  1.1004 +    this._wakeTimer = null;
  1.1005 +
  1.1006 +    for (let download of this._canceledOfflineDownloads) {
  1.1007 +      download.start();
  1.1008 +    }
  1.1009 +  },
  1.1010 +
  1.1011 +  ////////////////////////////////////////////////////////////////////////////
  1.1012 +  //// nsIObserver
  1.1013 +
  1.1014 +  observe: function DO_observe(aSubject, aTopic, aData) {
  1.1015 +    let downloadsCount;
  1.1016 +    let p = DownloadUIHelper.getPrompter();
  1.1017 +    switch (aTopic) {
  1.1018 +      case "quit-application-requested":
  1.1019 +        downloadsCount = this._publicInProgressDownloads.size +
  1.1020 +                         this._privateInProgressDownloads.size;
  1.1021 +        this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_QUIT);
  1.1022 +        break;
  1.1023 +      case "offline-requested":
  1.1024 +        downloadsCount = this._publicInProgressDownloads.size +
  1.1025 +                         this._privateInProgressDownloads.size;
  1.1026 +        this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_OFFLINE);
  1.1027 +        break;
  1.1028 +      case "last-pb-context-exiting":
  1.1029 +        downloadsCount = this._privateInProgressDownloads.size;
  1.1030 +        this._confirmCancelDownloads(aSubject, downloadsCount, p,
  1.1031 +                                     p.ON_LEAVE_PRIVATE_BROWSING);
  1.1032 +        break;
  1.1033 +      case "last-pb-context-exited":
  1.1034 +        let deferred = Task.spawn(function() {
  1.1035 +          let list = yield Downloads.getList(Downloads.PRIVATE);
  1.1036 +          let downloads = yield list.getAll();
  1.1037 +
  1.1038 +          // We can remove the downloads and finalize them in parallel.
  1.1039 +          for (let download of downloads) {
  1.1040 +            list.remove(download).then(null, Cu.reportError);
  1.1041 +            download.finalize(true).then(null, Cu.reportError);
  1.1042 +          }
  1.1043 +        });
  1.1044 +        // Handle test mode
  1.1045 +        if (DownloadIntegration.testMode) {
  1.1046 +          deferred.then((value) => { DownloadIntegration._deferTestClearPrivateList.resolve("success"); },
  1.1047 +                        (error) => { DownloadIntegration._deferTestClearPrivateList.reject(error); });
  1.1048 +        }
  1.1049 +        break;
  1.1050 +      case "sleep_notification":
  1.1051 +      case "suspend_process_notification":
  1.1052 +      case "network:offline-about-to-go-offline":
  1.1053 +        for (let download of this._publicInProgressDownloads) {
  1.1054 +          download.cancel();
  1.1055 +          this._canceledOfflineDownloads.add(download);
  1.1056 +        }
  1.1057 +        for (let download of this._privateInProgressDownloads) {
  1.1058 +          download.cancel();
  1.1059 +          this._canceledOfflineDownloads.add(download);
  1.1060 +        }
  1.1061 +        break;
  1.1062 +      case "wake_notification":
  1.1063 +      case "resume_process_notification":
  1.1064 +        let wakeDelay = 10000;
  1.1065 +        try {
  1.1066 +          wakeDelay = Services.prefs.getIntPref("browser.download.manager.resumeOnWakeDelay");
  1.1067 +        } catch(e) {}
  1.1068 +
  1.1069 +        if (wakeDelay >= 0) {
  1.1070 +          this._wakeTimer = new Timer(this._resumeOfflineDownloads.bind(this), wakeDelay,
  1.1071 +                                      Ci.nsITimer.TYPE_ONE_SHOT);
  1.1072 +        }
  1.1073 +        break;
  1.1074 +      case "network:offline-status-changed":
  1.1075 +        if (aData == "online") {
  1.1076 +          this._resumeOfflineDownloads();
  1.1077 +        }
  1.1078 +        break;
  1.1079 +      // We need to unregister observers explicitly before we reach the
  1.1080 +      // "xpcom-shutdown" phase, otherwise observers may be notified when some
  1.1081 +      // required services are not available anymore. We can't unregister
  1.1082 +      // observers on "quit-application", because this module is also loaded
  1.1083 +      // during "make package" automation, and the quit notification is not sent
  1.1084 +      // in that execution environment (bug 973637).
  1.1085 +      case "xpcom-will-shutdown":
  1.1086 +        for (let topic of kObserverTopics) {
  1.1087 +          Services.obs.removeObserver(this, topic);
  1.1088 +        }
  1.1089 +        break;
  1.1090 +    }
  1.1091 +  },
  1.1092 +
  1.1093 +  ////////////////////////////////////////////////////////////////////////////
  1.1094 +  //// nsISupports
  1.1095 +
  1.1096 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
  1.1097 +};
  1.1098 +
  1.1099 +////////////////////////////////////////////////////////////////////////////////
  1.1100 +//// DownloadHistoryObserver
  1.1101 +
  1.1102 +#ifdef MOZ_PLACES
  1.1103 +/**
  1.1104 + * Registers a Places observer so that operations on download history are
  1.1105 + * reflected on the provided list of downloads.
  1.1106 + *
  1.1107 + * You do not need to keep a reference to this object in order to keep it alive,
  1.1108 + * because the history service already keeps a strong reference to it.
  1.1109 + *
  1.1110 + * @param aList
  1.1111 + *        DownloadList object linked to this observer.
  1.1112 + */
  1.1113 +this.DownloadHistoryObserver = function (aList)
  1.1114 +{
  1.1115 +  this._list = aList;
  1.1116 +  PlacesUtils.history.addObserver(this, false);
  1.1117 +}
  1.1118 +
  1.1119 +this.DownloadHistoryObserver.prototype = {
  1.1120 +  /**
  1.1121 +   * DownloadList object linked to this observer.
  1.1122 +   */
  1.1123 +  _list: null,
  1.1124 +
  1.1125 +  ////////////////////////////////////////////////////////////////////////////
  1.1126 +  //// nsISupports
  1.1127 +
  1.1128 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
  1.1129 +
  1.1130 +  ////////////////////////////////////////////////////////////////////////////
  1.1131 +  //// nsINavHistoryObserver
  1.1132 +
  1.1133 +  onDeleteURI: function DL_onDeleteURI(aURI, aGUID) {
  1.1134 +    this._list.removeFinished(download => aURI.equals(NetUtil.newURI(
  1.1135 +                                                      download.source.url)));
  1.1136 +  },
  1.1137 +
  1.1138 +  onClearHistory: function DL_onClearHistory() {
  1.1139 +    this._list.removeFinished();
  1.1140 +  },
  1.1141 +
  1.1142 +  onTitleChanged: function () {},
  1.1143 +  onBeginUpdateBatch: function () {},
  1.1144 +  onEndUpdateBatch: function () {},
  1.1145 +  onVisit: function () {},
  1.1146 +  onPageChanged: function () {},
  1.1147 +  onDeleteVisits: function () {},
  1.1148 +};
  1.1149 +#else
  1.1150 +/**
  1.1151 + * Empty implementation when we have no Places support, for example on B2G.
  1.1152 + */
  1.1153 +this.DownloadHistoryObserver = function (aList) {}
  1.1154 +#endif
  1.1155 +
  1.1156 +////////////////////////////////////////////////////////////////////////////////
  1.1157 +//// DownloadAutoSaveView
  1.1158 +
  1.1159 +/**
  1.1160 + * This view can be added to a DownloadList object to trigger a save operation
  1.1161 + * in the given DownloadStore object when a relevant change occurs.  You should
  1.1162 + * call the "initialize" method in order to register the view and load the
  1.1163 + * current state from disk.
  1.1164 + *
  1.1165 + * You do not need to keep a reference to this object in order to keep it alive,
  1.1166 + * because the DownloadList object already keeps a strong reference to it.
  1.1167 + *
  1.1168 + * @param aList
  1.1169 + *        The DownloadList object on which the view should be registered.
  1.1170 + * @param aStore
  1.1171 + *        The DownloadStore object used for saving.
  1.1172 + */
  1.1173 +this.DownloadAutoSaveView = function (aList, aStore)
  1.1174 +{
  1.1175 +  this._list = aList;
  1.1176 +  this._store = aStore;
  1.1177 +  this._downloadsMap = new Map();
  1.1178 +  this._writer = new DeferredTask(() => this._store.save(), kSaveDelayMs);
  1.1179 +}
  1.1180 +
  1.1181 +this.DownloadAutoSaveView.prototype = {
  1.1182 +  /**
  1.1183 +   * DownloadList object linked to this view.
  1.1184 +   */
  1.1185 +  _list: null,
  1.1186 +
  1.1187 +  /**
  1.1188 +   * The DownloadStore object used for saving.
  1.1189 +   */
  1.1190 +  _store: null,
  1.1191 +
  1.1192 +  /**
  1.1193 +   * True when the initial state of the downloads has been loaded.
  1.1194 +   */
  1.1195 +  _initialized: false,
  1.1196 +
  1.1197 +  /**
  1.1198 +   * Registers the view and loads the current state from disk.
  1.1199 +   *
  1.1200 +   * @return {Promise}
  1.1201 +   * @resolves When the view has been registered.
  1.1202 +   * @rejects JavaScript exception.
  1.1203 +   */
  1.1204 +  initialize: function ()
  1.1205 +  {
  1.1206 +    // We set _initialized to true after adding the view, so that
  1.1207 +    // onDownloadAdded doesn't cause a save to occur.
  1.1208 +    return this._list.addView(this).then(() => this._initialized = true);
  1.1209 +  },
  1.1210 +
  1.1211 +  /**
  1.1212 +   * This map contains only Download objects that should be saved to disk, and
  1.1213 +   * associates them with the result of their getSerializationHash function, for
  1.1214 +   * the purpose of detecting changes to the relevant properties.
  1.1215 +   */
  1.1216 +  _downloadsMap: null,
  1.1217 +
  1.1218 +  /**
  1.1219 +   * DeferredTask for the save operation.
  1.1220 +   */
  1.1221 +  _writer: null,
  1.1222 +
  1.1223 +  /**
  1.1224 +   * Called when the list of downloads changed, this triggers the asynchronous
  1.1225 +   * serialization of the list of downloads.
  1.1226 +   */
  1.1227 +  saveSoon: function ()
  1.1228 +  {
  1.1229 +    this._writer.arm();
  1.1230 +  },
  1.1231 +
  1.1232 +  //////////////////////////////////////////////////////////////////////////////
  1.1233 +  //// DownloadList view
  1.1234 +
  1.1235 +  onDownloadAdded: function (aDownload)
  1.1236 +  {
  1.1237 +    if (DownloadIntegration.shouldPersistDownload(aDownload)) {
  1.1238 +      this._downloadsMap.set(aDownload, aDownload.getSerializationHash());
  1.1239 +      if (this._initialized) {
  1.1240 +        this.saveSoon();
  1.1241 +      }
  1.1242 +    }
  1.1243 +  },
  1.1244 +
  1.1245 +  onDownloadChanged: function (aDownload)
  1.1246 +  {
  1.1247 +    if (!DownloadIntegration.shouldPersistDownload(aDownload)) {
  1.1248 +      if (this._downloadsMap.has(aDownload)) {
  1.1249 +        this._downloadsMap.delete(aDownload);
  1.1250 +        this.saveSoon();
  1.1251 +      }
  1.1252 +      return;
  1.1253 +    }
  1.1254 +
  1.1255 +    let hash = aDownload.getSerializationHash();
  1.1256 +    if (this._downloadsMap.get(aDownload) != hash) {
  1.1257 +      this._downloadsMap.set(aDownload, hash);
  1.1258 +      this.saveSoon();
  1.1259 +    }
  1.1260 +  },
  1.1261 +
  1.1262 +  onDownloadRemoved: function (aDownload)
  1.1263 +  {
  1.1264 +    if (this._downloadsMap.has(aDownload)) {
  1.1265 +      this._downloadsMap.delete(aDownload);
  1.1266 +      this.saveSoon();
  1.1267 +    }
  1.1268 +  },
  1.1269 +};

mercurial