Bug 1265387 - The default action for uncommon downloads should open the file directly. r=past draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Wed, 20 Apr 2016 15:44:38 +0100
changeset 354289 4f30d59be56c51801f42f9222a35dc86def9c819
parent 354242 19b8851d8d4c19997ecc73960f4de8d90c981c28
child 354290 82c5ba0ba7c51d3db9c0dfa9c09a66bc970d719f
push id16018
push userpaolo.mozmail@amadzone.org
push dateWed, 20 Apr 2016 14:46:53 +0000
reviewerspast
bugs1265387
milestone48.0a1
Bug 1265387 - The default action for uncommon downloads should open the file directly. r=past MozReview-Commit-ID: GrZev8J5MRN
browser/components/downloads/DownloadsCommon.jsm
browser/components/downloads/DownloadsViewUI.jsm
browser/components/downloads/content/allDownloadsViewOverlay.js
browser/components/downloads/content/allDownloadsViewOverlay.xul
browser/components/downloads/content/download.xml
browser/components/downloads/content/downloads.css
browser/components/downloads/content/downloads.js
browser/components/downloads/content/downloadsOverlay.xul
browser/components/downloads/test/browser/browser_confirm_unblock_download.js
browser/locales/en-US/chrome/browser/downloads/downloads.dtd
browser/locales/en-US/chrome/browser/downloads/downloads.properties
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -457,25 +457,25 @@ this.DownloadsCommon = {
     } 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"]
@@ -516,59 +516,93 @@ this.DownloadsCommon = {
       }
     }
   },
 
   /**
    * Displays an alert message box which asks the user if they want to
    * unblock the downloaded file or not.
    *
-   * @param aVerdict
-   *        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.
-   * @param aOwnerWindow
-   *        The window with which this action is associated.
+   * @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.
    */
-  confirmUnblockDownload: Task.async(function* (aVerdict, aOwnerWindow) {
+  confirmUnblockDownload: Task.async(function* ({ verdict, window,
+                                                  dialogType }) {
     let s = DownloadsCommon.strings;
-    let title = s.unblockHeader;
-    let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
-                      (Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1);
-    let type = "";
-    let message = s.unblockTip;
-    let unblockButton = s.unblockButtonContinue;
-    let confirmBlockButton = s.unblockButtonCancel;
+
+    // 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 (aVerdict) {
+    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:
-        type = s.unblockTypeUncommon;
-        buttonFlags += (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
-                       Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
+        message = s.unblockTypeUncommon;
         break;
       case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
-        type = s.unblockTypePotentiallyUnwanted;
-        buttonFlags += (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
-                       Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
+        message = s.unblockTypePotentiallyUnwanted;
         break;
       default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
-        type = s.unblockTypeMalware;
-        buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+        message = s.unblockTypeMalware;
         break;
     }
-
-    if (type) {
-      message = type + "\n\n" + message;
-    }
+    message += "\n\n" + s.unblockTip;
 
     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 onLoad() {
           subj.removeEventListener("DOMContentLoaded", onLoad);
           if (subj.document.documentURI ==
@@ -579,22 +613,20 @@ this.DownloadsCommon = {
               // Change the dialog to use a warning icon.
               dialog.classList.add("alert-dialog");
             }
           }
         });
       }
     });
 
-    // The ordering of the ok/cancel buttons is used this way to allow "cancel"
-    // to have the same result as hitting the ESC or Close button (see bug 345067).
-    let rv = Services.prompt.confirmEx(aOwnerWindow, title, message, buttonFlags,
-                                       unblockButton, null, confirmBlockButton,
-                                       null, {});
-    return ["unblock", "cancel", "confirmBlock"][rv];
+    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);
--- a/browser/components/downloads/DownloadsViewUI.jsm
+++ b/browser/components/downloads/DownloadsViewUI.jsm
@@ -232,17 +232,17 @@ this.DownloadsViewUI.DownloadElementShel
         }
       } else if (this.download.canceled) {
         stateLabel = s.stateCanceled;
       } else if (this.download.error.becauseBlockedByParentalControls) {
         stateLabel = s.stateBlockedParentalControls;
       } else if (this.download.error.becauseBlockedByReputationCheck) {
         switch (this.download.error.reputationCheckVerdict) {
           case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
-            stateLabel = s.blockedUncommon;
+            stateLabel = s.blockedUncommon2;
             break;
           case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
             stateLabel = s.blockedPotentiallyUnwanted;
             break;
           default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
             stateLabel = s.blockedMalware;
             break;
         }
@@ -266,21 +266,28 @@ this.DownloadsViewUI.DownloadElementShel
 
   /**
    * Shows the appropriate unblock dialog based on the verdict, and executes the
    * action selected by the user in the dialog, which may involve unblocking,
    * opening or removing the file.
    *
    * @param window
    *        The window to which the dialog should be anchored.
+   * @param dialogType
+   *        Can be "unblock", "chooseUnblock", or "chooseOpen".
    */
-  confirmUnblock(window) {
-    let verdict = this.download.error.reputationCheckVerdict;
-    DownloadsCommon.confirmUnblockDownload(verdict, window).then(action => {
-      if (action == "unblock") {
+  confirmUnblock(window, dialogType) {
+    DownloadsCommon.confirmUnblockDownload({
+      verdict: this.download.error.reputationCheckVerdict,
+      window,
+      dialogType,
+    }).then(action => {
+      if (action == "open") {
+        return this.download.unblock().then(() => this.downloadsCmd_open());
+      } else if (action == "unblock") {
         return this.download.unblock();
       } else if (action == "confirmBlock") {
         return this.download.confirmBlock();
       }
     }).catch(Cu.reportError);
   },
 
   /**
@@ -318,16 +325,18 @@ this.DownloadsViewUI.DownloadElementShel
     switch (aCommand) {
       case "downloadsCmd_retry":
         return this.download.canceled || this.download.error;
       case "downloadsCmd_pauseResume":
         return this.download.hasPartialData && !this.download.error;
       case "downloadsCmd_openReferrer":
         return !!this.download.source.referrer;
       case "downloadsCmd_confirmBlock":
+      case "downloadsCmd_chooseUnblock":
+      case "downloadsCmd_chooseOpen":
       case "downloadsCmd_unblock":
         return this.download.hasBlockedData;
     }
     return false;
   },
 
   downloadsCmd_cancel() {
     // This is the correct way to avoid race conditions when cancelling.
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -372,17 +372,25 @@ HistoryDownloadElementShell.prototype = 
     }
     if (this._historyDownload) {
       let uri = NetUtil.newURI(this.download.source.url);
       PlacesUtils.bhistory.removePage(uri);
     }
   },
 
   downloadsCmd_unblock() {
-    this.confirmUnblock(window);
+    this.confirmUnblock(window, "unblock");
+  },
+
+  downloadsCmd_chooseUnblock() {
+    this.confirmUnblock(window, "chooseUnblock");
+  },
+
+  downloadsCmd_chooseOpen() {
+    this.confirmUnblock(window, "chooseOpen");
   },
 
   // Returns whether or not the download handled by this shell should
   // show up in the search results for the given term.  Both the display
   // name for the download and the url are searched.
   matchesSearchTerm(aTerm) {
     if (!aTerm) {
       return true;
--- a/browser/components/downloads/content/allDownloadsViewOverlay.xul
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.xul
@@ -58,16 +58,20 @@
               events="focus,select,contextmenu"
               oncommandupdate="goUpdateDownloadCommands();">
     <command id="downloadsCmd_pauseResume"
              oncommand="goDoCommand('downloadsCmd_pauseResume')"/>
     <command id="downloadsCmd_cancel"
              oncommand="goDoCommand('downloadsCmd_cancel')"/>
     <command id="downloadsCmd_unblock"
              oncommand="goDoCommand('downloadsCmd_unblock')"/>
+    <command id="downloadsCmd_chooseUnblock"
+             oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/>
+    <command id="downloadsCmd_chooseOpen"
+             oncommand="goDoCommand('downloadsCmd_chooseOpen')"/>
     <command id="downloadsCmd_confirmBlock"
              oncommand="goDoCommand('downloadsCmd_confirmBlock')"/>
     <command id="downloadsCmd_open"
              oncommand="goDoCommand('downloadsCmd_open')"/>
     <command id="downloadsCmd_show"
              oncommand="goDoCommand('downloadsCmd_show')"/>
     <command id="downloadsCmd_retry"
              oncommand="goDoCommand('downloadsCmd_retry')"/>
@@ -87,18 +91,18 @@
               label="&cmd.resume.label;"
               accesskey="&cmd.resume.accesskey;"/>
     <menuitem command="downloadsCmd_cancel"
               class="downloadCancelMenuItem"
               label="&cmd.cancel.label;"
               accesskey="&cmd.cancel.accesskey;"/>
     <menuitem command="downloadsCmd_unblock"
               class="downloadUnblockMenuItem"
-              label="&cmd.unblock.label;"
-              accesskey="&cmd.unblock.accesskey;"/>
+              label="&cmd.unblock2.label;"
+              accesskey="&cmd.unblock2.accesskey;"/>
     <menuitem command="cmd_delete"
               class="downloadRemoveFromHistoryMenuItem"
               label="&cmd.removeFromHistory.label;"
               accesskey="&cmd.removeFromHistory.accesskey;"/>
     <menuitem command="downloadsCmd_show"
               class="downloadShowMenuItem"
 #ifdef XP_MACOSX
               label="&cmd.showMac.label;"
--- a/browser/components/downloads/content/download.xml
+++ b/browser/components/downloads/content/download.xml
@@ -58,19 +58,22 @@
                     tooltiptext="&cmd.showMac.label;"
 #else
                     tooltiptext="&cmd.show.label;"
 #endif
                     oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_show');"/>
         <xul:button class="downloadButton downloadConfirmBlock downloadIconCancel"
                     tooltiptext="&cmd.removeFile.label;"
                     oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_confirmBlock');"/>
-        <xul:button class="downloadButton downloadUnblock downloadIconShow"
-                    tooltiptext="&cmd.unblock.label;"
-                    oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_unblock');"/>
+        <xul:button class="downloadButton downloadChooseUnblock downloadIconShow"
+                    tooltiptext="&cmd.chooseUnblock.label;"
+                    oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseUnblock');"/>
+        <xul:button class="downloadButton downloadChooseOpen downloadIconShow"
+                    tooltiptext="&cmd.chooseOpen.label;"
+                    oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseOpen');"/>
       </xul:stack>
     </content>
   </binding>
 
   <binding id="download-toolbarbutton"
            extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
     <content>
       <children />
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -122,23 +122,32 @@ richlistitem.download button {
 .download-state:not(          [state="8"]  /* Blocked (dirty)    */)
                                            .downloadConfirmBlock,
 .download-state[state="8"]:not(.temporary-block)
                                            .downloadConfirmBlock,
 .download-state[state="8"].temporary-block:not([verdict="Malware"])
                                            .downloadConfirmBlock,
 
 /* Blocked (dirty) downloads that have not been confirmed and
-   have temporary data, for cases other than Malware. */
+   have temporary data, for the Potentially Unwanted case. */
 .download-state:not(          [state="8"]  /* Blocked (dirty)    */)
-                                           .downloadUnblock,
+                                           .downloadChooseUnblock,
 .download-state[state="8"]:not(.temporary-block)
-                                           .downloadUnblock,
-.download-state[state="8"].temporary-block[verdict="Malware"]
-                                           .downloadUnblock,
+                                           .downloadChooseUnblock,
+.download-state[state="8"].temporary-block:not([verdict="PotentiallyUnwanted"])
+                                           .downloadChooseUnblock,
+
+/* Blocked (dirty) downloads that have not been confirmed and
+   have temporary data, for the Uncommon case. */
+.download-state:not(          [state="8"]  /* Blocked (dirty)    */)
+                                           .downloadChooseOpen,
+.download-state[state="8"]:not(.temporary-block)
+                                           .downloadChooseOpen,
+.download-state[state="8"].temporary-block:not([verdict="Uncommon"])
+                                           .downloadChooseOpen,
 
 .download-state:not(:-moz-any([state="2"], /* Failed             */
                               [state="3"]) /* Canceled           */)
                                            .downloadRetry,
 
 .download-state:not(          [state="1"]  /* Finished           */)
                                            .downloadShow
 
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -1087,17 +1087,27 @@ DownloadsViewItem.prototype = {
   cmd_delete() {
     DownloadsCommon.removeAndFinalizeDownload(this.download);
     PlacesUtils.bhistory.removePage(
                            NetUtil.newURI(this.download.source.url));
   },
 
   downloadsCmd_unblock() {
     DownloadsPanel.hidePanel();
-    this.confirmUnblock(window);
+    this.confirmUnblock(window, "unblock");
+  },
+
+  downloadsCmd_chooseUnblock() {
+    DownloadsPanel.hidePanel();
+    this.confirmUnblock(window, "chooseUnblock");
+  },
+
+  downloadsCmd_chooseOpen() {
+    DownloadsPanel.hidePanel();
+    this.confirmUnblock(window, "chooseOpen");
   },
 
   downloadsCmd_open() {
     this.download.launch().catch(Cu.reportError);
 
     // We explicitly close the panel here to give the user the feedback that
     // their click has been received, and we're handling the action.
     // Otherwise, we'd have to wait for the file-type handler to execute
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -18,16 +18,20 @@
     <command id="downloadsCmd_doDefault"
              oncommand="goDoCommand('downloadsCmd_doDefault')"/>
     <command id="downloadsCmd_pauseResume"
              oncommand="goDoCommand('downloadsCmd_pauseResume')"/>
     <command id="downloadsCmd_cancel"
              oncommand="goDoCommand('downloadsCmd_cancel')"/>
     <command id="downloadsCmd_unblock"
              oncommand="goDoCommand('downloadsCmd_unblock')"/>
+    <command id="downloadsCmd_chooseUnblock"
+             oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/>
+    <command id="downloadsCmd_chooseOpen"
+             oncommand="goDoCommand('downloadsCmd_chooseOpen')"/>
     <command id="downloadsCmd_confirmBlock"
              oncommand="goDoCommand('downloadsCmd_confirmBlock')"/>
     <command id="downloadsCmd_open"
              oncommand="goDoCommand('downloadsCmd_open')"/>
     <command id="downloadsCmd_show"
              oncommand="goDoCommand('downloadsCmd_show')"/>
     <command id="downloadsCmd_retry"
              oncommand="goDoCommand('downloadsCmd_retry')"/>
@@ -66,18 +70,18 @@
                   label="&cmd.resume.label;"
                   accesskey="&cmd.resume.accesskey;"/>
         <menuitem command="downloadsCmd_cancel"
                   class="downloadCancelMenuItem"
                   label="&cmd.cancel.label;"
                   accesskey="&cmd.cancel.accesskey;"/>
         <menuitem command="downloadsCmd_unblock"
                   class="downloadUnblockMenuItem"
-                  label="&cmd.unblock.label;"
-                  accesskey="&cmd.unblock.accesskey;"/>
+                  label="&cmd.unblock2.label;"
+                  accesskey="&cmd.unblock2.accesskey;"/>
         <menuitem command="cmd_delete"
                   class="downloadRemoveFromHistoryMenuItem"
                   label="&cmd.removeFromHistory.label;"
                   accesskey="&cmd.removeFromHistory.accesskey;"/>
         <menuitem command="downloadsCmd_show"
                   class="downloadShowMenuItem"
 #ifdef XP_MACOSX
                   label="&cmd.showMac.label;"
--- a/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
+++ b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
@@ -26,21 +26,91 @@ function addDialogOpenObserver(buttonAct
           let doc = subj.document.documentElement;
           doc.getButton(buttonAction).click();
         }
       });
     }
   });
 }
 
-add_task(function* test_confirm_unblock_dialog_unblock() {
-  addDialogOpenObserver("accept");
-  let result = yield DownloadsCommon.confirmUnblockDownload(Downloads.Error.BLOCK_VERDICT_MALWARE,
-                                                            window);
-  is(result, "unblock");
+function* assertDialogResult({ args, buttonToClick, expectedResult }) {
+  addDialogOpenObserver(buttonToClick);
+  is(yield DownloadsCommon.confirmUnblockDownload(args), expectedResult);
+}
+
+/**
+ * Tests the "unblock" dialog, for each of the possible verdicts.
+ */
+add_task(function* test_unblock_dialog_unblock() {
+  for (let verdict of [Downloads.Error.BLOCK_VERDICT_MALWARE,
+                       Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+                       Downloads.Error.BLOCK_VERDICT_UNCOMMON]) {
+    let args = { verdict, window, dialogType: "unblock" };
+
+    // Test both buttons.
+    yield assertDialogResult({
+      args,
+      buttonToClick: "accept",
+      expectedResult: "unblock",
+    });
+    yield assertDialogResult({
+      args,
+      buttonToClick: "cancel",
+      expectedResult: "cancel",
+    });
+  }
 });
 
-add_task(function* test_confirm_unblock_dialog_keep_safe() {
-  addDialogOpenObserver("cancel");
-  let result = yield DownloadsCommon.confirmUnblockDownload(Downloads.Error.BLOCK_VERDICT_MALWARE,
-                                                            window);
-  is(result, "cancel");
+/**
+ * Tests the "chooseUnblock" dialog for potentially unwanted downloads.
+ */
+add_task(function* test_chooseUnblock_dialog() {
+  let args = {
+    verdict: Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+    window,
+    dialogType: "chooseUnblock",
+  };
+
+  // Test each of the three buttons.
+  yield assertDialogResult({
+    args,
+    buttonToClick: "accept",
+    expectedResult: "unblock",
+  });
+  yield assertDialogResult({
+    args,
+    buttonToClick: "cancel",
+    expectedResult: "cancel",
+  });
+  yield assertDialogResult({
+    args,
+    buttonToClick: "extra1",
+    expectedResult: "confirmBlock",
+  });
 });
+
+/**
+ * Tests the "chooseOpen" dialog for uncommon downloads.
+ */
+add_task(function* test_chooseOpen_dialog() {
+  let args = {
+    verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+    window,
+    dialogType: "chooseOpen",
+  };
+
+  // Test each of the three buttons.
+  yield assertDialogResult({
+    args,
+    buttonToClick: "accept",
+    expectedResult: "open",
+  });
+  yield assertDialogResult({
+    args,
+    buttonToClick: "cancel",
+    expectedResult: "cancel",
+  });
+  yield assertDialogResult({
+    args,
+    buttonToClick: "extra1",
+    expectedResult: "confirmBlock",
+  });
+});
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
@@ -61,28 +61,37 @@
 <!ENTITY cmd.copyDownloadLink.label       "Copy Download Link">
 <!ENTITY cmd.copyDownloadLink.accesskey   "L">
 <!ENTITY cmd.removeFromHistory.label      "Remove From History">
 <!ENTITY cmd.removeFromHistory.accesskey  "e">
 <!ENTITY cmd.clearList.label              "Clear List">
 <!ENTITY cmd.clearList.accesskey          "a">
 <!ENTITY cmd.clearDownloads.label         "Clear Downloads">
 <!ENTITY cmd.clearDownloads.accesskey     "D">
-<!-- LOCALIZATION NOTE (cmd.unblock.label):
-     This command may be shown in the context menu, as a menu button item, or as
-     a text link when malware or potentially unwanted downloads are blocked.
+<!-- LOCALIZATION NOTE (cmd.unblock2.label):
+     This command is shown in the context menu when downloads are blocked.
      -->
-<!ENTITY cmd.unblock.label                "Unblock">
-<!ENTITY cmd.unblock.accesskey            "U">
+<!ENTITY cmd.unblock2.label               "Allow Download">
+<!ENTITY cmd.unblock2.accesskey           "o">
 <!-- LOCALIZATION NOTE (cmd.removeFile.label):
-     This command may be shown in the context menu or as a menu button label
-     when malware or potentially unwanted downloads are blocked.
+     This is the tooltip of the action button shown when malware is blocked.
      -->
 <!ENTITY cmd.removeFile.label             "Remove File">
-<!ENTITY cmd.removeFile.accesskey         "m">
+<!-- LOCALIZATION NOTE (cmd.chooseUnblock.tooltip):
+     This is the tooltip of the action button shown when potentially unwanted
+     downloads are blocked. This opens a dialog where the user can choose
+     whether to unblock or remove the download. Removing is the default option.
+     -->
+<!ENTITY cmd.chooseUnblock.label          "Remove File or Allow Download">
+<!-- LOCALIZATION NOTE (cmd.chooseOpen.tooltip):
+     This is the tooltip of the action button shown when uncommon downloads are
+     blocked.This opens a dialog where the user can choose whether to open the
+     file or remove the download. Opening is the default option.
+     -->
+<!ENTITY cmd.chooseOpen.label             "Open or Remove File">
 
 <!-- LOCALIZATION NOTE (blocked.label):
      Shown as a tag before the file name for some types of blocked downloads.
      Note: This string doesn't exist in the UI yet.  See bug 1053890.
      -->
 <!ENTITY blocked.label                    "BLOCKED">
 
 <!-- LOCALIZATION NOTE (learnMore.label):
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.properties
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.properties
@@ -34,41 +34,43 @@ stateBlockedParentalControls=Blocked by 
 # 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,
-#                    blockedUncommon):
+#                    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.
-blockedUncommon=This file may not be safe to open.
+blockedUncommon2=This file is not commonly downloaded.
 
-# LOCALIZATION NOTE (unblockHeader, unblockTypeMalware,
-#                    unblockTypePotentiallyUnwanted, unblockTypeUncommon,
-#                    unblockTip, unblockButtonContinue, unblockButtonCancel):
+# LOCALIZATION NOTE (unblockHeaderUnblock, unblockHeaderOpen,
+#                    unblockTypeMalware, unblockTypePotentiallyUnwanted,
+#                    unblockTypeUncommon, unblockTip, 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.
-# Note: These strings don't exist in the UI yet.  See bug 1053890.
-unblockHeader=Are you sure you want to unblock this file?
+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.
 unblockTypePotentiallyUnwanted=This file, disguised as a helpful download, will make unexpected changes to your programs and settings.
 unblockTypeUncommon=This file has been downloaded from an unfamiliar and potentially dangerous website and may not be safe to open.
 unblockTip=You can search for an alternate download source or try to download the file again later.
-unblockButtonContinue=Unblock anyway
-unblockButtonCancel=Keep me safe
+unblockButtonOpen=Open
+unblockButtonUnblock=Allow download
+unblockButtonConfirmBlock=Remove file
 
 # LOCALIZATION NOTE (sizeWithUnits):
 # %1$S is replaced with the size number, and %2$S with the measurement unit.
 sizeWithUnits=%1$S %2$S
 sizeUnknown=Unknown size
 
 # LOCALIZATION NOTE (shortTimeLeftSeconds, shortTimeLeftMinutes,
 #                    shortTimeLeftHours, shortTimeLeftDays):