Bug 888915 - Part 4. Fix remaining Download Manager problems. r=IanN
authorFrank-Rainer Grahl <frgrahl@gmx.net>
Mon, 09 Jul 2018 17:00:21 +0200
changeset 64791 4d63eb6735dfa4402ed7fdbf78b90164119e156f
parent 64790 91578f646104a7c409bcf06fa92e6dfa1df78188
child 64792 6a6f9c616b1cf7686825d3bd61cc46619e185791
push id6502
push usermozilla@jorgk.com
push dateWed, 11 Jul 2018 13:18:44 +0000
treeherdertry-comm-central@623e5cfcc5de [default view] [failures only]
reviewersIanN
bugs888915
Bug 888915 - Part 4. Fix remaining Download Manager problems. r=IanN
suite/components/downloads/DownloadsCommon.jsm
suite/components/downloads/content/downloadmanager.js
suite/components/downloads/content/downloadmanager.xul
suite/components/downloads/content/progressDialog.js
suite/components/downloads/content/treeView.js
suite/components/downloads/content/uploadProgress.js
suite/locales/en-US/chrome/common/downloads/downloadmanager.dtd
suite/locales/en-US/chrome/common/downloads/downloadmanager.properties
--- a/suite/components/downloads/DownloadsCommon.jsm
+++ b/suite/components/downloads/DownloadsCommon.jsm
@@ -18,49 +18,61 @@ this.EXPORTED_SYMBOLS = [
  * DownloadsCommon
  * This object is exposed directly to the consumers of this JavaScript module,
  * and provides shared methods for all the instances of the user interface.
  *
  * DownloadsData
  * Retrieves the list of past and completed downloads from the underlying
  * Downloads API data, and provides asynchronous notifications allowing
  * to build a consistent view of the available data.
- *
  */
 
 // Globals
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+                                  "resource://gre/modules/PluralForm.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadHistory",
                                   "resource://gre/modules/DownloadHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
                                   "resource://gre/modules/DownloadUIHelper.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
                                   "resource://gre/modules/DownloadUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
-                                  "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "DownloadsLogger", () => {
   let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {});
   let consoleOptions = {
     maxLogLevelPref: "browser.download.loglevel",
     prefix: "Downloads"
   };
   return new ConsoleAPI(consoleOptions);
 });
 
+const kDownloadsStringBundleUrl =
+  "chrome://communicator/locale/downloads/downloadmanager.properties";
+
+// Currently not used. Keep for future updates.
+const kDownloadsStringsRequiringFormatting = {
+  fileExecutableSecurityWarning: true
+};
+
+// Currently not used. Keep for future updates.
+const kDownloadsStringsRequiringPluralForm = {
+  otherDownloads3: true
+};
+
 const kPartialDownloadSuffix = ".part";
 
 const kPrefBranch = Services.prefs.getBranch("browser.download.");
 
 const PREF_DM_BEHAVIOR = "browser.download.manager.behavior";
 
 var PrefObserver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
@@ -118,16 +130,68 @@ this.DownloadsCommon = {
 
   // The following are the possible values of the "attention" property.
   ATTENTION_NONE: "",
   ATTENTION_SUCCESS: "success",
   ATTENTION_WARNING: "warning",
   ATTENTION_SEVERE: "severe",
 
   /**
+   * Returns an object whose keys are the string names from the downloads string
+   * bundle, and whose values are either the translated strings or functions
+   * returning formatted strings.
+   */
+  get strings() {
+    let strings = {};
+    let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
+    let enumerator = sb.getSimpleEnumeration();
+    while (enumerator.hasMoreElements()) {
+      let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+      let stringName = string.key;
+      if (stringName in kDownloadsStringsRequiringFormatting) {
+        strings[stringName] = function() {
+          // Convert "arguments" to a real array before calling into XPCOM.
+          return sb.formatStringFromName(stringName,
+                                         Array.slice(arguments, 0),
+                                         arguments.length);
+        };
+      } else if (stringName in kDownloadsStringsRequiringPluralForm) {
+        strings[stringName] = function(aCount) {
+          // Convert "arguments" to a real array before calling into XPCOM.
+          let formattedString = sb.formatStringFromName(stringName,
+                                         Array.slice(arguments, 0),
+                                         arguments.length);
+          return PluralForm.get(aCount, formattedString);
+        };
+      } else {
+        strings[stringName] = string.value;
+      }
+    }
+    delete this.strings;
+    return this.strings = strings;
+  },
+
+  /**
+   * Get access to one of the DownloadsData or HistoryDownloadsData objects
+   * depending on whether history downloads should be included.
+   *
+   * @param window
+   *        The browser window which owns the download button.
+   * @param [optional] history
+   *        True to include history downloads when the window is public.
+   */
+   // does not apply in SM
+  getData(window, history = false) {
+    if (history) {
+      return HistoryDownloadsData;
+    }
+    return DownloadsData;
+  },
+
+  /**
    * Initializes the Downloads Manager common code.
    */
   init() {
     Cu.import("resource://gre/modules/Downloads.jsm");
     Cu.import("resource://gre/modules/DownloadIntegration.jsm");
     DownloadIntegration.shouldPersistDownload = function() { return true; }
     DownloadsData.initializeDataLink();
   },
@@ -155,16 +219,284 @@ this.DownloadsCommon = {
     if (download.canceled) {
       if (download.hasPartialData) {
         return DownloadsCommon.DOWNLOAD_PAUSED;
       }
       return DownloadsCommon.DOWNLOAD_CANCELED;
     }
     return DownloadsCommon.DOWNLOAD_NOTSTARTED;
   },
+
+  /**
+   * Returns the state as a string for the provided Download object.
+   */
+  stateOfDownloadText(download) {
+    // Don't duplicate the logic so just call stateOfDownload.
+    let state = this.stateOfDownload(download);
+    let s = DownloadsCommon.strings;
+    let title = s.unblockHeaderUnblock;
+    let verboseState;
+
+    switch (state) {
+      case DownloadsCommon.DOWNLOAD_PAUSED:
+        verboseState = s.statePaused;
+        break;
+      case DownloadsCommon.DOWNLOAD_DOWNLOADING:
+        verboseState = s.stateDownloading;
+        break;
+      case DownloadsCommon.DOWNLOAD_FINISHED:
+        verboseState = s.stateCompleted;
+        break;
+      case DownloadsCommon.DOWNLOAD_FAILED:
+        verboseState = s.stateFailed;
+        break;
+      case DownloadsCommon.DOWNLOAD_CANCELED:
+        verboseState = s.stateCanceled;
+        break;
+      // Security Zone Policy
+      case DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL:
+      // Security Zone Policy
+        verboseState = s.stateBlockedParentalControls;
+        break;
+      // Security Zone Policy
+      case DownloadsCommon.DOWNLOAD_BLOCKED_POLICY:
+        verboseState = s.stateBlockedPolicy;
+        break;
+      // possible virus/spyware
+      case DownloadsCommon.DOWNLOAD_DIRTY:
+        verboseState = s.stateDirty;
+        break;
+      // Currently not returned.
+      case DownloadsCommon.DOWNLOAD_UPLOADING:
+        verboseState = s.stateNotStarted;
+        break;
+      case DownloadsCommon.DOWNLOAD_NOTSTARTED:
+        verboseState = s.stateNotStarted;
+        break;
+      // Whoops!
+      default:
+        verboseState = s.stateUnknown;
+        break;
+    }
+
+    return verboseState;
+  },
+
+  /**
+   * Opens a downloaded file.
+   *
+   * @param aFile
+   *        the downloaded file to be opened.
+   * @param aMimeInfo
+   *        the mime type info object.  May be null.
+   * @param aOwnerWindow
+   *        the window with which this action is associated.
+   */
+  openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
+    if (!(aFile instanceof Ci.nsIFile)) {
+      throw new Error("aFile must be a nsIFile object");
+    }
+    if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) {
+      throw new Error("Invalid value passed for aMimeInfo");
+    }
+    if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) {
+      throw new Error("aOwnerWindow must be a dom-window object");
+    }
+
+    let isWindowsExe = AppConstants.platform == "win" &&
+      aFile.leafName.toLowerCase().endsWith(".exe");
+
+    let promiseShouldLaunch;
+    // Don't prompt on Windows for .exe since there will be a native prompt.
+    if (aFile.isExecutable() && !isWindowsExe) {
+      // We get a prompter for the provided window here, even though anchoring
+      // to the most recently active window should work as well.
+      promiseShouldLaunch =
+        DownloadUIHelper.getPrompter(aOwnerWindow)
+                        .confirmLaunchExecutable(aFile.path);
+    } else {
+      promiseShouldLaunch = Promise.resolve(true);
+    }
+
+    promiseShouldLaunch.then(shouldLaunch => {
+      if (!shouldLaunch) {
+        return;
+      }
+
+      // Actually open the file.
+      try {
+        if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
+          aMimeInfo.launchWithFile(aFile);
+          return;
+        }
+      } catch (ex) { }
+
+      // If either we don't have the mime info, or the preferred action failed,
+      // attempt to launch the file directly.
+      try {
+        aFile.launch();
+      } catch (ex) {
+        // If launch fails, try sending it through the system's external "file:"
+        // URL handler.
+        Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+          .getService(Ci.nsIExternalProtocolService)
+          .loadUrl(NetUtil.newURI(aFile));
+      }
+    }).catch(Cu.reportError);
+  },
+
+  /**
+   * Show a downloaded file in the system file manager.
+   *
+   * @param aFile
+   *        a downloaded file.
+   */
+  showDownloadedFile(aFile) {
+    if (!(aFile instanceof Ci.nsIFile)) {
+      throw new Error("aFile must be a nsIFile object");
+    }
+    try {
+      // Show the directory containing the file and select the file.
+      aFile.reveal();
+    } catch (ex) {
+      // If reveal fails for some reason (e.g., it's not implemented on unix
+      // or the file doesn't exist), try using the parent if we have it.
+      let parent = aFile.parent;
+      if (parent) {
+        this.showDirectory(parent);
+      }
+    }
+  },
+
+  /**
+   * Show the specified folder in the system file manager.
+   *
+   * @param aDirectory
+   *        a directory to be opened with system file manager.
+   */
+  showDirectory(aDirectory) {
+    if (!(aDirectory instanceof Ci.nsIFile)) {
+      throw new Error("aDirectory must be a nsIFile object");
+    }
+    try {
+      aDirectory.launch();
+    } catch (ex) {
+      // If launch fails (probably because it's not implemented), let
+      // the OS handler try to open the directory.
+      Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+        .getService(Ci.nsIExternalProtocolService)
+        .loadUrl(NetUtil.newURI(aDirectory));
+    }
+  },
+
+  /**
+   * Displays an alert message box which asks the user if they want to
+   * unblock the downloaded file or not.
+   *
+   * @param options
+   *        An object with the following properties:
+   *        {
+   *          verdict:
+   *            The detailed reason why the download was blocked, according to
+   *            the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
+   *            reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
+   *            assumed.
+   *          window:
+   *            The window with which this action is associated.
+   *          dialogType:
+   *            String that determines which actions are available:
+   *             - "unblock" to offer just "unblock".
+   *             - "chooseUnblock" to offer "unblock" and "confirmBlock".
+   *             - "chooseOpen" to offer "open" and "confirmBlock".
+   *        }
+   *
+   * @return {Promise}
+   * @resolves String representing the action that should be executed:
+   *            - "open" to allow the download and open the file.
+   *            - "unblock" to allow the download without opening the file.
+   *            - "confirmBlock" to delete the blocked data permanently.
+   *            - "cancel" to do nothing and cancel the operation.
+   */
+  async confirmUnblockDownload({ verdict, window,
+                                                  dialogType }) {
+    let s = DownloadsCommon.strings;
+
+    // All the dialogs have an action button and a cancel button, while only
+    // some of them have an additonal button to remove the file. The cancel
+    // button must always be the one at BUTTON_POS_1 because this is the value
+    // returned by confirmEx when using ESC or closing the dialog (bug 345067).
+    let title = s.unblockHeaderUnblock;
+    let firstButtonText = s.unblockButtonUnblock;
+    let firstButtonAction = "unblock";
+    let buttonFlags =
+        (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+        (Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1);
+
+    switch (dialogType) {
+      case "unblock":
+        // Use only the unblock action. The default is to cancel.
+        buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+        break;
+      case "chooseUnblock":
+        // Use the unblock and remove file actions. The default is remove file.
+        buttonFlags +=
+          (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
+          Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
+        break;
+      case "chooseOpen":
+        // Use the unblock and open file actions. The default is open file.
+        title = s.unblockHeaderOpen;
+        firstButtonText = s.unblockButtonOpen;
+        firstButtonAction = "open";
+        buttonFlags +=
+          (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
+          Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
+        break;
+      default:
+        Cu.reportError("Unexpected dialog type: " + dialogType);
+        return "cancel";
+    }
+
+    let message;
+    switch (verdict) {
+      case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
+        message = s.unblockTypeUncommon2;
+        break;
+      case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
+        message = s.unblockTypePotentiallyUnwanted2;
+        break;
+      default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
+        message = s.unblockTypeMalware;
+        break;
+    }
+    message += "\n\n" + s.unblockTip2;
+
+    Services.ww.registerNotification(function onOpen(subj, topic) {
+      if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+        // Make sure to listen for "DOMContentLoaded" because it is fired
+        // before the "load" event.
+        subj.addEventListener("DOMContentLoaded", function() {
+          if (subj.document.documentURI ==
+              "chrome://global/content/commonDialog.xul") {
+            Services.ww.unregisterNotification(onOpen);
+            let dialog = subj.document.getElementById("commonDialog");
+            if (dialog) {
+              // Change the dialog to use a warning icon.
+              dialog.classList.add("alert-dialog");
+            }
+          }
+        }, {once: true});
+      }
+    });
+
+    let rv = Services.prompt.confirmEx(window, title, message, buttonFlags,
+                                       firstButtonText, null,
+                                       s.unblockButtonConfirmBlock, null, {});
+    return [firstButtonAction, "cancel", "confirmBlock"][rv];
+  },
 };
 
 XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "log", () => {
   return DownloadsLogger.log.bind(DownloadsLogger);
 });
 XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "error", () => {
   return DownloadsLogger.error.bind(DownloadsLogger);
 });
@@ -297,51 +629,35 @@ DownloadsDataCtor.prototype = {
     return; // No UI for behavior >= 2
   },
 
   onDownloadChanged(download) {
     let oldState = this.oldDownloadStates.get(download);
     let newState = DownloadsCommon.stateOfDownload(download);
     this.oldDownloadStates.set(download, newState);
 
-    if (oldState != newState) {
-      if (download.succeeded ||
-          (download.canceled && !download.hasPartialData) ||
-          download.error) {
-        // Store the end time that may be displayed by the views.
-        download.endTime = Date.now();
+    if (oldState != newState &&
+        (download.succeeded ||
+         (download.canceled && !download.hasPartialData) ||
+          download.error)) {
+      // Store the end time that may be displayed by the views.
+      download.endTime = Date.now();
 
-        // This state transition code should actually be located in a Downloads
-        // API module (bug 941009).
-        DownloadHistory.updateMetaData(download);
-      }
-
-      if (download.succeeded ||
-          (download.error && download.error.becauseBlocked)) {
-        this._notifyDownloadEvent("finish");
-      }
-    }
-
-    if (!download.newDownloadNotified) {
-      download.newDownloadNotified = true;
-      this._notifyDownloadEvent("start");
+      DownloadsCommon.log("DownloadHistory.updateMetaData");
+      // This state transition code should actually be located in a Downloads
+      // API module (bug 941009).
+      // This might end with an exception if it is an unsupported uri scheme.
+      DownloadHistory.updateMetaData(download);
     }
   },
 
   onDownloadRemoved(download) {
     this.oldDownloadStates.delete(download);
   },
 
-  onDownloadChanged: function(aDownload) {
-    // This mighe be effective but the staement s*cks.
-    aDownload.state = DownloadsCommon.stateOfDownload(aDownload);
-    if (this._downloadsLoaded && (aDownload.succeeded || !aDownload.stopped))
-      aDownload.endTime = Date.now();
-  },
-
   // Download summary
   onSummaryChanged:  function() {
 
   if (!gTaskbarProgress)
     return;
 
   const nsITaskbarProgress = Ci.nsITaskbarProgress;
   var currentBytes = gDownloadsSummary.progressCurrentBytes;
@@ -382,17 +698,16 @@ DownloadsDataCtor.prototype = {
    *
    * @param aView
    *        DownloadsView object to be removed.
    */
   removeView(aView) {
     this._promiseList.then(list => list.removeView(aView))
                      .catch(Cu.reportError);
   },
-
 };
 
 XPCOMUtils.defineLazyGetter(this, "HistoryDownloadsData", function() {
   return new DownloadsDataCtor({ isHistory: true });
 });
 
 XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
   return new DownloadsDataCtor();
@@ -418,16 +733,24 @@ const DownloadsViewPrototype = {
    * Array of view objects that should be notified when the available status
    * data changes.
    *
    * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
    */
   _views: null,
 
   /**
+   * Determines whether this view object is over the private or non-private
+   * downloads.
+   *
+   * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
+   */
+  _isPrivate: false,
+
+  /**
    * Adds an object to be notified when the available status data changes.
    * The specified object is initialized with the currently available status.
    *
    * @param aView
    *        View object to be added.  This reference must be
    *        passed to removeView before termination.
    */
   addView(aView) {
--- a/suite/components/downloads/content/downloadmanager.js
+++ b/suite/components/downloads/content/downloadmanager.js
@@ -1,19 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
 ChromeUtils.import("resource://gre/modules/Downloads.jsm");
 
-const nsIDownloadManager = Ci.nsIDownloadManager;
-const nsLocalFile = Components.Constructor("@mozilla.org/file/local;1",
-                                           Ci.nsIFile,
-                                           "initWithPath");
+XPCOMUtils.defineLazyScriptGetter(this, "DownloadsCommon",
+                                  "resource:///modules/DownloadsCommon.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
 
 var gDownloadTree;
 var gDownloadTreeView;
 var gDownloadList;
 var gDownloadStatus;
 var gDownloadListener;
 var gSearchBox;
 
@@ -124,112 +128,55 @@ function sortDownloads(aEventTarget)
 
   if (column) {
     // Set attributes to the sorting we did
     column.setAttribute("sortActive", "true");
     column.setAttribute("sortDirection", sortDirection);
   }
 }
 
-function removeDownload(aDownload)
+async function removeDownload(aDownload)
 {
-  aDownload.finalize(true);
-  gDownloadList.remove(aDownload);
+  // Remove the associated history element first, if any, so that the views
+  // that combine history and session downloads won't resurrect the history
+  // download into the view just before it is deleted permanently.
+  try {
+    await PlacesUtils.history.remove(aDownload.source.url);
+  } catch (ex) {
+    Cu.reportError(ex);
+  }
+  let list = await Downloads.getList(Downloads.ALL);
+  await list.remove(aDownload);
+  await aDownload.finalize(true);
 }
 
 function cancelDownload(aDownload)
 {
-  aDownload.cancel().then(function() {
-    var currentBytes = aDownload.currentBytes;
-    if (aDownload.hasPartialData) {
-      aDownload.removePartialData().then(function() {
-        aDownload.currentBytes = currentBytes;
-      });
-    }
-  });
+  // This is the correct way to avoid race conditions when cancelling.
+  aDownload.cancel().catch(() => {});
+  aDownload.removePartialData().catch(Cu.reportError);
 }
 
 function openDownload(aDownload)
 {
-  var name = aDownload.displayName;
-  var file = new nsLocalFile(aDownload.target.path);
-
-  if (file.isExecutable()) {
-    var alertOnEXEOpen = 
-      Services.prefs.getBoolPref("browser.download.manager.alertOnEXEOpen",
-                                 true);
-
-    // On Windows 7 and above, we rely on native security prompting for
-    // downloaded content unless it's disabled.
-    try {
-      var sysInfo = Cc["@mozilla.org/system-info;1"]
-                      .getService(Ci.nsIPropertyBag2);
-      if (/^Windows/.test(sysInfo.getProperty("name")) &&
-          Services.prefs.getBoolPref("browser.download.manager.scanWhenDone"))
-        alertOnEXEOpen = false;
-    } catch (ex) { }
-
-    if (alertOnEXEOpen) {
-      var dlbundle = document.getElementById("dmBundle");
-      var message = dlbundle.getFormattedString("fileExecutableSecurityWarning", [name, name]);
-
-      var title = dlbundle.getString("fileExecutableSecurityWarningTitle");
-      var dontAsk = dlbundle.getString("fileExecutableSecurityWarningDontAsk");
-
-      var checkbox = { value: false };
-      if (!Services.prompt.confirmCheck(window, title, message, dontAsk, checkbox))
-        return;
-      Services.prefs.setBoolPref("browser.download.manager.alertOnEXEOpen", !checkbox.value);
-    }
-  }
-
-  try {
-    var mimeInfo = aDownload.MIMEInfo;
-    if (mimeInfo && mimeInfo.preferredAction == mimeInfo.useHelperApp) {
-      mimeInfo.launchWithFile(file);
-      return;
-    }
-  } catch (ex) { }
-
-  try {
-    file.launch();
-  } catch (ex) {
-    // If launch fails, try sending it through the system's external
-    // file: URL handler
-    var uri = Services.io.newFileURI(file);
-    var protocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
-                        .getService(Ci.nsIExternalProtocolService);
-    protocolSvc.loadURI(uri);
-  }
+  let file = new FileUtils.File(aDownload.target.path);
+  DownloadsCommon.openDownloadedFile(file, null, window);
 }
 
 function showDownload(aDownload)
 {
-  var file = new nsLocalFile(aDownload.target.path);
-
-  try {
-    // Show the directory containing the file and select the file
-    file.reveal();
-  } catch (e) {
-    // If reveal fails for some reason (e.g., it's not implemented on unix or
-    // the file doesn't exist), try using the parent if we have it.
-    var parent = file.parent.QueryInterface(Ci.nsIFile);
+  let file;
 
-    try {
-      // "Double click" the parent directory to show where the file should be
-      parent.launch();
-    } catch (e) {
-      // If launch also fails (probably because it's not implemented), let the
-      // OS handler try to open the parent
-      var uri = Services.io.newFileURI(parent);
-      var protocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
-                          .getService(Ci.nsIExternalProtocolService);
-      protocolSvc.loadURI(uri);
-    }
+  if (aDownload.succeeded &&
+      aDownload.target.exists) {
+    file = new FileUtils.File(aDownload.target.path);
+  } else {
+    file = new FileUtils.File(aDownload.target.partFilePath);
   }
+  DownloadsCommon.showDownloadedFile(file);
 }
 
 function showProperties(aDownload)
 {
   openDialog("chrome://communicator/content/downloads/progressDialog.xul",
              null, "chrome,titlebar,centerscreen,minimizable=yes,dialog=no",
              { wrappedJSObject: aDownload }, true);
 }
@@ -446,18 +393,25 @@ var dlTreeController = {
             return false;
         }
         return true;
       case "cmd_open":
         return selectionCount == 1 &&
                selItemData[0].succeeded &&
                selItemData[0].target.exists;
       case "cmd_show":
+        // target.exists is only set if the download finished and the target
+        // is still located there.
+        // For simplicity we just assume the target is there if the download
+        // has not succeeded e.g. is still in progress. This might be wrong
+        // but showDownload will deal with it.
         return selectionCount == 1 &&
-               selItemData[0].target.exists;
+               ((selItemData[0].succeeded &&
+                 selItemData[0].target.exists) ||
+                 !selItemData[0].succeeded);
       case "cmd_cancel":
         if (!selectionCount)
           return false;
         for (let dldata of selItemData) {
           if (dldata.stopped && !dldata.hasPartialData)
             return false;
         }
         return true;
@@ -485,17 +439,17 @@ var dlTreeController = {
       case "cmd_properties":
         return selectionCount == 1;
       case "cmd_selectAll":
         return gDownloadTreeView.rowCount != selectionCount;
       case "cmd_clearList":
         // Since active downloads always sort before removable downloads,
         // we only need to check that the last download has stopped.
         return gDownloadTreeView.rowCount &&
-               gDownloadTreeView.getRowData(gDownloadTreeView.rowCount - 1).isActive;
+               !gDownloadTreeView.getRowData(gDownloadTreeView.rowCount - 1).isActive;
       case "cmd_paste":
         return true;
       default:
         return false;
     }
   },
 
   doCommand: function(aCommand) {
@@ -526,26 +480,28 @@ var dlTreeController = {
         }
         break;
       case "cmd_pause":
         for (let dldata of selItemData)
           dldata.cancel();
         break;
       case "cmd_resume":
       case "cmd_retry":
-        for (let dldata of selItemData)
-          dldata.start();
+        for (let dldata of selItemData) {
+         // Errors when retrying are already reported as download failures.
+         dldata.start();
+        }
         break;
       case "cmd_cancel":
         for (let dldata of selItemData)
           cancelDownload(dldata);
         break;
       case "cmd_remove":
         for (let dldata of selItemData)
-          removeDownload(dldata);
+          removeDownload(dldata).catch(Cu.reportError);
         break;
       case "cmd_stop":
         for (let dldata of selItemData) {
           if (dldata.isActive)
             cancelDownload(dldata);
           else
             gDownloadList.remove(dldata);
         }
@@ -618,17 +574,17 @@ var gDownloadDNDObserver = {
   onDragStart: function (aEvent)
   {
     if (!gDownloadTreeView ||
         !gDownloadTreeView.selection ||
         !gDownloadTreeView.selection.count)
       return;
 
     var selItemData = gDownloadTreeView.getRowData(gDownloadTree.currentIndex);
-    var file = new nsLocalFile(selItemData.target.path);
+    var file = new FileUtils.File(selItemData.target.path);
 
     if (!file.exists())
       return;
 
     var url = Services.io.newFileURI(file).spec;
     var dt = aEvent.dataTransfer;
     dt.mozSetDataAt("application/x-moz-file", file, 0);
     dt.setData("text/uri-list", url + "\r\n");
--- a/suite/components/downloads/content/downloadmanager.xul
+++ b/suite/components/downloads/content/downloadmanager.xul
@@ -325,31 +325,33 @@
           </menu>
         </menupopup>
       </menu>
       <menu id="tasksMenu">
         <menupopup id="taskPopup">
           <menuitem id="dlMenu_find"
                     label="&search.label;"
                     accesskey="&search.accesskey;"
+                    hidden="true"
                     command="cmd_search_focus"
                     key="key_search_focus"/>
           <menuseparator/>
         </menupopup>
       </menu>
       <menu id="windowMenu"/>
       <menu id="menu_Help"/>
     </menubar>
     <toolbar class="chromeclass-toolbar"
              id="downloadToolbar"
              align="center"
              grippytooltiptext="&searchBar.tooltip;">
       <textbox id="search-box"
                clickSelectsAll="true"
                type="search"
+               hidden="true"
                aria-controls="downloadTree"
                class="compact"
                placeholder="&search.placeholder;"
                oncommand="searchDownloads(this.value);"/>
       <spacer flex="1"/>
       <button id="clearListButton" command="cmd_clearList"
               label="&cmd.clearList.label;"
               accesskey="&cmd.clearList.accesskey;"
--- a/suite/components/downloads/content/progressDialog.js
+++ b/suite/components/downloads/content/progressDialog.js
@@ -1,14 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
 
+XPCOMUtils.defineLazyScriptGetter(this, "DownloadsCommon",
+                                  "resource:///modules/DownloadsCommon.jsm");
+
 var gDownload;
 var gDownloadBundle;
 var gTkDlBundle;
 
 var gDlList;
 var gDlStatus;
 var gDlListener;
 var gDlSize;
@@ -95,42 +100,18 @@ function updateDownload() {
     gProgressMeter.mode = "undetermined";
   }
   if (gDownload.stopped) {
     gProgressMeter.style.opacity = 0.5;
   } else {
     gProgressMeter.style.opacity = 1;
   }
   // Update window title
-  var statusString;
-  switch (gDownload.state) {
-    case nsIDownloadManager.DOWNLOAD_PAUSED:
-      statusString = gDownloadBundle.getString("paused");
-      break;
-    case nsIDownloadManager.DOWNLOAD_DOWNLOADING:
-      statusString = gDownloadBundle.getString("downloading");
-      break;
-    case nsIDownloadManager.DOWNLOAD_FINISHED:
-      statusString = gDownloadBundle.getString("finished");
-      break;
-    case nsIDownloadManager.DOWNLOAD_FAILED:
-      statusString = gDownloadBundle.getString("failed");
-      break;
-    case nsIDownloadManager.DOWNLOAD_CANCELED:
-      statusString = gDownloadBundle.getString("canceled");
-      break;
-    case nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls
-    case nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY:   // Security Zone Policy
-    case nsIDownloadManager.DOWNLOAD_DIRTY:            // possible virus/spyware
-      statusString = gDownloadBundle.getString("blocked");
-      break;
-    default:
-      statusString = gDownloadBundle.getString("notStarted");
-      break;
-  }
+  let statusString = DownloadsCommon.stateOfDownloadText(gDownloadBundle);
+
   if (gDownload.hasProgress) {
     document.title = gDownloadBundle.getFormattedString("progressTitlePercent",
                                                         [gDownload.progress,
                                                          gDownload.displayName,
                                                          statusString]);
   }
   else {
     document.title = gDownloadBundle.getFormattedString("progressTitle",
@@ -154,24 +135,26 @@ function updateDownload() {
   if (!gDownload.stopped) {
     // Calculate the time remaining if we have valid values
     var seconds = (gDownload.speed > 0) && (gDownload.totalBytes > 0)
                   ? (gDownload.totalBytes - gDownload.currentBytes) / gDownload.speed
                   : -1;
     var [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, gLastSec);
     gLastSec = newLast;
   }
-  switch (gDownload.state) {
-    case nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls
+
+  let state = DownloadsCommon.stateOfDownload(gDownload);
+  switch (state) {
+    case DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls
       gDlStatus.value = gTkDlBundle.getString("stateBlocked");
       break;
-    case nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY:   // Security Zone Policy
+    case DownloadsCommon.DOWNLOAD_BLOCKED_POLICY:   // Security Zone Policy
       gDlStatus.value = gTkDlBundle.getString("stateBlockedPolicy");
       break;
-    case nsIDownloadManager.DOWNLOAD_DIRTY:            // possible virus/spyware
+    case DownloadsCommon.DOWNLOAD_DIRTY:            // possible virus/spyware
       gDlStatus.value = gTkDlBundle.getString("stateDirty");
       break;
     default:
       if (gDlActive)
         gDlStatus.value = gDownloadBundle.getFormattedString("statusActive",
                                                              [statusString, timeLeft]);
       else
         gDlStatus.value = statusString;
--- a/suite/components/downloads/content/treeView.js
+++ b/suite/components/downloads/content/treeView.js
@@ -1,63 +1,74 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
 
-const nsITreeView = Ci.nsITreeView;
-// const nsIDownloadManager is already defined in downloadmanager.js
+XPCOMUtils.defineLazyScriptGetter(this, "DownloadsCommon",
+                                  "resource:///modules/DownloadsCommon.jsm");
+
+XPCOMUtils.defineLazyScriptGetter(this, "DownloadHistory",
+                                  "resource://gre/modules/DownloadHistory.jsm");
 
 function DownloadTreeView() {
   this._dlList = [];
   this._searchTerms = [];
+  this.dateTimeFormatter =
+    new Services.intl.DateTimeFormat(undefined,
+                                     {dateStyle: "short",
+                                      timeStyle: "long"});
 }
 
 DownloadTreeView.prototype = {
-  QueryInterface: XPCOMUtils.generateQI([nsITreeView]),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
 
   // ***** nsITreeView attributes and methods *****
   get rowCount() {
     return this._dlList.length;
   },
 
   selection: null,
 
   getRowProperties: function(aRow) {
-    var dl = this._dlList[aRow];
+    let dl = this._dlList[aRow];
     // (in)active
-    var properties = dl.isActive ? "active": "inactive";
+    let properties = dl.isActive ? "active": "inactive";
     // resumable
     if (dl.hasPartialData)
       properties += " resumable";
+
     // Download states
-    switch (dl.state) {
-      case nsIDownloadManager.DOWNLOAD_PAUSED:
+    let state = DownloadsCommon.stateOfDownload(dl);
+    switch (state) {
+      case DownloadsCommon.DOWNLOAD_PAUSED:
         properties += " paused";
         break;
-      case nsIDownloadManager.DOWNLOAD_DOWNLOADING:
+      case DownloadsCommon.DOWNLOAD_DOWNLOADING:
         properties += " downloading";
         break;
-      case nsIDownloadManager.DOWNLOAD_FINISHED:
+      case DownloadsCommon.DOWNLOAD_FINISHED:
         properties += " finished";
         break;
-      case nsIDownloadManager.DOWNLOAD_FAILED:
+      case DownloadsCommon.DOWNLOAD_FAILED:
         properties += " failed";
         break;
-      case nsIDownloadManager.DOWNLOAD_CANCELED:
+      case DownloadsCommon.DOWNLOAD_CANCELED:
         properties += " canceled";
         break;
-      case nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls
-      case nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY:   // Security Zone Policy
-      case nsIDownloadManager.DOWNLOAD_DIRTY:            // possible virus/spyware
+      case DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls
+      case DownloadsCommon.DOWNLOAD_BLOCKED_POLICY:   // Security Zone Policy
+      case DownloadsCommon.DOWNLOAD_DIRTY:            // possible virus/spyware
         properties += " blocked";
         break;
     }
+
     return properties;
   },
   getCellProperties: function(aRow, aColumn) {
     // Append all row properties to the cell
     return this.getRowProperties(aRow);
   },
   getColumnProperties: function(aColumn) { return ""; },
   isContainer: function(aRow) { return false; },
@@ -75,113 +86,125 @@ DownloadTreeView.prototype = {
     if (aColumn.id == "Name")
       return "moz-icon://" + this._dlList[aRow].target.path + "?size=16";
     return "";
   },
 
   getProgressMode: function(aRow, aColumn) {
     if (aColumn.id == "Progress")
       return this._dlList[aRow].progressMode;
-    return nsITreeView.PROGRESS_NONE;
+    return Ci.nsITreeView.PROGRESS_NONE;
   },
 
   getCellValue: function(aRow, aColumn) {
     if (aColumn.id == "Progress")
       return this._dlList[aRow].progress;
     return "";
   },
 
   getCellText: function(aRow, aColumn) {
     var dl = this._dlList[aRow];
     switch (aColumn.id) {
       case "Name":
         return dl.displayName;
       case "Status":
-        switch (dl.state) {
-          case nsIDownloadManager.DOWNLOAD_PAUSED:
-            return this._dlbundle.getString("paused");
-          case nsIDownloadManager.DOWNLOAD_DOWNLOADING:
-            return this._dlbundle.getString("downloading");
-          case nsIDownloadManager.DOWNLOAD_FINISHED:
-            return this._dlbundle.getString("finished");
-          case nsIDownloadManager.DOWNLOAD_FAILED:
-            return this._dlbundle.getString("failed");
-          case nsIDownloadManager.DOWNLOAD_CANCELED:
-            return this._dlbundle.getString("canceled");
-          case nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls
-          case nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY:   // Security Zone Policy
-          case nsIDownloadManager.DOWNLOAD_DIRTY:            // possible virus/spyware
-            return this._dlbundle.getString("blocked");
-        }
-        return this._dlbundle.getString("notStarted");
+        return DownloadsCommon.stateOfDownloadText(dl);
       case "Progress":
         if (dl.isActive)
           return dl.progress;
-        switch (dl.state) {
-          case nsIDownloadManager.DOWNLOAD_FINISHED:
-            return this._dlbundle.getString("finished");
-          case nsIDownloadManager.DOWNLOAD_FAILED:
-            return this._dlbundle.getString("failed");
-          case nsIDownloadManager.DOWNLOAD_CANCELED:
-            return this._dlbundle.getString("canceled");
-          case nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL: // Parental Controls
-          case nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY:   // Security Zone Policy
-          case nsIDownloadManager.DOWNLOAD_DIRTY:            // possible virus/spyware
-            return this._dlbundle.getString("blocked");
-        }
-        return this._dlbundle.getString("notStarted");
+        return DownloadsCommon.stateOfDownloadText(dl);
       case "ProgressPercent":
         return dl.succeeded ? 100 : dl.progress;
       case "TimeRemaining":
         if (!dl.stopped) {
           var lastSec = (dl.lastSec == null) ? Infinity : dl.lastSec;
           // Calculate the time remaining if we have valid values
           var seconds = (dl.speed > 0) && (dl.totalBytes > 0)
                         ? (dl.totalBytes - dl.currentBytes) / dl.speed
                         : -1;
           var [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, lastSec);
           this._dlList[aRow].lastSec = newLast;
           return timeLeft;
         }
         return "";
       case "Transferred":
-        if (dl.succeeded)
-          return DownloadUtils.getTransferTotal(dl.totalBytes, -1);
-        if (dl.stopped && !dl.currentBytes)
+        let currentBytes;
+        let totalBytes;
+        // Download in progress.
+        // Download paused / canceled and has partial data.
+        if (!dl.stopped ||
+            (dl.canceled && dl.hasPartialData)) {
+          currentBytes = dl.currentBytes,
+          totalBytes = dl.hasProgress ? dl.totalBytes : -1;
+        // Download done but file missing.
+        } else if (dl.succeeded && !dl.exists) {
+          currentBytes = dl.totalBytes ? dl.totalBytes : -1;
+          totalBytes = -1;
+        // For completed downloads, show the file size
+        } else if (dl.succeeded && dl.target.size !== undefined) {
+           currentBytes = dl.target.size;
+           totalBytes = -1;
+        // Some local files saves e.g. from attachments also have no size.
+        // They only have a target in downloads.json but no target.path.
+        // FIX ME later.
+        } else {
+          currentBytes = -1;
+          totalBytes = -1;
+        }
+
+        // We do not want to show 0 of xxx bytes.
+        if (currentBytes == 0) {
+          currentBytes = -1;
+        }
+
+        if (totalBytes == 0) {
+          totalBytes = -1;
+        }
+
+        // We tried everything.
+        if (currentBytes == -1 && totalBytes == -1) {
           return "";
-        return DownloadUtils.getTransferTotal(dl.currentBytes, dl.totalBytes);
+        }
+
+        return DownloadUtils.getTransferTotal(currentBytes, totalBytes);
       case "TransferRate":
-        switch (dl.state) {
-          case nsIDownloadManager.DOWNLOAD_DOWNLOADING:
+        let state = DownloadsCommon.stateOfDownload(dl);
+        switch (state) {
+          case DownloadsCommon.DOWNLOAD_DOWNLOADING:
             var [rate, unit] = DownloadUtils.convertByteUnits(dl.speed);
             return this._dlbundle.getFormattedString("speedFormat", [rate, unit]);
-          case nsIDownloadManager.DOWNLOAD_PAUSED:
-            return this._dlbundle.getString("paused");
-          case nsIDownloadManager.DOWNLOAD_NOTSTARTED:
-          case nsIDownloadManager.DOWNLOAD_QUEUED:
-            return this._dlbundle.getString("notStarted");
+          case DownloadsCommon.DOWNLOAD_PAUSED:
+            return this._dlbundle.getString("statePaused");
+          case DownloadsCommon.DOWNLOAD_NOTSTARTED:
+            return this._dlbundle.getString("stateNotStarted");
         }
         return "";
       case "TimeElapsed":
         if (dl.endTime && dl.startTime && (dl.endTime > dl.startTime)) {
           var seconds = (dl.endTime - dl.startTime) / 1000;
           var [time1, unit1, time2, unit2] =
             DownloadUtils.convertTimeUnits(seconds);
           if (seconds < 3600 || time2 == 0)
             return this._dlbundle.getFormattedString("timeSingle", [time1, unit1]);
           return this._dlbundle.getFormattedString("timeDouble", [time1, unit1, time2, unit2]);
         }
         return "";
       case "StartTime":
-        if (dl.startTime)
-          return this._convertTimeToString(dl.startTime);
+        if (dl.startTime) {
+          return this.dateTimeFormatter.format(dl.startTime);
+        }
         return "";
       case "EndTime":
-        if (dl.endTime)
-          return this._convertTimeToString(dl.endTime);
+        // This might end with an exception if it is an unsupported uri
+        // scheme.
+        let metaData = DownloadHistory.getPlacesMetaDataFor(dl.source.url);
+
+        if (metaData.endTime) {
+          return this.dateTimeFormatter.format(metaData.endTime);
+        }
         return "";
       case "Source":
         return dl.source.url;
     }
     return "";
   },
 
   setTree: function(aTree) {
@@ -218,29 +241,28 @@ DownloadTreeView.prototype = {
   setCellText: function(aRow, aColumn, aText) { },
   performAction: function(aAction) { },
   performActionOnRow: function(aAction, aRow) { },
   performActionOnCell: function(aAction, aRow, aColumn) { },
 
   // ***** local public methods *****
 
   addDownload: function(aDownload) {
-    aDownload.progressMode = nsITreeView.PROGRESS_NONE;
+    aDownload.progressMode = Ci.nsITreeView.PROGRESS_NONE;
     aDownload.lastSec = Infinity;
-    switch (aDownload.state) {
-      case nsIDownloadManager.DOWNLOAD_DOWNLOADING:
+    let state = DownloadsCommon.stateOfDownload(aDownload);
+    switch (state) {
+      case DownloadsCommon.DOWNLOAD_DOWNLOADING:
         aDownload.endTime = Date.now();
         // At this point, we know if we are an indeterminate download or not.
         aDownload.progressMode = aDownload.hasProgress ?
-                                               nsITreeView.PROGRESS_UNDETERMINED :
-                                               nsITreeView.PROGRESS_NORMAL;
-      case nsIDownloadManager.DOWNLOAD_NOTSTARTED:
-      case nsIDownloadManager.DOWNLOAD_PAUSED:
-      case nsIDownloadManager.DOWNLOAD_QUEUED:
-      case nsIDownloadManager.DOWNLOAD_SCANNING:
+                                               Ci.nsITreeView.PROGRESS_UNDETERMINED :
+                                               Ci.nsITreeView.PROGRESS_NORMAL;
+      case DownloadsCommon.DOWNLOAD_NOTSTARTED:
+      case DownloadsCommon.DOWNLOAD_PAUSED:
         aDownload.isActive = 1;
         break;
       default:
         aDownload.isActive = 0;
         break;
     }
 
     // prepend in natural sorting
@@ -261,30 +283,29 @@ DownloadTreeView.prototype = {
   updateDownload: function(aDownload) {
     var row = this._dlList.indexOf(aDownload);
     if (row == -1) {
       // No download row found to update, but as it's obviously going on,
       // add it to the list now (can happen with very fast, e.g. local dls)
       this.onDownloadAdded(aDownload);
       return;
     }
-    switch (aDownload.state) {
-      case nsIDownloadManager.DOWNLOAD_DOWNLOADING:
+    let state = DownloadsCommon.stateOfDownload(aDownload);
+    switch (state) {
+      case DownloadsCommon.DOWNLOAD_DOWNLOADING:
         // At this point, we know if we are an indeterminate download or not.
         aDownload.progressMode = aDownload.hasProgress ?
-          nsITreeView.PROGRESS_NORMAL : nsITreeView.PROGRESS_UNDETERMINED;
-      case nsIDownloadManager.DOWNLOAD_NOTSTARTED:
-      case nsIDownloadManager.DOWNLOAD_PAUSED:
-      case nsIDownloadManager.DOWNLOAD_QUEUED:
-      case nsIDownloadManager.DOWNLOAD_SCANNING:
+          Ci.nsITreeView.PROGRESS_NORMAL : Ci.nsITreeView.PROGRESS_UNDETERMINED;
+      case DownloadsCommon.DOWNLOAD_NOTSTARTED:
+      case DownloadsCommon.DOWNLOAD_PAUSED:
         aDownload.isActive = 1;
         break;
       default:
         aDownload.isActive = 0;
-        aDownload.progressMode = nsITreeView.PROGRESS_NONE;
+        aDownload.progressMode = Ci.nsITreeView.PROGRESS_NONE;
         // This preference may not be set, so defaulting to two.
         var flashCount = 2;
         try {
           flashCount = Services.prefs.getIntPref(PREF_FLASH_COUNT);
         } catch (e) { }
         getAttentionWithCycleCount(flashCount);
         break;
     }
@@ -369,18 +390,18 @@ DownloadTreeView.prototype = {
       var comp_a = null;
       var comp_b = null;
       switch (aColumnID) {
         case "Name":
           comp_a = a.displayName.toLowerCase();
           comp_b = b.displayName.toLowerCase();
           break;
         case "Status":
-          comp_a = a.state;
-          comp_b = b.state;
+          comp_a = DownloadsCommon.stateOfDownload(a);
+          comp_b = DownloadsCommon.stateOfDownload(b);
           break;
         case "Progress":
         case "ProgressPercent":
           // Use original sorting for inactive entries
           // Use only one isActive to be sure we do the same
           comp_a = a.isActive ? a.progress : a.listIndex;
           comp_b = a.isActive ? b.progress : b.listIndex;
           break;
@@ -474,17 +495,17 @@ DownloadTreeView.prototype = {
     // Abort if there's already something cached
     if (this._selectionCache)
       return;
 
     this._selectionCache = [];
     if (this.selection.count < 1)
       return;
 
-    // Walk all selected rows and cache theior download IDs
+    // Walk all selected rows and cache their download IDs
     var start = {};
     var end = {};
     var numRanges = this.selection.getRangeCount();
     for (let rg = 0; rg < numRanges; rg++){
       this.selection.getRangeAt(rg, start, end);
       for (let row = start.value; row <= end.value; row++){
         this._selectionCache.push(this._dlList[row]);
       }
@@ -502,49 +523,9 @@ DownloadTreeView.prototype = {
       // Find out what row this is now and if possible, add it to the selection
       var row = this._dlList.indexOf(dl);
       if (row != -1)
         this.selection.rangedSelect(row, row, true);
     }
     // Work done, clear the cache
     this._selectionCache = null;
   },
-
-  _convertTimeToString: function(aTime) {
-    const MS_PER_MINUTE = 60000;
-    const MS_PER_DAY = 86400000;
-    let timeMs = aTime / 1000; // PRTime is in microseconds
-
-    // Date is calculated starting from midnight, so the modulo with a day are
-    // milliseconds from today's midnight.
-    // getTimezoneOffset corrects that based on local time, notice midnight
-    // can have a different offset during DST-change days.
-    let dateObj = new Date();
-    let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE;
-    let midnight = now - (now % MS_PER_DAY);
-    midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE;
-
-    let timeObj = new Date(timeMs);
-    return timeMs >= midnight ? this._todayFormatter.format(timeObj)
-                              : this._dateFormatter.format(timeObj);
-  },
-
-  // We use a different formatter for times within the current day,
-  // so we cache both a "today" formatter and a general date formatter.
-  __todayFormatter: null,
-  get _todayFormatter() {
-    if (!this.__todayFormatter) {
-      const dtOptions = { timeStyle: "short" };
-      this.__todayFormatter = new Services.intl.DateTimeFormat(undefined, dtOptions);
-    }
-    return this.__todayFormatter;
-  },
-
-  __dateFormatter: null,
-  get _dateFormatter() {
-    if (!this.__dateFormatter) {
-      const dtOptions = { dateStyle: "short", timeStyle: "short" };
-      this.__dateFormatter = new Services.intl.DateTimeFormat(undefined, dtOptions);
-    }
-    return this.__dateFormatter;
-  },
-
 };
--- a/suite/components/downloads/content/uploadProgress.js
+++ b/suite/components/downloads/content/uploadProgress.js
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
 
 const kInterval = 750; // Default to .75 seconds.
 
 var gPersist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
                  .createInstance(Ci.nsIWebBrowserPersist);
 var gSource = window.arguments[0].QueryInterface(Ci.nsIFileURL);
 var gTarget = window.arguments[1].QueryInterface(Ci.nsIURL);
@@ -28,17 +28,17 @@ var gMeter;
 function onLoad()
 {
   gBundle = document.getElementById("dmBundle");
   gStatus = document.getElementById("status");
   gTime = document.getElementById("timeElapsed");
   gSize = document.getElementById("size");
   gProgress = document.getElementById("progressText");
   gMeter = document.getElementById("progress");
-  var status = gBundle.getString("notStarted");
+  var status = gBundle.getString("stateNotStarted");
   document.title =
       gBundle.getFormattedString("progressTitle", [gFileName, status]);
   gStatus.value = status;
   gTime.value = gBundle.getFormattedString("timeSingle",
                                            DownloadUtils.convertTimeUnits(0));
   gSize.value = DownloadUtils.getTransferTotal(0, gFileSize);
   document.getElementById("target").value =
       gBundle.getFormattedString("toTarget", [gTarget.resolve(".")]);
@@ -70,17 +70,17 @@ var gProgressListener = {
   // ----- nsIWebProgressListener methods -----
 
   // Look for STATE_STOP and close dialog to indicate completion when it happens.
   onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
     if (aRequest instanceof Ci.nsIChannel &&
         aRequest.URI.equals(gTarget) &&
         aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
       gPersist = null;
-      var status = gBundle.getString("finished");
+      var status = gBundle.getString("stateCompleted");
       setPercent(100, status);
       gStatus.value = status;
       gSize.value = DownloadUtils.getTransferTotal(gFileSize, gFileSize);
       setTimeout(window.close, kInterval);
     }
   },
 
   // Handle progress notifications.
@@ -108,17 +108,17 @@ var gProgressListener = {
 
       // Update this time.
       gLastUpdate = now;
 
       // Update elapsed time.
       var elapsed = (now - gStartTime) / 1000;
 
       // Calculate percentage.
-      var status = gBundle.getString("uploading");
+      var status = gBundle.getString("stateUploading");
       var percent = -1;
       if (gFileSize > 0)
         percent = Math.floor(aCurTotalProgress * 100 / gFileSize);
       if (percent != gPercent)
         setPercent(percent, status);
 
       // Update time remaining.
       var rate = elapsed && aCurTotalProgress / elapsed;
--- a/suite/locales/en-US/chrome/common/downloads/downloadmanager.dtd
+++ b/suite/locales/en-US/chrome/common/downloads/downloadmanager.dtd
@@ -83,8 +83,14 @@
 <!ENTITY cmd.show.label                  "Open Containing Folder">
 <!ENTITY cmd.show.accesskey              "F">
 <!ENTITY cmd.goToDownloadPage.label      "Go to Download Page">
 <!ENTITY cmd.goToDownloadPage.accesskey  "G">
 <!ENTITY cmd.copyDownloadLink.label      "Copy Download Link">
 <!ENTITY cmd.copyDownloadLink.accesskey  "L">
 <!ENTITY cmd.properties.label            "Properties…">
 <!ENTITY cmd.properties.accesskey        "s">
+<!-- LOCALIZATION NOTE (cmd.unblock2.label):
+     This command is shown in the context menu when downloads are blocked.
+     -->
+<!ENTITY cmd.unblock2.label               "Allow Download">
+<!ENTITY cmd.unblock2.accesskey           "l">
+
--- a/suite/locales/en-US/chrome/common/downloads/downloadmanager.properties
+++ b/suite/locales/en-US/chrome/common/downloads/downloadmanager.properties
@@ -1,20 +1,105 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-paused=Paused
-downloading=Downloading
-uploading=Uploading
-notStarted=Not Started
-failed=Failed
-finished=Finished
-canceled=Canceled
-blocked=Blocked
+# LOCALIZATION NOTE (stateUnknown):
+# Indicates that the download stat is unknown.
+# You should never see this in the ui.
+stateUnknown=Unknown
+# LOCALIZATION NOTE (stateDownloading):
+# Indicates that the download is in progress.
+stateDownloading=Downloading
+# LOCALIZATION NOTE (stateUploading):
+# Indicates that the upload is in progress.
+stateUploading=Uploading
+# LOCALIZATION NOTE (stateStarting):
+# Indicates that the download is starting.
+# You won't probably ever see this in the ui.
+stateStarting=Starting…
+# LOCALIZATION NOTE (stateNotStarted):
+# Indicates that the download has not started yet.
+# You won't probably ever see this in the ui.
+stateNotStarted=Not Started
+# LOCALIZATION NOTE (stateScanning):
+# Indicates that an external program is scanning the download for viruses.
+stateScanning=Scanning for viruses…
+# LOCALIZATION NOTE (stateFailed):
+# Indicates that the download failed because of an error.
+stateFailed=Failed
+# LOCALIZATION NOTE (statePaused):
+# Indicates that the download was paused by the user.
+statePaused=Paused
+# LOCALIZATION NOTE (stateCanceled):
+# Indicates that the download was canceled by the user.
+stateCanceled=Canceled
+# LOCALIZATION NOTE (stateCompleted):
+# Indicates that the download was completed.
+stateCompleted=Finished
+# LOCALIZATION NOTE (stateBlockedParentalControls):
+# Indicates that the download was blocked by the Parental Controls feature of
+# Windows.  "Parental Controls" should be consistently named and capitalized
+# with the display of this feature in Windows.  The following article can
+# provide a reference for the translation of "Parental Controls" in various
+# languages:
+# http://windows.microsoft.com/en-US/windows-vista/Set-up-Parental-Controls
+stateBlockedParentalControls=Blocked by Parental Controls
+# LOCALIZATION NOTE (stateBlockedPolicy):
+# Indicates that the download was blocked on Windows because of the "Launching
+# applications and unsafe files" setting of the "security zone" associated with
+# the target site.  "Security zone" should be consistently named and capitalized
+# with the display of this feature in Windows.  The following article can
+# provide a reference for the translation of "security zone" in various
+# languages:
+# http://support.microsoft.com/kb/174360
+stateBlockedPolicy=Blocked by your security zone policy
+# LOCALIZATION NOTE (stateDirty):
+# Indicates that the download was blocked after scanning.
+stateDirty=Blocked: May contain a virus or spyware
+
+# LOCALIZATION NOTE (blockedMalware, blockedPotentiallyUnwanted,
+#                    blockedUncommon2):
+# These strings are shown in the panel for some types of blocked downloads, and
+# are immediately followed by the "Learn More" link, thus they must end with a
+# period.  You may need to adjust "downloadDetails.width" in "downloads.dtd" if
+# this turns out to be longer than the other existing status strings.
+# Note: These strings don't exist in the UI yet.  See bug 1053890.
+blockedMalware=This file contains a virus or malware.
+blockedPotentiallyUnwanted=This file may harm your computer.
+blockedUncommon2=This file is not commonly downloaded.
+
+# LOCALIZATION NOTE (unblockHeaderUnblock, unblockHeaderOpen,
+#                    unblockTypeMalware, unblockTypePotentiallyUnwanted2,
+#                    unblockTypeUncommon2, unblockTip2, unblockButtonOpen,
+#                    unblockButtonUnblock, unblockButtonConfirmBlock):
+# These strings are displayed in the dialog shown when the user asks a blocked
+# download to be unblocked.  The severity of the threat is expressed in
+# descending order by the unblockType strings, it is higher for files detected
+# as malware and lower for uncommon downloads.
+unblockHeaderUnblock=Are you sure you want to allow this download?
+unblockHeaderOpen=Are you sure you want to open this file?
+unblockTypeMalware=This file contains a virus or other malware that will harm your computer.
+unblockTypePotentiallyUnwanted2=This file is disguised as a helpful download, but it can make unexpected changes to your programs and settings.
+unblockTypeUncommon2=This file is not commonly downloaded and may not be safe to open. It may contain a virus or make unexpected changes to your programs and settings.
+unblockTip2=You can search for an alternate download source or try again later.
+unblockButtonOpen=Open
+unblockButtonUnblock=Allow download
+unblockButtonConfirmBlock=Remove file
+
+fileExecutableSecurityWarning=“%S” is an executable file. Executable files may contain viruses or other malicious code that could harm your computer. Use caution when opening this file. Are you sure you want to launch “%S”?
+fileExecutableSecurityWarningTitle=Open Executable File?
+fileExecutableSecurityWarningDontAsk=Don’t ask me this again
+
+# LOCALIZATION NOTE (otherDownloads3):
+# This is displayed in an item at the bottom of the Downloads Panel when
+# there are more downloads than can fit in the list in the panel. Use a
+# semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/Localization_and_Plurals
+otherDownloads3=%1$S file downloading;%1$S files downloading
 
 # LOCALIZATION NOTE (downloadsTitleFiles, downloadsTitlePercent): Semi-colon list of
 # plural forms. See: http://developer.mozilla.org/en/Localization_and_Plurals
 # %1$S number of files; %2$S overall download percent (only for downloadsTitlePercent)
 # %% will appear as a single % sign, so %2$S%% is the percent number plus the % sign
 # examples: 2% of 1 file - Download Manager; 22% of 11 files - Download Manager
 downloadsTitleFiles=%1$S file - Download Manager;%1$S files - Download Manager
 downloadsTitlePercent=%2$S%% of %1$S file - Download Manager;%2$S%% of %1$S files - Download Manager
@@ -62,12 +147,8 @@ sizeSpeed=%1$S (%2$S)
 
 # LOCALIZATION NOTE (statusActive): — is the "em dash" (long dash)
 # %1$S download status; %2$S time remaining
 # example: Paused — 11 hours, 2 minutes remaining
 statusActive=%1$S — %2$S
 
 fromSource=From %S
 toTarget=To %S
-
-fileExecutableSecurityWarning="%S" is an executable file. Executable files may contain viruses or other malicious code that could harm your computer. Use caution when opening this file. Are you sure you want to launch "%S"?
-fileExecutableSecurityWarningTitle=Open Executable File?
-fileExecutableSecurityWarningDontAsk=Don't ask me this again