michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Provides functions to integrate with the host application, handling for michael@0: * example the global prompts on shutdown. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "DownloadIntegration", michael@0: ]; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", michael@0: "resource://gre/modules/DeferredTask.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Downloads", michael@0: "resource://gre/modules/Downloads.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore", michael@0: "resource://gre/modules/DownloadStore.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport", michael@0: "resource://gre/modules/DownloadImport.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", michael@0: "resource://gre/modules/DownloadUIHelper.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", michael@0: "resource://gre/modules/FileUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: #ifdef MOZ_PLACES michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: #endif michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Services", michael@0: "resource://gre/modules/Services.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gDownloadPlatform", michael@0: "@mozilla.org/toolkit/download-platform;1", michael@0: "mozIDownloadPlatform"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gEnvironment", michael@0: "@mozilla.org/process/environment;1", michael@0: "nsIEnvironment"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService", michael@0: "@mozilla.org/mime;1", michael@0: "nsIMIMEService"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gExternalProtocolService", michael@0: "@mozilla.org/uriloader/external-protocol-service;1", michael@0: "nsIExternalProtocolService"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() { michael@0: if ("@mozilla.org/parental-controls-service;1" in Cc) { michael@0: return Cc["@mozilla.org/parental-controls-service;1"] michael@0: .createInstance(Ci.nsIParentalControlsService); michael@0: } michael@0: return null; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gApplicationReputationService", michael@0: "@mozilla.org/downloads/application-reputation-service;1", michael@0: Ci.nsIApplicationReputationService); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "volumeService", michael@0: "@mozilla.org/telephony/volume-service;1", michael@0: "nsIVolumeService"); michael@0: michael@0: const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", michael@0: "initWithCallback"); michael@0: michael@0: /** michael@0: * Indicates the delay between a change to the downloads data and the related michael@0: * save operation. This value is the result of a delicate trade-off, assuming michael@0: * the host application uses the browser history instead of the download store michael@0: * to save completed downloads. michael@0: * michael@0: * If a download takes less than this interval to complete (for example, saving michael@0: * a page that is already displayed), then no input/output is triggered by the michael@0: * download store except for an existence check, resulting in the best possible michael@0: * efficiency. michael@0: * michael@0: * Conversely, if the browser is closed before this interval has passed, the michael@0: * download will not be saved. This prevents it from being restored in the next michael@0: * session, and if there is partial data associated with it, then the ".part" michael@0: * file will not be deleted when the browser starts again. michael@0: * michael@0: * In all cases, for best efficiency, this value should be high enough that the michael@0: * input/output for opening or closing the target file does not overlap with the michael@0: * one for saving the list of downloads. michael@0: */ michael@0: const kSaveDelayMs = 1500; michael@0: michael@0: /** michael@0: * This pref indicates if we have already imported (or attempted to import) michael@0: * the downloads database from the previous SQLite storage. michael@0: */ michael@0: const kPrefImportedFromSqlite = "browser.download.importedFromSqlite"; michael@0: michael@0: /** michael@0: * List of observers to listen against michael@0: */ michael@0: const kObserverTopics = [ michael@0: "quit-application-requested", michael@0: "offline-requested", michael@0: "last-pb-context-exiting", michael@0: "last-pb-context-exited", michael@0: "sleep_notification", michael@0: "suspend_process_notification", michael@0: "wake_notification", michael@0: "resume_process_notification", michael@0: "network:offline-about-to-go-offline", michael@0: "network:offline-status-changed", michael@0: "xpcom-will-shutdown", michael@0: ]; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadIntegration michael@0: michael@0: /** michael@0: * Provides functions to integrate with the host application, handling for michael@0: * example the global prompts on shutdown. michael@0: */ michael@0: this.DownloadIntegration = { michael@0: // For testing only michael@0: _testMode: false, michael@0: testPromptDownloads: 0, michael@0: dontLoadList: false, michael@0: dontLoadObservers: false, michael@0: dontCheckParentalControls: false, michael@0: shouldBlockInTest: false, michael@0: #ifdef MOZ_URL_CLASSIFIER michael@0: dontCheckApplicationReputation: false, michael@0: #else michael@0: dontCheckApplicationReputation: true, michael@0: #endif michael@0: shouldBlockInTestForApplicationReputation: false, michael@0: dontOpenFileAndFolder: false, michael@0: downloadDoneCalled: false, michael@0: _deferTestOpenFile: null, michael@0: _deferTestShowDir: null, michael@0: _deferTestClearPrivateList: null, michael@0: michael@0: /** michael@0: * Main DownloadStore object for loading and saving the list of persistent michael@0: * downloads, or null if the download list was never requested and thus it michael@0: * doesn't need to be persisted. michael@0: */ michael@0: _store: null, michael@0: michael@0: /** michael@0: * Gets and sets test mode michael@0: */ michael@0: get testMode() this._testMode, michael@0: set testMode(mode) { michael@0: this._downloadsDirectory = null; michael@0: return (this._testMode = mode); michael@0: }, michael@0: michael@0: /** michael@0: * Performs initialization of the list of persistent downloads, before its michael@0: * first use by the host application. This function may be called only once michael@0: * during the entire lifetime of the application. michael@0: * michael@0: * @param aList michael@0: * DownloadList object to be populated with the download objects michael@0: * serialized from the previous session. This list will be persisted michael@0: * to disk during the session lifetime. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the list has been populated. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: initializePublicDownloadList: function(aList) { michael@0: return Task.spawn(function task_DI_initializePublicDownloadList() { michael@0: if (this.dontLoadList) { michael@0: // In tests, only register the history observer. This object is kept michael@0: // alive by the history service, so we don't keep a reference to it. michael@0: new DownloadHistoryObserver(aList); michael@0: return; michael@0: } michael@0: michael@0: if (this._store) { michael@0: throw new Error("initializePublicDownloadList may be called only once."); michael@0: } michael@0: michael@0: this._store = new DownloadStore(aList, OS.Path.join( michael@0: OS.Constants.Path.profileDir, michael@0: "downloads.json")); michael@0: this._store.onsaveitem = this.shouldPersistDownload.bind(this); michael@0: michael@0: if (this._importedFromSqlite) { michael@0: try { michael@0: yield this._store.load(); michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: } else { michael@0: let sqliteDBpath = OS.Path.join(OS.Constants.Path.profileDir, michael@0: "downloads.sqlite"); michael@0: michael@0: if (yield OS.File.exists(sqliteDBpath)) { michael@0: let sqliteImport = new DownloadImport(aList, sqliteDBpath); michael@0: yield sqliteImport.import(); michael@0: michael@0: let importCount = (yield aList.getAll()).length; michael@0: if (importCount > 0) { michael@0: try { michael@0: yield this._store.save(); michael@0: } catch (ex) { } michael@0: } michael@0: michael@0: // No need to wait for the file removal. michael@0: OS.File.remove(sqliteDBpath).then(null, Cu.reportError); michael@0: } michael@0: michael@0: Services.prefs.setBoolPref(kPrefImportedFromSqlite, true); michael@0: michael@0: // Don't even report error here because this file is pre Firefox 3 michael@0: // and most likely doesn't exist. michael@0: OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir, michael@0: "downloads.rdf")); michael@0: michael@0: } michael@0: michael@0: // After the list of persistent downloads has been loaded, add the michael@0: // DownloadAutoSaveView and the DownloadHistoryObserver (even if the load michael@0: // operation failed). These objects are kept alive by the underlying michael@0: // DownloadList and by the history service respectively. We wait for a michael@0: // complete initialization of the view used for detecting changes to michael@0: // downloads to be persisted, before other callers get a chance to modify michael@0: // the list without being detected. michael@0: yield new DownloadAutoSaveView(aList, this._store).initialize(); michael@0: new DownloadHistoryObserver(aList); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: #ifdef MOZ_WIDGET_GONK michael@0: /** michael@0: * Finds the default download directory which can be either in the michael@0: * internal storage or on the sdcard. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The downloads directory string path. michael@0: */ michael@0: _getDefaultDownloadDirectory: function() { michael@0: return Task.spawn(function() { michael@0: let directoryPath; michael@0: let win = Services.wm.getMostRecentWindow("navigator:browser"); michael@0: let storages = win.navigator.getDeviceStorages("sdcard"); michael@0: let preferredStorageName; michael@0: // Use the first one or the default storage. michael@0: storages.forEach((aStorage) => { michael@0: if (aStorage.default || !preferredStorageName) { michael@0: preferredStorageName = aStorage.storageName; michael@0: } michael@0: }); michael@0: michael@0: // Now get the path for this storage area. michael@0: if (preferredStorageName) { michael@0: let volume = volumeService.getVolumeByName(preferredStorageName); michael@0: if (volume && michael@0: volume.isMediaPresent && michael@0: !volume.isMountLocked && michael@0: !volume.isSharing) { michael@0: directoryPath = OS.Path.join(volume.mountPoint, "downloads"); michael@0: yield OS.File.makeDir(directoryPath, { ignoreExisting: true }); michael@0: } michael@0: } michael@0: if (directoryPath) { michael@0: throw new Task.Result(directoryPath); michael@0: } else { michael@0: throw new Components.Exception("No suitable storage for downloads.", michael@0: Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH); michael@0: } michael@0: }); michael@0: }, michael@0: #endif michael@0: michael@0: /** michael@0: * Determines if a Download object from the list of persistent downloads michael@0: * should be saved into a file, so that it can be restored across sessions. michael@0: * michael@0: * This function allows filtering out downloads that the host application is michael@0: * not interested in persisting across sessions, for example downloads that michael@0: * finished successfully. michael@0: * michael@0: * @param aDownload michael@0: * The Download object to be inspected. This is originally taken from michael@0: * the global DownloadList object for downloads that were not started michael@0: * from a private browsing window. The item may have been removed michael@0: * from the list since the save operation started, though in this case michael@0: * the save operation will be repeated later. michael@0: * michael@0: * @return True to save the download, false otherwise. michael@0: */ michael@0: shouldPersistDownload: function (aDownload) michael@0: { michael@0: // In the default implementation, we save all the downloads currently in michael@0: // progress, as well as stopped downloads for which we retained partially michael@0: // downloaded data. Stopped downloads for which we don't need to track the michael@0: // presence of a ".part" file are only retained in the browser history. michael@0: // On b2g, we keep a few days of history. michael@0: #ifdef MOZ_B2G michael@0: let maxTime = Date.now() - michael@0: Services.prefs.getIntPref("dom.downloads.max_retention_days") * 24 * 60 * 60 * 1000; michael@0: return (aDownload.startTime > maxTime) || michael@0: aDownload.hasPartialData || michael@0: !aDownload.stopped; michael@0: #else michael@0: return aDownload.hasPartialData || !aDownload.stopped; michael@0: #endif michael@0: }, michael@0: michael@0: /** michael@0: * Returns the system downloads directory asynchronously. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The downloads directory string path. michael@0: */ michael@0: getSystemDownloadsDirectory: function DI_getSystemDownloadsDirectory() { michael@0: return Task.spawn(function() { michael@0: if (this._downloadsDirectory) { michael@0: // This explicitly makes this function a generator for Task.jsm. We michael@0: // need this because calls to the "yield" operator below may be michael@0: // preprocessed out on some platforms. michael@0: yield undefined; michael@0: throw new Task.Result(this._downloadsDirectory); michael@0: } michael@0: michael@0: let directoryPath = null; michael@0: #ifdef XP_MACOSX michael@0: directoryPath = this._getDirectory("DfltDwnld"); michael@0: #elifdef XP_WIN michael@0: // For XP/2K, use My Documents/Downloads. Other version uses michael@0: // the default Downloads directory. michael@0: let version = parseFloat(Services.sysinfo.getProperty("version")); michael@0: if (version < 6) { michael@0: directoryPath = yield this._createDownloadsDirectory("Pers"); michael@0: } else { michael@0: directoryPath = this._getDirectory("DfltDwnld"); michael@0: } michael@0: #elifdef XP_UNIX michael@0: #ifdef MOZ_WIDGET_ANDROID michael@0: // Android doesn't have a $HOME directory, and by default we only have michael@0: // write access to /data/data/org.mozilla.{$APP} and /sdcard michael@0: directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY"); michael@0: if (!directoryPath) { michael@0: throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.", michael@0: Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH); michael@0: } michael@0: #elifdef MOZ_WIDGET_GONK michael@0: directoryPath = this._getDefaultDownloadDirectory(); michael@0: #else michael@0: // For Linux, use XDG download dir, with a fallback to Home/Downloads michael@0: // if the XDG user dirs are disabled. michael@0: try { michael@0: directoryPath = this._getDirectory("DfltDwnld"); michael@0: } catch(e) { michael@0: directoryPath = yield this._createDownloadsDirectory("Home"); michael@0: } michael@0: #endif michael@0: #else michael@0: directoryPath = yield this._createDownloadsDirectory("Home"); michael@0: #endif michael@0: this._downloadsDirectory = directoryPath; michael@0: throw new Task.Result(this._downloadsDirectory); michael@0: }.bind(this)); michael@0: }, michael@0: _downloadsDirectory: null, michael@0: michael@0: /** michael@0: * Returns the user downloads directory asynchronously. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The downloads directory string path. michael@0: */ michael@0: getPreferredDownloadsDirectory: function DI_getPreferredDownloadsDirectory() { michael@0: return Task.spawn(function() { michael@0: let directoryPath = null; michael@0: #ifdef MOZ_WIDGET_GONK michael@0: directoryPath = this._getDefaultDownloadDirectory(); michael@0: #else michael@0: let prefValue = 1; michael@0: michael@0: try { michael@0: prefValue = Services.prefs.getIntPref("browser.download.folderList"); michael@0: } catch(e) {} michael@0: michael@0: switch(prefValue) { michael@0: case 0: // Desktop michael@0: directoryPath = this._getDirectory("Desk"); michael@0: break; michael@0: case 1: // Downloads michael@0: directoryPath = yield this.getSystemDownloadsDirectory(); michael@0: break; michael@0: case 2: // Custom michael@0: try { michael@0: let directory = Services.prefs.getComplexValue("browser.download.dir", michael@0: Ci.nsIFile); michael@0: directoryPath = directory.path; michael@0: yield OS.File.makeDir(directoryPath, { ignoreExisting: true }); michael@0: } catch(ex) { michael@0: // Either the preference isn't set or the directory cannot be created. michael@0: directoryPath = yield this.getSystemDownloadsDirectory(); michael@0: } michael@0: break; michael@0: default: michael@0: directoryPath = yield this.getSystemDownloadsDirectory(); michael@0: } michael@0: #endif michael@0: throw new Task.Result(directoryPath); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the temporary downloads directory asynchronously. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The downloads directory string path. michael@0: */ michael@0: getTemporaryDownloadsDirectory: function DI_getTemporaryDownloadsDirectory() { michael@0: return Task.spawn(function() { michael@0: let directoryPath = null; michael@0: #ifdef XP_MACOSX michael@0: directoryPath = yield this.getPreferredDownloadsDirectory(); michael@0: #elifdef MOZ_WIDGET_ANDROID michael@0: directoryPath = yield this.getSystemDownloadsDirectory(); michael@0: #elifdef MOZ_WIDGET_GONK michael@0: directoryPath = yield this.getSystemDownloadsDirectory(); michael@0: #else michael@0: // For Metro mode on Windows 8, we want searchability for documents michael@0: // that the user chose to open with an external application. michael@0: if (Services.metro && Services.metro.immersive) { michael@0: directoryPath = yield this.getSystemDownloadsDirectory(); michael@0: } else { michael@0: directoryPath = this._getDirectory("TmpD"); michael@0: } michael@0: #endif michael@0: throw new Task.Result(directoryPath); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Checks to determine whether to block downloads for parental controls. michael@0: * michael@0: * aParam aDownload michael@0: * The download object. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The boolean indicates to block downloads or not. michael@0: */ michael@0: shouldBlockForParentalControls: function DI_shouldBlockForParentalControls(aDownload) { michael@0: if (this.dontCheckParentalControls) { michael@0: return Promise.resolve(this.shouldBlockInTest); michael@0: } michael@0: michael@0: let isEnabled = gParentalControlsService && michael@0: gParentalControlsService.parentalControlsEnabled; michael@0: let shouldBlock = isEnabled && michael@0: gParentalControlsService.blockFileDownloadsEnabled; michael@0: michael@0: // Log the event if required by parental controls settings. michael@0: if (isEnabled && gParentalControlsService.loggingEnabled) { michael@0: gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload, michael@0: shouldBlock, michael@0: NetUtil.newURI(aDownload.source.url), null); michael@0: } michael@0: michael@0: return Promise.resolve(shouldBlock); michael@0: }, michael@0: michael@0: /** michael@0: * Checks to determine whether to block downloads because they might be michael@0: * malware, based on application reputation checks. michael@0: * michael@0: * aParam aDownload michael@0: * The download object. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The boolean indicates to block downloads or not. michael@0: */ michael@0: shouldBlockForReputationCheck: function (aDownload) { michael@0: if (this.dontCheckApplicationReputation) { michael@0: return Promise.resolve(this.shouldBlockInTestForApplicationReputation); michael@0: } michael@0: let hash; michael@0: let sigInfo; michael@0: try { michael@0: hash = aDownload.saver.getSha256Hash(); michael@0: sigInfo = aDownload.saver.getSignatureInfo(); michael@0: } catch (ex) { michael@0: // Bail if DownloadSaver doesn't have a hash. michael@0: return Promise.resolve(false); michael@0: } michael@0: if (!hash || !sigInfo) { michael@0: return Promise.resolve(false); michael@0: } michael@0: let deferred = Promise.defer(); michael@0: let aReferrer = null; michael@0: if (aDownload.source.referrer) { michael@0: aReferrer: NetUtil.newURI(aDownload.source.referrer); michael@0: } michael@0: gApplicationReputationService.queryReputation({ michael@0: sourceURI: NetUtil.newURI(aDownload.source.url), michael@0: referrerURI: aReferrer, michael@0: fileSize: aDownload.currentBytes, michael@0: sha256Hash: hash, michael@0: signatureInfo: sigInfo }, michael@0: function onComplete(aShouldBlock, aRv) { michael@0: deferred.resolve(aShouldBlock); michael@0: }); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: #ifdef XP_WIN michael@0: /** michael@0: * Checks whether downloaded files should be marked as coming from michael@0: * Internet Zone. michael@0: * michael@0: * @return true if files should be marked michael@0: */ michael@0: _shouldSaveZoneInformation: function() { michael@0: let key = Cc["@mozilla.org/windows-registry-key;1"] michael@0: .createInstance(Ci.nsIWindowsRegKey); michael@0: try { michael@0: key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, michael@0: "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments", michael@0: Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE); michael@0: try { michael@0: return key.readIntValue("SaveZoneInformation") != 1; michael@0: } finally { michael@0: key.close(); michael@0: } michael@0: } catch (ex) { michael@0: // If the key is not present, files should be marked by default. michael@0: return true; michael@0: } michael@0: }, michael@0: #endif michael@0: michael@0: /** michael@0: * Performs platform-specific operations when a download is done. michael@0: * michael@0: * aParam aDownload michael@0: * The Download object. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When all the operations completed successfully. michael@0: * @rejects JavaScript exception if any of the operations failed. michael@0: */ michael@0: downloadDone: function(aDownload) { michael@0: return Task.spawn(function () { michael@0: #ifdef XP_WIN michael@0: // On Windows, we mark any file saved to the NTFS file system as coming michael@0: // from the Internet security zone unless Group Policy disables the michael@0: // feature. We do this by writing to the "Zone.Identifier" Alternate michael@0: // Data Stream directly, because the Save method of the michael@0: // IAttachmentExecute interface would trigger operations that may cause michael@0: // the application to hang, or other performance issues. michael@0: // The stream created in this way is forward-compatible with all the michael@0: // current and future versions of Windows. michael@0: if (this._shouldSaveZoneInformation()) { michael@0: let zone; michael@0: try { michael@0: zone = gDownloadPlatform.mapUrlToZone(aDownload.source.url); michael@0: } catch (e) { michael@0: // Default to Internet Zone if mapUrlToZone failed for michael@0: // whatever reason. michael@0: zone = Ci.mozIDownloadPlatform.ZONE_INTERNET; michael@0: } michael@0: try { michael@0: // Don't write zone IDs for Local, Intranet, or Trusted sites michael@0: // to match Windows behavior. michael@0: if (zone >= Ci.mozIDownloadPlatform.ZONE_INTERNET) { michael@0: let streamPath = aDownload.target.path + ":Zone.Identifier"; michael@0: let stream = yield OS.File.open(streamPath, { create: true }); michael@0: try { michael@0: yield stream.write(new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=" + zone + "\r\n")); michael@0: } finally { michael@0: yield stream.close(); michael@0: } michael@0: } michael@0: } catch (ex) { michael@0: // If writing to the stream fails, we ignore the error and continue. michael@0: // The Windows API error 123 (ERROR_INVALID_NAME) is expected to michael@0: // occur when working on a file system that does not support michael@0: // Alternate Data Streams, like FAT32, thus we don't report this michael@0: // specific error. michael@0: if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) { michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: // Now that the file is completely downloaded, mark it michael@0: // accessible by other users on this system, if the user's michael@0: // global preferences so indicate. (On Unix, this applies the michael@0: // umask. On Windows, currently does nothing.) michael@0: // Errors should be reported, but are not fatal. michael@0: try { michael@0: yield OS.File.setPermissions(aDownload.target.path); michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: michael@0: gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url), michael@0: new FileUtils.File(aDownload.target.path), michael@0: aDownload.contentType, michael@0: aDownload.source.isPrivate); michael@0: this.downloadDoneCalled = true; michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /* michael@0: * Launches a file represented by the target of a download. This can michael@0: * open the file with the default application for the target MIME type michael@0: * or file extension, or with a custom application if michael@0: * aDownload.launcherPath is set. michael@0: * michael@0: * @param aDownload michael@0: * A Download object that contains the necessary information michael@0: * to launch the file. The relevant properties are: the target michael@0: * file, the contentType and the custom application chosen michael@0: * to launch it. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the instruction to launch the file has been michael@0: * successfully given to the operating system. Note that michael@0: * the OS might still take a while until the file is actually michael@0: * launched. michael@0: * @rejects JavaScript exception if there was an error trying to launch michael@0: * the file. michael@0: */ michael@0: launchDownload: function (aDownload) { michael@0: let deferred = Task.spawn(function DI_launchDownload_task() { michael@0: let file = new FileUtils.File(aDownload.target.path); michael@0: michael@0: #ifndef XP_WIN michael@0: // Ask for confirmation if the file is executable, except on Windows where michael@0: // the operating system will show the prompt based on the security zone. michael@0: // We do this here, instead of letting the caller handle the prompt michael@0: // separately in the user interface layer, for two reasons. The first is michael@0: // because of its security nature, so that add-ons cannot forget to do michael@0: // this check. The second is that the system-level security prompt would michael@0: // be displayed at launch time in any case. michael@0: if (file.isExecutable() && !this.dontOpenFileAndFolder) { michael@0: // We don't anchor the prompt to a specific window intentionally, not michael@0: // only because this is the same behavior as the system-level prompt, michael@0: // but also because the most recently active window is the right choice michael@0: // in basically all cases. michael@0: let shouldLaunch = yield DownloadUIHelper.getPrompter() michael@0: .confirmLaunchExecutable(file.path); michael@0: if (!shouldLaunch) { michael@0: return; michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: // In case of a double extension, like ".tar.gz", we only michael@0: // consider the last one, because the MIME service cannot michael@0: // handle multiple extensions. michael@0: let fileExtension = null, mimeInfo = null; michael@0: let match = file.leafName.match(/\.([^.]+)$/); michael@0: if (match) { michael@0: fileExtension = match[1]; michael@0: } michael@0: michael@0: try { michael@0: // The MIME service might throw if contentType == "" and it can't find michael@0: // a MIME type for the given extension, so we'll treat this case as michael@0: // an unknown mimetype. michael@0: mimeInfo = gMIMEService.getFromTypeAndExtension(aDownload.contentType, michael@0: fileExtension); michael@0: } catch (e) { } michael@0: michael@0: if (aDownload.launcherPath) { michael@0: if (!mimeInfo) { michael@0: // This should not happen on normal circumstances because launcherPath michael@0: // is only set when we had an instance of nsIMIMEInfo to retrieve michael@0: // the custom application chosen by the user. michael@0: throw new Error( michael@0: "Unable to create nsIMIMEInfo to launch a custom application"); michael@0: } michael@0: michael@0: // Custom application chosen michael@0: let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"] michael@0: .createInstance(Ci.nsILocalHandlerApp); michael@0: localHandlerApp.executable = new FileUtils.File(aDownload.launcherPath); michael@0: michael@0: mimeInfo.preferredApplicationHandler = localHandlerApp; michael@0: mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp; michael@0: michael@0: // In test mode, allow the test to verify the nsIMIMEInfo instance. michael@0: if (this.dontOpenFileAndFolder) { michael@0: throw new Task.Result(mimeInfo); michael@0: } michael@0: michael@0: mimeInfo.launchWithFile(file); michael@0: return; michael@0: } michael@0: michael@0: // No custom application chosen, let's launch the file with the default michael@0: // handler. In test mode, we indicate this with a null value. michael@0: if (this.dontOpenFileAndFolder) { michael@0: throw new Task.Result(null); michael@0: } michael@0: michael@0: // First let's try to launch it through the MIME service application michael@0: // handler michael@0: if (mimeInfo) { michael@0: mimeInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault; michael@0: michael@0: try { michael@0: mimeInfo.launchWithFile(file); michael@0: return; michael@0: } catch (ex) { } michael@0: } michael@0: michael@0: // If it didn't work or if there was no MIME info available, michael@0: // let's try to directly launch the file. michael@0: try { michael@0: file.launch(); michael@0: return; michael@0: } catch (ex) { } michael@0: michael@0: // If our previous attempts failed, try sending it through michael@0: // the system's external "file:" URL handler. michael@0: gExternalProtocolService.loadUrl(NetUtil.newURI(file)); michael@0: yield undefined; michael@0: }.bind(this)); michael@0: michael@0: if (this.dontOpenFileAndFolder) { michael@0: deferred.then((value) => { this._deferTestOpenFile.resolve(value); }, michael@0: (error) => { this._deferTestOpenFile.reject(error); }); michael@0: } michael@0: michael@0: return deferred; michael@0: }, michael@0: michael@0: /* michael@0: * Shows the containing folder of a file. michael@0: * michael@0: * @param aFilePath michael@0: * The path to the file. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the instruction to open the containing folder has been michael@0: * successfully given to the operating system. Note that michael@0: * the OS might still take a while until the folder is actually michael@0: * opened. michael@0: * @rejects JavaScript exception if there was an error trying to open michael@0: * the containing folder. michael@0: */ michael@0: showContainingDirectory: function (aFilePath) { michael@0: let deferred = Task.spawn(function DI_showContainingDirectory_task() { michael@0: let file = new FileUtils.File(aFilePath); michael@0: michael@0: if (this.dontOpenFileAndFolder) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: // Show the directory containing the file and select the file. michael@0: file.reveal(); michael@0: return; michael@0: } catch (ex) { } michael@0: michael@0: // If reveal fails for some reason (e.g., it's not implemented on unix michael@0: // or the file doesn't exist), try using the parent if we have it. michael@0: let parent = file.parent; michael@0: if (!parent) { michael@0: throw new Error( michael@0: "Unexpected reference to a top-level directory instead of a file"); michael@0: } michael@0: michael@0: try { michael@0: // Open the parent directory to show where the file should be. michael@0: parent.launch(); michael@0: return; michael@0: } catch (ex) { } michael@0: michael@0: // If launch also fails (probably because it's not implemented), let michael@0: // the OS handler try to open the parent. michael@0: gExternalProtocolService.loadUrl(NetUtil.newURI(parent)); michael@0: yield undefined; michael@0: }.bind(this)); michael@0: michael@0: if (this.dontOpenFileAndFolder) { michael@0: deferred.then((value) => { this._deferTestShowDir.resolve("success"); }, michael@0: (error) => { michael@0: // Ensure that _deferTestShowDir has at least one consumer michael@0: // for the error, otherwise the error will be reported as michael@0: // uncaught. michael@0: this._deferTestShowDir.promise.then(null, function() {}); michael@0: this._deferTestShowDir.reject(error); michael@0: }); michael@0: } michael@0: michael@0: return deferred; michael@0: }, michael@0: michael@0: /** michael@0: * Calls the directory service, create a downloads directory and returns an michael@0: * nsIFile for the downloads directory. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The directory string path. michael@0: */ michael@0: _createDownloadsDirectory: function DI_createDownloadsDirectory(aName) { michael@0: // We read the name of the directory from the list of translated strings michael@0: // that is kept by the UI helper module, even if this string is not strictly michael@0: // displayed in the user interface. michael@0: let directoryPath = OS.Path.join(this._getDirectory(aName), michael@0: DownloadUIHelper.strings.downloadsFolder); michael@0: michael@0: // Create the Downloads folder and ignore if it already exists. michael@0: return OS.File.makeDir(directoryPath, { ignoreExisting: true }). michael@0: then(function() { michael@0: return directoryPath; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Calls the directory service and returns an nsIFile for the requested michael@0: * location name. michael@0: * michael@0: * @return The directory string path. michael@0: */ michael@0: _getDirectory: function DI_getDirectory(aName) { michael@0: return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile).path; michael@0: }, michael@0: michael@0: /** michael@0: * Register the downloads interruption observers. michael@0: * michael@0: * @param aList michael@0: * The public or private downloads list. michael@0: * @param aIsPrivate michael@0: * True if the list is private, false otherwise. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the views and observers are added. michael@0: */ michael@0: addListObservers: function DI_addListObservers(aList, aIsPrivate) { michael@0: if (this.dontLoadObservers) { michael@0: return Promise.resolve(); michael@0: } michael@0: michael@0: DownloadObserver.registerView(aList, aIsPrivate); michael@0: if (!DownloadObserver.observersAdded) { michael@0: DownloadObserver.observersAdded = true; michael@0: for (let topic of kObserverTopics) { michael@0: Services.obs.addObserver(DownloadObserver, topic, false); michael@0: } michael@0: } michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Checks if we have already imported (or attempted to import) michael@0: * the downloads database from the previous SQLite storage. michael@0: * michael@0: * @return boolean True if we the previous DB was imported. michael@0: */ michael@0: get _importedFromSqlite() { michael@0: try { michael@0: return Services.prefs.getBoolPref(kPrefImportedFromSqlite); michael@0: } catch (ex) { michael@0: return false; michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadObserver michael@0: michael@0: this.DownloadObserver = { michael@0: /** michael@0: * Flag to determine if the observers have been added previously. michael@0: */ michael@0: observersAdded: false, michael@0: michael@0: /** michael@0: * Timer used to delay restarting canceled downloads upon waking and returning michael@0: * online. michael@0: */ michael@0: _wakeTimer: null, michael@0: michael@0: /** michael@0: * Set that contains the in progress publics downloads. michael@0: * It's kept updated when a public download is added, removed or changes its michael@0: * properties. michael@0: */ michael@0: _publicInProgressDownloads: new Set(), michael@0: michael@0: /** michael@0: * Set that contains the in progress private downloads. michael@0: * It's kept updated when a private download is added, removed or changes its michael@0: * properties. michael@0: */ michael@0: _privateInProgressDownloads: new Set(), michael@0: michael@0: /** michael@0: * Set that contains the downloads that have been canceled when going offline michael@0: * or to sleep. These are started again when returning online or waking. This michael@0: * list is not persisted so when exiting and restarting, the downloads will not michael@0: * be started again. michael@0: */ michael@0: _canceledOfflineDownloads: new Set(), michael@0: michael@0: /** michael@0: * Registers a view that updates the corresponding downloads state set, based michael@0: * on the aIsPrivate argument. The set is updated when a download is added, michael@0: * removed or changes its properties. michael@0: * michael@0: * @param aList michael@0: * The public or private downloads list. michael@0: * @param aIsPrivate michael@0: * True if the list is private, false otherwise. michael@0: */ michael@0: registerView: function DO_registerView(aList, aIsPrivate) { michael@0: let downloadsSet = aIsPrivate ? this._privateInProgressDownloads michael@0: : this._publicInProgressDownloads; michael@0: let downloadsView = { michael@0: onDownloadAdded: aDownload => { michael@0: if (!aDownload.stopped) { michael@0: downloadsSet.add(aDownload); michael@0: } michael@0: }, michael@0: onDownloadChanged: aDownload => { michael@0: if (aDownload.stopped) { michael@0: downloadsSet.delete(aDownload); michael@0: } else { michael@0: downloadsSet.add(aDownload); michael@0: } michael@0: }, michael@0: onDownloadRemoved: aDownload => { michael@0: downloadsSet.delete(aDownload); michael@0: // The download must also be removed from the canceled when offline set. michael@0: this._canceledOfflineDownloads.delete(aDownload); michael@0: } michael@0: }; michael@0: michael@0: // We register the view asynchronously. michael@0: aList.addView(downloadsView).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: /** michael@0: * Wrapper that handles the test mode before calling the prompt that display michael@0: * a warning message box that informs that there are active downloads, michael@0: * and asks whether the user wants to cancel them or not. michael@0: * michael@0: * @param aCancel michael@0: * The observer notification subject. michael@0: * @param aDownloadsCount michael@0: * The current downloads count. michael@0: * @param aPrompter michael@0: * The prompter object that shows the confirm dialog. michael@0: * @param aPromptType michael@0: * The type of prompt notification depending on the observer. michael@0: */ michael@0: _confirmCancelDownloads: function DO_confirmCancelDownload( michael@0: aCancel, aDownloadsCount, aPrompter, aPromptType) { michael@0: // If user has already dismissed the request, then do nothing. michael@0: if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) { michael@0: return; michael@0: } michael@0: // Handle test mode michael@0: if (DownloadIntegration.testMode) { michael@0: DownloadIntegration.testPromptDownloads = aDownloadsCount; michael@0: return; michael@0: } michael@0: michael@0: aCancel.data = aPrompter.confirmCancelDownloads(aDownloadsCount, aPromptType); michael@0: }, michael@0: michael@0: /** michael@0: * Resume all downloads that were paused when going offline, used when waking michael@0: * from sleep or returning from being offline. michael@0: */ michael@0: _resumeOfflineDownloads: function DO_resumeOfflineDownloads() { michael@0: this._wakeTimer = null; michael@0: michael@0: for (let download of this._canceledOfflineDownloads) { michael@0: download.start(); michael@0: } michael@0: }, michael@0: michael@0: //////////////////////////////////////////////////////////////////////////// michael@0: //// nsIObserver michael@0: michael@0: observe: function DO_observe(aSubject, aTopic, aData) { michael@0: let downloadsCount; michael@0: let p = DownloadUIHelper.getPrompter(); michael@0: switch (aTopic) { michael@0: case "quit-application-requested": michael@0: downloadsCount = this._publicInProgressDownloads.size + michael@0: this._privateInProgressDownloads.size; michael@0: this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_QUIT); michael@0: break; michael@0: case "offline-requested": michael@0: downloadsCount = this._publicInProgressDownloads.size + michael@0: this._privateInProgressDownloads.size; michael@0: this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_OFFLINE); michael@0: break; michael@0: case "last-pb-context-exiting": michael@0: downloadsCount = this._privateInProgressDownloads.size; michael@0: this._confirmCancelDownloads(aSubject, downloadsCount, p, michael@0: p.ON_LEAVE_PRIVATE_BROWSING); michael@0: break; michael@0: case "last-pb-context-exited": michael@0: let deferred = Task.spawn(function() { michael@0: let list = yield Downloads.getList(Downloads.PRIVATE); michael@0: let downloads = yield list.getAll(); michael@0: michael@0: // We can remove the downloads and finalize them in parallel. michael@0: for (let download of downloads) { michael@0: list.remove(download).then(null, Cu.reportError); michael@0: download.finalize(true).then(null, Cu.reportError); michael@0: } michael@0: }); michael@0: // Handle test mode michael@0: if (DownloadIntegration.testMode) { michael@0: deferred.then((value) => { DownloadIntegration._deferTestClearPrivateList.resolve("success"); }, michael@0: (error) => { DownloadIntegration._deferTestClearPrivateList.reject(error); }); michael@0: } michael@0: break; michael@0: case "sleep_notification": michael@0: case "suspend_process_notification": michael@0: case "network:offline-about-to-go-offline": michael@0: for (let download of this._publicInProgressDownloads) { michael@0: download.cancel(); michael@0: this._canceledOfflineDownloads.add(download); michael@0: } michael@0: for (let download of this._privateInProgressDownloads) { michael@0: download.cancel(); michael@0: this._canceledOfflineDownloads.add(download); michael@0: } michael@0: break; michael@0: case "wake_notification": michael@0: case "resume_process_notification": michael@0: let wakeDelay = 10000; michael@0: try { michael@0: wakeDelay = Services.prefs.getIntPref("browser.download.manager.resumeOnWakeDelay"); michael@0: } catch(e) {} michael@0: michael@0: if (wakeDelay >= 0) { michael@0: this._wakeTimer = new Timer(this._resumeOfflineDownloads.bind(this), wakeDelay, michael@0: Ci.nsITimer.TYPE_ONE_SHOT); michael@0: } michael@0: break; michael@0: case "network:offline-status-changed": michael@0: if (aData == "online") { michael@0: this._resumeOfflineDownloads(); michael@0: } michael@0: break; michael@0: // We need to unregister observers explicitly before we reach the michael@0: // "xpcom-shutdown" phase, otherwise observers may be notified when some michael@0: // required services are not available anymore. We can't unregister michael@0: // observers on "quit-application", because this module is also loaded michael@0: // during "make package" automation, and the quit notification is not sent michael@0: // in that execution environment (bug 973637). michael@0: case "xpcom-will-shutdown": michael@0: for (let topic of kObserverTopics) { michael@0: Services.obs.removeObserver(this, topic); michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: //////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadHistoryObserver michael@0: michael@0: #ifdef MOZ_PLACES michael@0: /** michael@0: * Registers a Places observer so that operations on download history are michael@0: * reflected on the provided list of downloads. michael@0: * michael@0: * You do not need to keep a reference to this object in order to keep it alive, michael@0: * because the history service already keeps a strong reference to it. michael@0: * michael@0: * @param aList michael@0: * DownloadList object linked to this observer. michael@0: */ michael@0: this.DownloadHistoryObserver = function (aList) michael@0: { michael@0: this._list = aList; michael@0: PlacesUtils.history.addObserver(this, false); michael@0: } michael@0: michael@0: this.DownloadHistoryObserver.prototype = { michael@0: /** michael@0: * DownloadList object linked to this observer. michael@0: */ michael@0: _list: null, michael@0: michael@0: //////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]), michael@0: michael@0: //////////////////////////////////////////////////////////////////////////// michael@0: //// nsINavHistoryObserver michael@0: michael@0: onDeleteURI: function DL_onDeleteURI(aURI, aGUID) { michael@0: this._list.removeFinished(download => aURI.equals(NetUtil.newURI( michael@0: download.source.url))); michael@0: }, michael@0: michael@0: onClearHistory: function DL_onClearHistory() { michael@0: this._list.removeFinished(); michael@0: }, michael@0: michael@0: onTitleChanged: function () {}, michael@0: onBeginUpdateBatch: function () {}, michael@0: onEndUpdateBatch: function () {}, michael@0: onVisit: function () {}, michael@0: onPageChanged: function () {}, michael@0: onDeleteVisits: function () {}, michael@0: }; michael@0: #else michael@0: /** michael@0: * Empty implementation when we have no Places support, for example on B2G. michael@0: */ michael@0: this.DownloadHistoryObserver = function (aList) {} michael@0: #endif michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadAutoSaveView michael@0: michael@0: /** michael@0: * This view can be added to a DownloadList object to trigger a save operation michael@0: * in the given DownloadStore object when a relevant change occurs. You should michael@0: * call the "initialize" method in order to register the view and load the michael@0: * current state from disk. michael@0: * michael@0: * You do not need to keep a reference to this object in order to keep it alive, michael@0: * because the DownloadList object already keeps a strong reference to it. michael@0: * michael@0: * @param aList michael@0: * The DownloadList object on which the view should be registered. michael@0: * @param aStore michael@0: * The DownloadStore object used for saving. michael@0: */ michael@0: this.DownloadAutoSaveView = function (aList, aStore) michael@0: { michael@0: this._list = aList; michael@0: this._store = aStore; michael@0: this._downloadsMap = new Map(); michael@0: this._writer = new DeferredTask(() => this._store.save(), kSaveDelayMs); michael@0: } michael@0: michael@0: this.DownloadAutoSaveView.prototype = { michael@0: /** michael@0: * DownloadList object linked to this view. michael@0: */ michael@0: _list: null, michael@0: michael@0: /** michael@0: * The DownloadStore object used for saving. michael@0: */ michael@0: _store: null, michael@0: michael@0: /** michael@0: * True when the initial state of the downloads has been loaded. michael@0: */ michael@0: _initialized: false, michael@0: michael@0: /** michael@0: * Registers the view and loads the current state from disk. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the view has been registered. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: initialize: function () michael@0: { michael@0: // We set _initialized to true after adding the view, so that michael@0: // onDownloadAdded doesn't cause a save to occur. michael@0: return this._list.addView(this).then(() => this._initialized = true); michael@0: }, michael@0: michael@0: /** michael@0: * This map contains only Download objects that should be saved to disk, and michael@0: * associates them with the result of their getSerializationHash function, for michael@0: * the purpose of detecting changes to the relevant properties. michael@0: */ michael@0: _downloadsMap: null, michael@0: michael@0: /** michael@0: * DeferredTask for the save operation. michael@0: */ michael@0: _writer: null, michael@0: michael@0: /** michael@0: * Called when the list of downloads changed, this triggers the asynchronous michael@0: * serialization of the list of downloads. michael@0: */ michael@0: saveSoon: function () michael@0: { michael@0: this._writer.arm(); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadList view michael@0: michael@0: onDownloadAdded: function (aDownload) michael@0: { michael@0: if (DownloadIntegration.shouldPersistDownload(aDownload)) { michael@0: this._downloadsMap.set(aDownload, aDownload.getSerializationHash()); michael@0: if (this._initialized) { michael@0: this.saveSoon(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: onDownloadChanged: function (aDownload) michael@0: { michael@0: if (!DownloadIntegration.shouldPersistDownload(aDownload)) { michael@0: if (this._downloadsMap.has(aDownload)) { michael@0: this._downloadsMap.delete(aDownload); michael@0: this.saveSoon(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let hash = aDownload.getSerializationHash(); michael@0: if (this._downloadsMap.get(aDownload) != hash) { michael@0: this._downloadsMap.set(aDownload, hash); michael@0: this.saveSoon(); michael@0: } michael@0: }, michael@0: michael@0: onDownloadRemoved: function (aDownload) michael@0: { michael@0: if (this._downloadsMap.has(aDownload)) { michael@0: this._downloadsMap.delete(aDownload); michael@0: this.saveSoon(); michael@0: } michael@0: }, michael@0: };