Bug 1116385. r=Mossop

This commit is contained in:
Felipe Gomes 2015-10-13 20:32:25 -03:00
parent 4b72a507bb
commit ad9d2a6598
6 changed files with 248 additions and 75 deletions

View File

@ -10,6 +10,9 @@ const Cc = Components.classes;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EnableDelayHelper",
"resource://gre/modules/SharedPromptUtils.jsm");
this.CommonDialog = function CommonDialog(args, ui) {
@ -161,13 +164,11 @@ CommonDialog.prototype = {
this.setDefaultFocus(true);
if (this.args.enableDelay) {
this.setButtonsEnabledState(false);
// Use a longer, pref-controlled delay when the dialog is first opened.
let delayTime = Services.prefs.getIntPref("security.dialog_enable_delay");
this.startOnFocusDelay(delayTime);
let self = this;
this.ui.focusTarget.addEventListener("blur", function(e) { self.onBlur(e); }, false);
this.ui.focusTarget.addEventListener("focus", function(e) { self.onFocus(e); }, false);
this.delayHelper = new EnableDelayHelper({
disableDialog: () => this.setButtonsEnabledState(false),
enableDialog: () => this.setButtonsEnabledState(true),
focusTarget: this.ui.focusTarget
});
}
// Play a sound (unless we're tab-modal -- don't want those to feel like OS prompts).
@ -230,44 +231,6 @@ CommonDialog.prototype = {
this.ui.button3.disabled = !enabled;
},
onBlur : function (aEvent) {
if (aEvent.target != this.ui.focusTarget)
return;
this.setButtonsEnabledState(false);
// If we blur while waiting to enable the buttons, just cancel the
// timer to ensure the delay doesn't fire while not focused.
if (this.focusTimer) {
this.focusTimer.cancel();
this.focusTimer = null;
}
},
onFocus : function (aEvent) {
if (aEvent.target != this.ui.focusTarget)
return;
this.startOnFocusDelay();
},
startOnFocusDelay : function(delayTime) {
// Shouldn't already have a timer, but just in case...
if (this.focusTimer)
return;
// If no delay specified, use 250ms. (This is the normal case for when
// after the dialog has been opened and focus shifts.)
if (!delayTime)
delayTime = 250;
let self = this;
this.focusTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.focusTimer.initWithCallback(function() { self.onFocusTimeout(); },
delayTime, Ci.nsITimer.TYPE_ONE_SHOT);
},
onFocusTimeout : function() {
this.focusTimer = null;
this.setButtonsEnabledState(true);
},
setDefaultFocus : function(isInitialLoad) {
let b = (this.args.defaultButtonNum || 0);
let button = this.ui["button" + b];

View File

@ -1,10 +1,12 @@
this.EXPORTED_SYMBOLS = [ "PromptUtils" ];
this.EXPORTED_SYMBOLS = [ "PromptUtils", "EnableDelayHelper" ];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
this.PromptUtils = {
// Fire a dialog open/close event. Used by tabbrowser to focus the
// tab which is triggering a prompt.
@ -40,3 +42,110 @@ this.PromptUtils = {
obj[propName] = propBag.getProperty(propName);
},
};
/**
* This helper handles the enabling/disabling of dialogs that might
* be subject to fast-clicking attacks. It handles the initial delayed
* enabling of the dialog, as well as disabling it on blur and reapplying
* the delay when the dialog regains focus.
*
* @param enableDialog A custom function to be called when the dialog
* is to be enabled.
* @param diableDialog A custom function to be called when the dialog
* is to be disabled.
* @param focusTarget The window used to watch focus/blur events.
*/
this.EnableDelayHelper = function({enableDialog, disableDialog, focusTarget}) {
this.enableDialog = makeSafe(enableDialog);
this.disableDialog = makeSafe(disableDialog);
this.focusTarget = focusTarget;
this.disableDialog();;
this.focusTarget.addEventListener("blur", this, false);
this.focusTarget.addEventListener("focus", this, false);
this.focusTarget.document.addEventListener("unload", this, false);
this.startOnFocusDelay();
};
this.EnableDelayHelper.prototype = {
get delayTime() {
return Services.prefs.getIntPref("security.dialog_enable_delay");
},
handleEvent : function(event) {
if (event.target != this.focusTarget &&
event.target != this.focusTarget.document)
return;
switch (event.type) {
case "blur":
this.onBlur();
break;
case "focus":
this.onFocus();
break;
case "unload":
this.onUnload();
break;
}
},
onBlur : function () {
this.disableDialog();
// If we blur while waiting to enable the buttons, just cancel the
// timer to ensure the delay doesn't fire while not focused.
if (this._focusTimer) {
this._focusTimer.cancel();
this._focusTimer = null;
}
},
onFocus : function () {
this.startOnFocusDelay();
},
onUnload: function() {
this.focusTarget.removeEventListener("blur", this, false);
this.focusTarget.removeEventListener("focus", this, false);
this.focusTarget.document.removeEventListener("unload", this, false);
if (this._focusTimer) {
this._focusTimer.cancel();
this._focusTimer = null;
}
this.focusTarget = this.enableDialog = this.disableDialog = null;
},
startOnFocusDelay : function() {
if (this._focusTimer)
return;
this._focusTimer = Cc["@mozilla.org/timer;1"]
.createInstance(Ci.nsITimer);
this._focusTimer.initWithCallback(
() => { this.onFocusTimeout(); },
this.delayTime,
Ci.nsITimer.TYPE_ONE_SHOT
);
},
onFocusTimeout : function() {
this._focusTimer = null;
this.enableDialog();
},
};
function makeSafe(fn) {
return function () {
// The dialog could be gone by now (if the user closed it),
// which makes it likely that the given fn might throw.
try {
fn();
} catch (e) { }
};
}

View File

@ -19,7 +19,6 @@
<dialog id="unknownContentType"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
onload="dialog.initDialog();" onunload="if (dialog) dialog.onCancel();"
onblur="if (dialog) dialog.onBlur(event);" onfocus="dialog.onFocus(event);"
#ifdef XP_WIN
style="width: 36em;"
#else

View File

@ -8,6 +8,9 @@
const {utils: Cu, interfaces: Ci, classes: Cc, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EnableDelayHelper",
"resource://gre/modules/SharedPromptUtils.jsm");
///////////////////////////////////////////////////////////////////////////////
//// Helper Functions
@ -539,25 +542,21 @@ nsUnknownContentTypeDialog.prototype = {
this.mDialog.setTimeout("dialog.postShowCallback()", 0);
let acceptDelay = Services.prefs.getIntPref("security.dialog_enable_delay");
this.mDialog.document.documentElement.getButton("accept").disabled = true;
this._showTimer = Components.classes["@mozilla.org/timer;1"]
.createInstance(nsITimer);
this._showTimer.initWithCallback(this, acceptDelay, nsITimer.TYPE_ONE_SHOT);
this.delayHelper = new EnableDelayHelper({
disableDialog: () => {
this.mDialog.document.documentElement.getButton("accept").disabled = true;
},
enableDialog: () => {
this.mDialog.document.documentElement.getButton("accept").disabled = false;
},
focusTarget: this.mDialog
});
},
notify: function (aTimer) {
if (aTimer == this._showTimer) {
if (!this.mDialog) {
this.reallyShow();
} else {
// The user may have already canceled the dialog.
try {
if (!this._blurred) {
this.mDialog.document.documentElement.getButton("accept").disabled = false;
}
} catch (ex) {}
this._delayExpired = true;
}
// The timer won't release us, so we have to release it.
this._showTimer = null;
@ -641,21 +640,6 @@ nsUnknownContentTypeDialog.prototype = {
}
},
_blurred: false,
_delayExpired: false,
onBlur: function(aEvent) {
this._blurred = true;
this.mDialog.document.documentElement.getButton("accept").disabled = true;
},
onFocus: function(aEvent) {
this._blurred = false;
if (this._delayExpired) {
var script = "document.documentElement.getButton('accept').disabled = false";
this.mDialog.setTimeout(script, 250);
}
},
// Returns true if opening the default application makes sense.
openWithDefaultOK: function() {
// The checking is different on Windows...

View File

@ -36,4 +36,5 @@ skip-if = os != 'win' && toolkit != 'cocoa'
# disabled for very frequent orange--bug 630567
skip-if = os != 'win' || true
[test_ui_stays_open_on_alert_clickback.xul]
[test_unknownContentType_delayedbutton.xul]
[test_unknownContentType_dialog_layout.xul]

View File

@ -0,0 +1,117 @@
<?xml version="1.0"?>
<!-- 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/. -->
<!--
* The unknownContentType popup can have two different layouts depending on
* whether a helper application can be selected or not.
* This tests that both layouts have correct collapsed elements.
-->
<window title="Unknown Content Type Dialog Test"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
onload="doTest()">
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
<script type="application/javascript"
src="utils.js"/>
<script type="application/javascript"><![CDATA[
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/Task.jsm");
Components.utils.import("resource://gre/modules/Promise.jsm");
const UCT_URI = "chrome://mozapps/content/downloads/unknownContentType.xul";
const LOAD_URI = "http://mochi.test:8888/chrome/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt";
const DIALOG_DELAY = Services.prefs.getIntPref("security.dialog_enable_delay") + 200;
let UCTObserver = {
opened: Promise.defer(),
closed: Promise.defer(),
observe: function(aSubject, aTopic, aData) {
let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
switch (aTopic) {
case "domwindowopened":
win.addEventListener("load", function onLoad(event) {
win.removeEventListener("load", onLoad, false);
// Let the dialog initialize
SimpleTest.executeSoon(function() {
UCTObserver.opened.resolve(win);
});
}, false);
break;
case "domwindowclosed":
if (win.location == UCT_URI) {
this.closed.resolve();
}
break;
}
}
};
Services.ww.registerNotification(UCTObserver);
SimpleTest.waitForExplicitFinish();
SimpleTest.requestFlakyTimeout("This test is testing a timing-based feature, so it really needs to wait a certain amount of time to verify that the feature worked.");
function waitDelay(delay) {
return new Promise((resolve, reject) => {
window.setTimeout(resolve, delay);
});
}
function doTest() {
Task.spawn(function test_aboutCrashed() {
let frame = document.getElementById("testframe");
frame.setAttribute("src", LOAD_URI);
let uctWindow = yield UCTObserver.opened.promise;
let ok = uctWindow.document.documentElement.getButton("accept");
SimpleTest.is(ok.disabled, true, "button started disabled");
yield waitDelay(DIALOG_DELAY);
SimpleTest.is(ok.disabled, false, "button was enabled");
focusOutOfDialog = SimpleTest.promiseFocus(window);
window.focus();
yield focusOutOfDialog;
SimpleTest.is(ok.disabled, true, "button was disabled");
focusOnDialog = SimpleTest.promiseFocus(uctWindow);
uctWindow.focus();
yield focusOnDialog;
SimpleTest.is(ok.disabled, true, "button remained disabled");
yield waitDelay(DIALOG_DELAY);
SimpleTest.is(ok.disabled, false, "button re-enabled after delay");
uctWindow.document.documentElement.cancelDialog();
yield UCTObserver.closed.promise;
Services.ww.unregisterNotification(UCTObserver);
uctWindow = null;
UCTObserver = null;
SimpleTest.finish();
});
}
]]></script>
<body xmlns="http://www.w3.org/1999/xhtml">
<p id="display"></p>
<div id="content" style="display:none;"></div>
<pre id="test"></pre>
</body>
<iframe xmlns="http://www.w3.org/1999/xhtml"
id="testframe">
</iframe>
</window>