Bug 1265387 - The default action for uncommon downloads should open the file directly. r=past
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Wed, 20 Apr 2016 15:44:38 +0100
changeset 294336 a850056f05279162055f782757d4577df9ca8b85
parent 294335 9ad67a907cae9644493ecbabae4529602ca3fa72
child 294337 5344f269671b9cb2cebccfcb0cc6667053c77279
push id75513
push userkwierso@gmail.com
push dateThu, 21 Apr 2016 22:02:48 +0000
treeherdermozilla-inbound@3d46eafd05b9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs1265387
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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):