toolkit/components/jsdownloads/src/DownloadIntegration.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

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

mercurial