Bug 1415918 - Allow discarding browsers that have beforeunload handlers in tabbrowser.discardBrowser. r=bz

MozReview-Commit-ID: 5KQcrOQTSpK
This commit is contained in:
Kevin Jones 2017-11-24 15:14:46 +01:00
parent ecd94ed690
commit 25bd05b91c
16 changed files with 452 additions and 21 deletions

View File

@ -2539,19 +2539,22 @@
<method name="discardBrowser">
<parameter name="aBrowser"/>
<parameter name="aForceDiscard"/>
<body>
<![CDATA[
"use strict";
let tab = this.getTabForBrowser(aBrowser);
let permitUnloadFlags = aForceDiscard ? aBrowser.dontPromptAndUnload : aBrowser.dontPromptAndDontUnload;
if (!tab ||
tab.selected ||
tab.closing ||
this._windowIsClosing ||
!aBrowser.isConnected ||
!aBrowser.isRemoteBrowser ||
aBrowser.frameLoader.tabParent.hasBeforeUnload) {
!aBrowser.permitUnload(permitUnloadFlags).permitUnload) {
return;
}

View File

@ -56,6 +56,7 @@ support-files =
restore_redirect_target.html
browser_1234021_page.html
browser_1284886_suspend_tab.html
browser_1284886_suspend_tab_2.html
#NB: the following are disabled
# browser_464620_a.html

View File

@ -2,6 +2,12 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
add_task(async function test() {
await SpecialPowers.pushPrefEnv({
"set": [
["dom.require_user_interaction_for_beforeunload", false],
]
});
let url = "about:robots";
let tab0 = gBrowser.tabs[0];
let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
@ -23,18 +29,35 @@ add_task(async function test() {
gBrowser._endRemoveTab(tab1);
// Test that tab with beforeunload handler is not able to be suspended.
// Open tab containing a page which has a beforeunload handler which shows a prompt.
url = "http://example.com/browser/browser/components/sessionstore/test/browser_1284886_suspend_tab.html";
tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
let browser1 = tab1.linkedBrowser;
await BrowserTestUtils.switchTab(gBrowser, tab0);
gBrowser.discardBrowser(tab1.linkedBrowser);
ok(tab1.linkedPanel, "cannot suspend a tab with beforeunload handler");
// Test that tab with beforeunload handler which would show a prompt cannot be suspended.
gBrowser.discardBrowser(browser1);
ok(tab1.linkedPanel, "cannot suspend a tab with beforeunload handler which would show a prompt");
// Test that tab with beforeunload handler which would show a prompt will be suspended if forced.
gBrowser.discardBrowser(browser1, true);
ok(!tab1.linkedPanel, "force suspending a tab with beforeunload handler which would show a prompt");
await promiseRemoveTab(tab1);
// Test that remote tab is not able to be suspended.
// Open tab containing a page which has a beforeunload handler which does not show a prompt.
url = "http://example.com/browser/browser/components/sessionstore/test/browser_1284886_suspend_tab_2.html";
tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
browser1 = tab1.linkedBrowser;
await BrowserTestUtils.switchTab(gBrowser, tab0);
// Test that tab with beforeunload handler which would not show a prompt can be suspended.
gBrowser.discardBrowser(browser1);
ok(!tab1.linkedPanel, "can suspend a tab with beforeunload handler which would not show a prompt");
await promiseRemoveTab(tab1);
// Test that non-remote tab is not able to be suspended.
url = "about:robots";
tab1 = BrowserTestUtils.addTab(gBrowser, url, { forceNotRemote: true });
await promiseBrowserLoaded(tab1.linkedBrowser, true, url);

View File

@ -0,0 +1,11 @@
<html>
<head>
<script>
window.onbeforeunload = function() {
};
</script>
</head>
<body>
TEST PAGE
</body>
</html>

View File

@ -50,10 +50,35 @@ interface nsIContentViewer : nsISupports
[noscript] readonly attribute boolean isStopped;
/**
* Checks if the document wants to prevent unloading by firing beforeunload on
* the document, and if it does, prompts the user. The result is returned.
* aPermitUnloadFlags are passed to PermitUnload to indicate what action to take
* if a beforeunload handler wants to prompt the user. It is also used by
* permitUnloadInternal to ensure we only prompt once.
*
* ePrompt: Prompt and return the user's choice (default).
* eDontPromptAndDontUnload: Don't prompt and return false (unload not permitted)
* if the document (or its children) asks us to prompt.
* eDontPromptAndUnload: Don't prompt and return true (unload permitted) no matter what.
*/
boolean permitUnload();
const unsigned long ePrompt = 0;
const unsigned long eDontPromptAndDontUnload = 1;
const unsigned long eDontPromptAndUnload = 2;
/**
* Overload PermitUnload method for C++ consumers with no aPermitUnloadFlags
* argument.
*/
%{C++
nsresult PermitUnload(bool* canUnload) {
return PermitUnload(ePrompt, canUnload);
}
%}
/**
* Checks if the document wants to prevent unloading by firing beforeunload on
* the document, and if it does, takes action directed by aPermitUnloadFlags.
* The result is returned.
*/
boolean permitUnload([optional] in unsigned long aPermitUnloadFlags);
/**
* Exposes whether we're blocked in a call to permitUnload.
@ -61,11 +86,11 @@ interface nsIContentViewer : nsISupports
readonly attribute boolean inPermitUnload;
/**
* As above, but this passes around the aShouldPrompt argument to keep
* As above, but this passes around the aPermitUnloadFlags argument to keep
* track of whether the user has responded to a prompt.
* Used internally by the scriptable version to ensure we only prompt once.
*/
[noscript,nostdcall] boolean permitUnloadInternal(inout boolean aShouldPrompt);
[noscript,nostdcall] boolean permitUnloadInternal(inout unsigned long aPermitUnloadFlags);
/**
* Exposes whether we're in the process of firing the beforeunload event.

View File

@ -39,6 +39,11 @@ support-files =
file_bug1328501.html
file_bug1328501_frame.html
file_bug1328501_framescript.js
file_bug1415918_beforeunload_2.html
file_bug1415918_beforeunload_3.html
file_bug1415918_beforeunload_iframe_2.html
file_bug1415918_beforeunload_iframe.html
file_bug1415918_beforeunload.html
file_multiple_pushState.html
print_postdata.sjs
test-form_sjis.html
@ -56,6 +61,7 @@ support-files =
[browser_bug1328501.js]
[browser_bug1347823.js]
[browser_bug134911.js]
[browser_bug1415918_beforeunload_options.js]
[browser_bug234628-1.js]
[browser_bug234628-10.js]
[browser_bug234628-11.js]

View File

@ -0,0 +1,242 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
add_task(async function test() {
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
await SpecialPowers.pushPrefEnv({
"set": [
["dom.require_user_interaction_for_beforeunload", false],
]
});
let url = TEST_PATH + "file_bug1415918_beforeunload.html";
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
let browser = tab.linkedBrowser;
let stack = browser.parentNode;
let buttonId;
let promptShown = false;
let observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (buttonId && mutation.type == "attributes" && browser.hasAttribute("tabmodalPromptShowing")) {
let prompt = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt")[0];
document.getAnonymousElementByAttribute(prompt, "anonid", buttonId).click();
promptShown = true;
}
});
});
observer.observe(browser, { attributes: true });
/*
* Check condition where beforeunload handlers request a prompt.
*/
// Prompt is shown, user clicks OK.
buttonId = "button0";
promptShown = false;
ok(browser.permitUnload().permitUnload, "permit unload should be true");
ok(promptShown, "prompt should have been displayed");
// Check that all beforeunload handlers fired and reset attributes.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
content.window.document.body.removeAttribute("fired");
for (let frame of Array.from(content.window.frames)) {
ok(frame.document.body.hasAttribute("fired"), "frame document beforeunload handler should fire");
frame.document.body.removeAttribute("fired");
}
});
// Prompt is shown, user clicks CANCEL.
buttonId = "button1";
promptShown = false;
ok(!browser.permitUnload().permitUnload, "permit unload should be false");
ok(promptShown, "prompt should have been displayed");
buttonId = "";
// Check that only the parent beforeunload handler fired, and reset attribute.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
content.window.document.body.removeAttribute("fired");
for (let frame of Array.from(content.window.frames)) {
ok(!frame.document.body.hasAttribute("fired"), "frame document beforeunload handler should not fire");
}
});
// Prompt is not shown, don't permit unload.
promptShown = false;
ok(!browser.permitUnload(browser.dontPromptAndDontUnload).permitUnload, "permit unload should be false");
ok(!promptShown, "prompt should not have been displayed");
// Check that only the parent beforeunload handler fired, and reset attribute.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
content.window.document.body.removeAttribute("fired");
for (let frame of Array.from(content.window.frames)) {
ok(!frame.document.body.hasAttribute("fired"), "frame document beforeunload handler should not fire");
}
});
// Prompt is not shown, permit unload.
promptShown = false;
ok(browser.permitUnload(browser.dontPromptAndUnload).permitUnload, "permit unload should be true");
ok(!promptShown, "prompt should not have been displayed");
// Check that all beforeunload handlers fired.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
for (let frame of Array.from(content.window.frames)) {
ok(frame.document.body.hasAttribute("fired"), "frame document beforeunload handler should fire");
}
});
/*
* Check condition where no one requests a prompt. In all cases,
* permitUnload should be true, and all handlers fired.
*/
buttonId = "button0";
url = TEST_PATH + "file_bug1415918_beforeunload_2.html";
browser.loadURI(url);
await BrowserTestUtils.browserLoaded(browser, false, url);
buttonId = "";
promptShown = false;
ok(browser.permitUnload().permitUnload, "permit unload should be true");
ok(!promptShown, "prompt should not have been displayed");
// Check that all beforeunload handlers fired and reset attributes.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
content.window.document.body.removeAttribute("fired");
for (let frame of Array.from(content.window.frames)) {
ok(frame.document.body.hasAttribute("fired"), "frame document beforeunload handler should fire");
frame.document.body.removeAttribute("fired");
}
});
promptShown = false;
ok(browser.permitUnload(browser.dontPromptAndDontUnload).permitUnload, "permit unload should be true");
ok(!promptShown, "prompt should not have been displayed");
// Check that all beforeunload handlers fired and reset attributes.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
content.window.document.body.removeAttribute("fired");
for (let frame of Array.from(content.window.frames)) {
ok(frame.document.body.hasAttribute("fired"), "frame document beforeunload handler should fire");
frame.document.body.removeAttribute("fired");
}
});
promptShown = false;
ok(browser.permitUnload(browser.dontPromptAndUnload).permitUnload, "permit unload should be true");
ok(!promptShown, "prompt should not have been displayed");
// Check that all beforeunload handlers fired.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
for (let frame of Array.from(content.window.frames)) {
ok(frame.document.body.hasAttribute("fired"), "frame document beforeunload handler should fire");
}
});
/*
* Check condition where the parent beforeunload handler does not request a prompt,
* but a child beforeunload handler does.
*/
buttonId = "button0";
url = TEST_PATH + "file_bug1415918_beforeunload_3.html";
browser.loadURI(url);
await BrowserTestUtils.browserLoaded(browser, false, url);
// Prompt is shown, user clicks OK.
promptShown = false;
ok(browser.permitUnload().permitUnload, "permit unload should be true");
ok(promptShown, "prompt should have been displayed");
// Check that all beforeunload handlers fired and reset attributes.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
content.window.document.body.removeAttribute("fired");
for (let frame of Array.from(content.window.frames)) {
ok(frame.document.body.hasAttribute("fired"), "frame document beforeunload handler should fire");
frame.document.body.removeAttribute("fired");
}
});
// Prompt is shown, user clicks CANCEL.
buttonId = "button1";
promptShown = false;
ok(!browser.permitUnload().permitUnload, "permit unload should be false");
ok(promptShown, "prompt should have been displayed");
buttonId = "";
// Check that the parent beforeunload handler fired, and only one child beforeunload
// handler fired. Reset attributes.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
content.window.document.body.removeAttribute("fired");
let count = 0;
for (let frame of Array.from(content.window.frames)) {
if (frame.document.body.hasAttribute("fired")) {
count++;
frame.document.body.removeAttribute("fired");
}
}
is(count, 1, "only one frame document beforeunload handler should fire");
});
// Prompt is not shown, don't permit unload.
promptShown = false;
ok(!browser.permitUnload(browser.dontPromptAndDontUnload).permitUnload, "permit unload should be false");
ok(!promptShown, "prompt should not have been displayed");
// Check that the parent beforeunload handler fired, and only one child beforeunload
// handler fired. Reset attributes.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
content.window.document.body.removeAttribute("fired");
let count = 0;
for (let frame of Array.from(content.window.frames)) {
if (frame.document.body.hasAttribute("fired")) {
count++;
frame.document.body.removeAttribute("fired");
}
}
is(count, 1, "only one frame document beforeunload handler should fire");
});
// Prompt is not shown, permit unload.
promptShown = false;
ok(browser.permitUnload(browser.dontPromptAndUnload).permitUnload, "permit unload should be true");
ok(!promptShown, "prompt should not have been displayed");
// Check that all beforeunload handlers fired.
await ContentTask.spawn(browser, null, () => {
ok(content.window.document.body.hasAttribute("fired"), "parent document beforeunload handler should fire");
for (let frame of Array.from(content.window.frames)) {
ok(frame.document.body.hasAttribute("fired"), "frame document beforeunload handler should fire");
}
});
// Remove tab.
buttonId = "button0";
BrowserTestUtils.removeTab(tab);
});

View File

@ -0,0 +1,22 @@
<html>
<head>
<style>
body[fired] {
background-color: #f00;
}
</style>
<script>
window.onbeforeunload = function() {
document.body.setAttribute("fired", true);
return "Parent document prompt";
}
</script>
</head>
<body>
TEST PAGE
<iframe style="width: 150px; height: 60px" src="file_bug1415918_beforeunload_iframe.html">
</iframe>
<iframe style="width: 150px; height: 60px" src="file_bug1415918_beforeunload_iframe.html">
</iframe>
</body>
</html>

View File

@ -0,0 +1,21 @@
<html>
<head>
<style>
body[fired] {
background-color: #f00;
}
</style>
<script>
window.onbeforeunload = function() {
document.body.setAttribute("fired", true);
}
</script>
</head>
<body>
TEST PAGE
<iframe style="width: 150px; height: 60px" src="file_bug1415918_beforeunload_iframe_2.html">
</iframe>
<iframe style="width: 150px; height: 60px" src="file_bug1415918_beforeunload_iframe_2.html">
</iframe>
</body>
</html>

View File

@ -0,0 +1,21 @@
<html>
<head>
<style>
body[fired] {
background-color: #f00;
}
</style>
<script>
window.onbeforeunload = function() {
document.body.setAttribute("fired", true);
}
</script>
</head>
<body>
TEST PAGE
<iframe style="width: 150px; height: 60px" src="file_bug1415918_beforeunload_iframe.html">
</iframe>
<iframe style="width: 150px; height: 60px" src="file_bug1415918_beforeunload_iframe.html">
</iframe>
</body>
</html>

View File

@ -0,0 +1,18 @@
<html>
<head>
<style>
body[fired] {
background-color: #0f0;
}
</style>
<script>
window.onbeforeunload = function() {
document.body.setAttribute("fired", true);
return "Frame document prompt";
}
</script>
</head>
</body>
FRAME
</body>
</html>

View File

@ -0,0 +1,17 @@
<html>
<head>
<style>
body[fired] {
background-color: #0f0;
}
</style>
<script>
window.onbeforeunload = function() {
document.body.setAttribute("fired", true);
}
</script>
</head>
</body>
FRAME
</body>
</html>

View File

@ -1144,15 +1144,14 @@ nsDocumentViewer::GetIsStopped(bool* aOutIsStopped)
}
NS_IMETHODIMP
nsDocumentViewer::PermitUnload(bool *aPermitUnload)
nsDocumentViewer::PermitUnload(uint32_t aPermitUnloadFlags, bool *aPermitUnload)
{
bool shouldPrompt = true;
return PermitUnloadInternal(&shouldPrompt, aPermitUnload);
return PermitUnloadInternal(&aPermitUnloadFlags, aPermitUnload);
}
nsresult
nsDocumentViewer::PermitUnloadInternal(bool *aShouldPrompt,
nsDocumentViewer::PermitUnloadInternal(uint32_t *aPermitUnloadFlags,
bool *aPermitUnload)
{
AutoDontWarnAboutSyncXHR disableSyncXHRWarning;
@ -1238,12 +1237,23 @@ nsDocumentViewer::PermitUnloadInternal(bool *aShouldPrompt,
nsAutoString text;
event->GetReturnValue(text);
if (sIsBeforeUnloadDisabled) {
*aPermitUnloadFlags = eDontPromptAndUnload;
}
// NB: we nullcheck mDocument because it might now be dead as a result of
// the event being dispatched.
if (!sIsBeforeUnloadDisabled && *aShouldPrompt && dialogsAreEnabled &&
if (*aPermitUnloadFlags != eDontPromptAndUnload && dialogsAreEnabled &&
mDocument && !(mDocument->GetSandboxFlags() & SANDBOXED_MODALS) &&
(!sBeforeUnloadRequiresInteraction || mDocument->UserHasInteracted()) &&
(event->WidgetEventPtr()->DefaultPrevented() || !text.IsEmpty())) {
// If the consumer wants prompt requests to just stop unloading, we don't
// need to prompt and can return immediately.
if (*aPermitUnloadFlags == eDontPromptAndDontUnload) {
*aPermitUnload = false;
return NS_OK;
}
// Ask the user if it's ok to unload the current page
nsCOMPtr<nsIPrompt> prompt = do_GetInterface(docShell);
@ -1322,7 +1332,7 @@ nsDocumentViewer::PermitUnloadInternal(bool *aShouldPrompt,
// If the user decided to go ahead, make sure not to prompt the user again
// by toggling the internal prompting bool to false:
if (*aPermitUnload) {
*aShouldPrompt = false;
*aPermitUnloadFlags = eDontPromptAndUnload;
}
}
}
@ -1342,7 +1352,7 @@ nsDocumentViewer::PermitUnloadInternal(bool *aShouldPrompt,
docShell->GetContentViewer(getter_AddRefs(cv));
if (cv) {
cv->PermitUnloadInternal(aShouldPrompt, aPermitUnload);
cv->PermitUnloadInternal(aPermitUnloadFlags, aPermitUnload);
}
}
}

View File

@ -619,7 +619,7 @@ addMessageListener("PermitUnload", msg => {
let permitUnload = true;
if (docShell && docShell.contentViewer) {
permitUnload = docShell.contentViewer.permitUnload();
permitUnload = docShell.contentViewer.permitUnload(msg.data.aPermitUnloadFlags);
}
sendAsyncMessage("PermitUnload", {id: msg.data.id, kind: "end", permitUnload});

View File

@ -1594,13 +1594,23 @@
</body>
</method>
<property name="dontPromptAndDontUnload"
onget="return 1;"
readonly="true"/>
<property name="dontPromptAndUnload"
onget="return 2;"
readonly="true"/>
<method name="permitUnload">
<parameter name="aPermitUnloadFlags"/>
<body>
<![CDATA[
if (!this.docShell || !this.docShell.contentViewer) {
return {permitUnload: true, timedOut: false};
}
return {permitUnload: this.docShell.contentViewer.permitUnload(), timedOut: false};
return {permitUnload: this.docShell.contentViewer.permitUnload(aPermitUnloadFlags),
timedOut: false};
]]>
</body>
</method>

View File

@ -285,6 +285,7 @@
</method>
<method name="permitUnload">
<parameter name="aPermitUnloadFlags"/>
<body>
<![CDATA[
let {tabParent} = this.frameLoader;
@ -326,7 +327,7 @@
Services.obs.removeObserver(observer, "message-manager-close");
}
mm.sendAsyncMessage("PermitUnload", {id});
mm.sendAsyncMessage("PermitUnload", {id, aPermitUnloadFlags});
mm.addMessageListener("PermitUnload", msgListener);
Services.obs.addObserver(observer, "message-manager-close");