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 +};