Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
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 | }; |