Bug 1068660 - Add confirmation dialog to unblock downloads. r=Paolo r=MattN
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -62,16 +62,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
"resource:///modules/DownloadsLogger.jsm");
const nsIDM = Ci.nsIDownloadManager;
const kDownloadsStringBundleUrl =
"chrome://browser/locale/downloads/downloads.properties";
@@ -138,16 +140,23 @@ PrefObserver.register({
////////////////////////////////////////////////////////////////////////////////
//// 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.
*/
this.DownloadsCommon = {
+ /**
+ * Constants with the different types of unblock messages.
+ */
+ BLOCK_VERDICT_MALWARE: "Malware",
+ BLOCK_VERDICT_POTENTIALLY_UNWANTED: "PotentiallyUnwanted",
+ BLOCK_VERDICT_UNCOMMON: "Uncommon",
+
log: function DC_log(...aMessageArgs) {
delete this.log;
this.log = function DC_log(...aMessageArgs) {
if (!PrefObserver.debug) {
return;
}
DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs);
}
@@ -506,17 +515,79 @@ this.DownloadsCommon = {
// If launch also fails (probably because it's not implemented), let
// the OS handler try to open the parent.
Cc["@mozilla.org/uriloader/external-protocol-service;1"]
.getService(Ci.nsIExternalProtocolService)
.loadUrl(NetUtil.newURI(parent));
}
}
}
- }
+ },
+
+ /**
+ * Displays an alert message box which asks the user if they want to
+ * unblock the downloaded file or not.
+ *
+ * @param aType
+ * The type of malware the downloaded file contains.
+ * @param aOwnerWindow
+ * The window with which this action is associated.
+ *
+ * @return True to unblock the file, false to keep the user safe and
+ * cancel the operation.
+ */
+ confirmUnblockDownload: Task.async(function* DP_confirmUnblockDownload(aType, aOwnerWindow) {
+ 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_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1);
+ let type = "";
+ let message = s.unblockTip;
+ let okButton = s.unblockButtonContinue;
+ let cancelButton = s.unblockButtonCancel;
+
+ switch (aType) {
+ case this.BLOCK_VERDICT_MALWARE:
+ type = s.unblockTypeMalware;
+ break;
+ case this.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
+ type = s.unblockTypePotentiallyUnwanted;
+ break;
+ case this.BLOCK_VERDICT_UNCOMMON:
+ type = s.unblockTypeUncommon;
+ break;
+ }
+
+ if (type) {
+ message = type + "\n\n" + message;
+ }
+
+ 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 ==
+ "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");
+ }
+ }
+ });
+ }
+ });
+
+ let rv = Services.prompt.confirmEx(aOwnerWindow, title, message, buttonFlags,
+ cancelButton, okButton, null, null, {});
+ return (rv == 1);
+ }),
};
/**
* Returns true if we are executing on Windows Vista or a later version.
*/
XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () {
let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
if (os != "WINNT") {
--- a/browser/components/downloads/test/browser/browser.ini
+++ b/browser/components/downloads/test/browser/browser.ini
@@ -2,8 +2,9 @@
support-files = head.js
[browser_basic_functionality.js]
skip-if = buildapp == "mulet" || e10s
[browser_first_download_panel.js]
skip-if = os == "linux" # Bug 949434
[browser_overflow_anchor.js]
skip-if = os == "linux" # Bug 952422
+[browser_confirm_unblock_download.js]
--- a/browser/components/downloads/test/browser/browser_basic_functionality.js
+++ b/browser/components/downloads/test/browser/browser_basic_functionality.js
@@ -1,57 +1,55 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
+registerCleanupFunction(function*() {
+ yield task_resetState();
+});
+
/**
* Make sure the downloads panel can display items in the right order and
* contains the expected data.
*/
-function test_task()
-{
+add_task(function* test_basic_functionality() {
// Display one of each download state.
const DownloadData = [
{ state: nsIDM.DOWNLOAD_NOTSTARTED },
{ state: nsIDM.DOWNLOAD_PAUSED },
{ state: nsIDM.DOWNLOAD_FINISHED },
{ state: nsIDM.DOWNLOAD_FAILED },
{ state: nsIDM.DOWNLOAD_CANCELED },
];
- try {
- // Wait for focus first
- yield promiseFocus();
+ // Wait for focus first
+ yield promiseFocus();
- // Ensure that state is reset in case previous tests didn't finish.
- yield task_resetState();
+ // Ensure that state is reset in case previous tests didn't finish.
+ yield task_resetState();
- // For testing purposes, show all the download items at once.
- var originalCountLimit = DownloadsView.kItemCountLimit;
- DownloadsView.kItemCountLimit = DownloadData.length;
- registerCleanupFunction(function () {
- DownloadsView.kItemCountLimit = originalCountLimit;
- });
+ // For testing purposes, show all the download items at once.
+ var originalCountLimit = DownloadsView.kItemCountLimit;
+ DownloadsView.kItemCountLimit = DownloadData.length;
+ registerCleanupFunction(function () {
+ DownloadsView.kItemCountLimit = originalCountLimit;
+ });
- // Populate the downloads database with the data required by this test.
- yield task_addDownloads(DownloadData);
+ // Populate the downloads database with the data required by this test.
+ yield task_addDownloads(DownloadData);
- // Open the user interface and wait for data to be fully loaded.
- yield task_openPanel();
+ // Open the user interface and wait for data to be fully loaded.
+ yield task_openPanel();
- // Test item data and count. This also tests the ordering of the display.
- let richlistbox = document.getElementById("downloadsListBox");
-/* disabled for failing intermittently (bug 767828)
+ // Test item data and count. This also tests the ordering of the display.
+ let richlistbox = document.getElementById("downloadsListBox");
+ /* disabled for failing intermittently (bug 767828)
is(richlistbox.children.length, DownloadData.length,
"There is the correct number of richlistitems");
-*/
- let itemCount = richlistbox.children.length;
- for (let i = 0; i < itemCount; i++) {
- let element = richlistbox.children[itemCount - i - 1];
- let dataItem = new DownloadsViewItemController(element).dataItem;
- is(dataItem.state, DownloadData[i].state, "Download states match up");
- }
- } finally {
- // Clean up when the test finishes.
- yield task_resetState();
+ */
+ let itemCount = richlistbox.children.length;
+ for (let i = 0; i < itemCount; i++) {
+ let element = richlistbox.children[itemCount - i - 1];
+ let dataItem = new DownloadsViewItemController(element).dataItem;
+ is(dataItem.state, DownloadData[i].state, "Download states match up");
}
-}
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the dialog which allows the user to unblock a downloaded file.
+
+registerCleanupFunction(() => {});
+
+function addDialogOpenObserver(buttonAction) {
+ Services.ww.registerNotification(function onOpen(subj, topic, data) {
+ if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+ // The test listens for the "load" event which guarantees that the alert
+ // class has already been added (it is added when "DOMContentLoaded" is
+ // fired).
+ subj.addEventListener("load", function onLoad() {
+ subj.removeEventListener("load", onLoad);
+ if (subj.document.documentURI ==
+ "chrome://global/content/commonDialog.xul") {
+ Services.ww.unregisterNotification(onOpen);
+
+ let dialog = subj.document.getElementById("commonDialog");
+ ok(dialog.classList.contains("alert-dialog"),
+ "The dialog element should contain an alert class.");
+
+ let doc = subj.document.documentElement;
+ doc.getButton(buttonAction).click();
+ }
+ });
+ }
+ });
+}
+
+add_task(function* test_confirm_unblock_dialog_unblock() {
+ addDialogOpenObserver("cancel");
+ let result = yield DownloadsCommon.confirmUnblockDownload(DownloadsCommon.UNBLOCK_MALWARE,
+ window);
+ ok(result, "Should return true when the user clicks on `Unblock` button.");
+});
+
+add_task(function* test_confirm_unblock_dialog_keep_safe() {
+ addDialogOpenObserver("accept");
+ let result = yield DownloadsCommon.confirmUnblockDownload(DownloadsCommon.UNBLOCK_MALWARE,
+ window);
+ ok(!result, "Should return false when the user clicks on `Keep me safe` button.");
+});
--- a/browser/components/downloads/test/browser/browser_first_download_panel.js
+++ b/browser/components/downloads/test/browser/browser_first_download_panel.js
@@ -3,63 +3,55 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure the downloads panel only opens automatically on the first
* download it notices. All subsequent downloads, even across sessions, should
* not open the panel automatically.
*/
-function test_task()
-{
+add_task(function* test_first_download_panel() {
// Clear the download panel has shown preference first as this test is used to
// verify this preference's behaviour.
- let oldPrefValue = true;
- try {
- oldPrefValue = Services.prefs.getBoolPref("browser.download.panel.shown");
- } catch(ex) { }
+ let oldPrefValue = Services.prefs.getBoolPref("browser.download.panel.shown");
Services.prefs.setBoolPref("browser.download.panel.shown", false);
- try {
- // Ensure that state is reset in case previous tests didn't finish.
+ registerCleanupFunction(function*() {
+ // Clean up when the test finishes.
yield task_resetState();
- // With this set to false, we should automatically open the panel the first
- // time a download is started.
- DownloadsCommon.getData(window).panelHasShownBefore = false;
-
- let promise = promisePanelOpened();
- DownloadsCommon.getData(window)._notifyDownloadEvent("start");
- yield promise;
-
- // If we got here, that means the panel opened.
- DownloadsPanel.hidePanel();
-
- ok(DownloadsCommon.getData(window).panelHasShownBefore,
- "Should have recorded that the panel was opened on a download.")
-
- // Next, make sure that if we start another download, we don't open the
- // panel automatically.
- let originalOnPopupShown = DownloadsPanel.onPopupShown;
- DownloadsPanel.onPopupShown = function () {
- originalOnPopupShown.apply(this, arguments);
- ok(false, "Should not have opened the downloads panel.");
- };
-
- try {
- DownloadsCommon.getData(window)._notifyDownloadEvent("start");
-
- // Wait 2 seconds to ensure that the panel does not open.
- let deferTimeout = Promise.defer();
- setTimeout(deferTimeout.resolve, 2000);
- yield deferTimeout.promise;
- } finally {
- DownloadsPanel.onPopupShown = originalOnPopupShown;
- }
- } finally {
- // Clean up when the test finishes.
- yield task_resetState();
// Set the preference instead of clearing it afterwards to ensure the
// right value is used no matter what the default was. This ensures the
// panel doesn't appear and affect other tests.
Services.prefs.setBoolPref("browser.download.panel.shown", oldPrefValue);
- }
-}
+ });
+
+ // Ensure that state is reset in case previous tests didn't finish.
+ yield task_resetState();
+
+ // With this set to false, we should automatically open the panel the first
+ // time a download is started.
+ DownloadsCommon.getData(window).panelHasShownBefore = false;
+
+ let promise = promisePanelOpened();
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+ yield promise;
+
+ // If we got here, that means the panel opened.
+ DownloadsPanel.hidePanel();
+
+ ok(DownloadsCommon.getData(window).panelHasShownBefore,
+ "Should have recorded that the panel was opened on a download.")
+
+ // Next, make sure that if we start another download, we don't open the
+ // panel automatically.
+ let originalOnPopupShown = DownloadsPanel.onPopupShown;
+ DownloadsPanel.onPopupShown = function () {
+ originalOnPopupShown.apply(this, arguments);
+ ok(false, "Should not have opened the downloads panel.");
+ };
+
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+
+ // Wait 2 seconds to ensure that the panel does not open.
+ yield new Promise(resolve => setTimeout(resolve, 2000));
+ DownloadsPanel.onPopupShown = originalOnPopupShown;
+});
--- a/browser/components/downloads/test/browser/browser_overflow_anchor.js
+++ b/browser/components/downloads/test/browser/browser_overflow_anchor.js
@@ -1,74 +1,74 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
+registerCleanupFunction(function*() {
+ // Clean up when the test finishes.
+ yield task_resetState();
+});
+
/**
* Make sure the downloads button and indicator overflows into the nav-bar
* chevron properly, and then when those buttons are clicked in the overflow
* panel that the downloads panel anchors to the chevron.
*/
-function test_task() {
- try {
- // Ensure that state is reset in case previous tests didn't finish.
- yield task_resetState();
+add_task(function* test_overflow_anchor() {
+ // Ensure that state is reset in case previous tests didn't finish.
+ yield task_resetState();
- // Record the original width of the window so we can put it back when
- // this test finishes.
- let oldWidth = window.outerWidth;
+ // Record the original width of the window so we can put it back when
+ // this test finishes.
+ let oldWidth = window.outerWidth;
- // The downloads button should not be overflowed to begin with.
- let button = CustomizableUI.getWidget("downloads-button")
- .forWindow(window);
- ok(!button.overflowed, "Downloads button should not be overflowed.");
+ // The downloads button should not be overflowed to begin with.
+ let button = CustomizableUI.getWidget("downloads-button")
+ .forWindow(window);
+ ok(!button.overflowed, "Downloads button should not be overflowed.");
- // Hack - we lock the size of the default flex-y items in the nav-bar,
- // namely, the URL and search inputs. That way we can resize the
- // window without worrying about them flexing.
- const kFlexyItems = ["urlbar-container", "search-container"];
- registerCleanupFunction(() => unlockWidth(kFlexyItems));
- lockWidth(kFlexyItems);
+ // Hack - we lock the size of the default flex-y items in the nav-bar,
+ // namely, the URL and search inputs. That way we can resize the
+ // window without worrying about them flexing.
+ const kFlexyItems = ["urlbar-container", "search-container"];
+ registerCleanupFunction(() => unlockWidth(kFlexyItems));
+ lockWidth(kFlexyItems);
- // Resize the window to half of its original size. That should
- // be enough to overflow the downloads button.
- window.resizeTo(oldWidth / 2, window.outerHeight);
- yield waitForOverflowed(button, true);
+ // Resize the window to half of its original size. That should
+ // be enough to overflow the downloads button.
+ window.resizeTo(oldWidth / 2, window.outerHeight);
+ yield waitForOverflowed(button, true);
- let promise = promisePanelOpened();
- button.node.doCommand();
- yield promise;
-
- let panel = DownloadsPanel.panel;
- let chevron = document.getElementById("nav-bar-overflow-button");
- is(panel.anchorNode, chevron, "Panel should be anchored to the chevron.");
+ let promise = promisePanelOpened();
+ button.node.doCommand();
+ yield promise;
- DownloadsPanel.hidePanel();
+ let panel = DownloadsPanel.panel;
+ let chevron = document.getElementById("nav-bar-overflow-button");
+ is(panel.anchorNode, chevron, "Panel should be anchored to the chevron.");
- // Unlock the widths on the flex-y items.
- unlockWidth(kFlexyItems);
+ DownloadsPanel.hidePanel();
- // Put the window back to its original dimensions.
- window.resizeTo(oldWidth, window.outerHeight);
+ // Unlock the widths on the flex-y items.
+ unlockWidth(kFlexyItems);
- // The downloads button should eventually be un-overflowed.
- yield waitForOverflowed(button, false);
+ // Put the window back to its original dimensions.
+ window.resizeTo(oldWidth, window.outerHeight);
- // Now try opening the panel again.
- promise = promisePanelOpened();
- button.node.doCommand();
- yield promise;
+ // The downloads button should eventually be un-overflowed.
+ yield waitForOverflowed(button, false);
- is(panel.anchorNode.id, "downloads-indicator-anchor");
+ // Now try opening the panel again.
+ promise = promisePanelOpened();
+ button.node.doCommand();
+ yield promise;
- DownloadsPanel.hidePanel();
- } finally {
- // Clean up when the test finishes.
- yield task_resetState();
- }
-}
+ is(panel.anchorNode.id, "downloads-indicator-anchor");
+
+ DownloadsPanel.hidePanel();
+});
/**
* For some node IDs, finds the nodes and sets their min-width's to their
* current width, preventing them from flex-shrinking.
*
* @param aItemIDs an array of item IDs to set min-width on.
*/
function lockWidth(aItemIDs) {
--- a/browser/components/downloads/test/browser/head.js
+++ b/browser/components/downloads/test/browser/head.js
@@ -24,25 +24,16 @@ const nsIDM = Ci.nsIDownloadManager;
let gTestTargetFile = FileUtils.getFile("TmpD", ["dm-ui-test.file"]);
gTestTargetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
registerCleanupFunction(function () {
gTestTargetFile.remove(false);
});
////////////////////////////////////////////////////////////////////////////////
-//// Infrastructure
-
-function test()
-{
- waitForExplicitFinish();
- Task.spawn(test_task).then(null, ex => ok(false, ex)).then(finish);
-}
-
-////////////////////////////////////////////////////////////////////////////////
//// Asynchronous support subroutines
function promiseFocus()
{
let deferred = Promise.defer();
waitForFocus(deferred.resolve);
return deferred.promise;
}
--- a/toolkit/components/prompts/content/commonDialog.xul
+++ b/toolkit/components/prompts/content/commonDialog.xul
@@ -8,26 +8,30 @@
<?xml-stylesheet href="chrome://global/content/commonDialog.css" type="text/css"?>
<?xml-stylesheet href="chrome://global/skin/commonDialog.css" type="text/css"?>
<!DOCTYPE dialog SYSTEM "chrome://global/locale/commonDialog.dtd">
<dialog id="commonDialog"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
aria-describedby="info.body"
- onload="commonDialogOnLoad();"
onunload="commonDialogOnUnload();"
ondialogaccept="Dialog.onButton0(); return true;"
ondialogcancel="Dialog.onButton1(); return true;"
ondialogextra1="Dialog.onButton2(); window.close();"
ondialogextra2="Dialog.onButton3(); window.close();"
buttonpack="center">
<script type="application/javascript" src="chrome://global/content/commonDialog.js"/>
<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript">
+ document.addEventListener("DOMContentLoaded", function() {
+ commonDialogOnLoad();
+ });
+ </script>
<commandset id="selectEditMenuItems">
<command id="cmd_copy" oncommand="goDoCommand('cmd_copy')" disabled="true"/>
<command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')"/>
</commandset>
<popupset id="contentAreaContextSet">
<menupopup id="contentAreaContextMenu"
--- a/toolkit/themes/linux/global/global.css
+++ b/toolkit/themes/linux/global/global.css
@@ -55,16 +55,17 @@ window.dialog {
}
/* ::::: alert icons :::::*/
.message-icon {
list-style-image: url("moz-icon://stock/gtk-dialog-info?size=dialog");
}
+.alert-dialog #info\.icon,
.alert-icon {
list-style-image: url("moz-icon://stock/gtk-dialog-warning?size=dialog");
}
.error-icon {
list-style-image: url("moz-icon://stock/gtk-dialog-error?size=dialog");
}
--- a/toolkit/themes/osx/global/global.css
+++ b/toolkit/themes/osx/global/global.css
@@ -73,16 +73,17 @@ window.dialog {
margin: 6px;
-moz-margin-end: 20px;
}
.message-icon {
list-style-image: url("chrome://global/skin/icons/information-64.png");
}
+.alert-dialog #info\.icon,
.alert-icon {
list-style-image: url("chrome://global/skin/icons/warning-64.png");
}
.error-icon {
list-style-image: url("chrome://global/skin/icons/error-64.png");
}
--- a/toolkit/themes/windows/global/global.css
+++ b/toolkit/themes/windows/global/global.css
@@ -51,16 +51,17 @@ window.dialog {
width: 32px;
height: 32px;
}
.message-icon {
list-style-image: url("chrome://global/skin/icons/information-32.png");
}
+.alert-dialog #info\.icon,
.alert-icon {
list-style-image: url("chrome://global/skin/icons/Warning.png");
}
.error-icon {
list-style-image: url("chrome://global/skin/icons/Error.png");
}