diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 735d0cc76060..3fc4a0c476c3 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -2539,19 +2539,22 @@ + + + + + +TEST PAGE + + diff --git a/docshell/base/nsIContentViewer.idl b/docshell/base/nsIContentViewer.idl index 8cfe7955533e..06618a87a4f9 100644 --- a/docshell/base/nsIContentViewer.idl +++ b/docshell/base/nsIContentViewer.idl @@ -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. diff --git a/docshell/test/browser/browser.ini b/docshell/test/browser/browser.ini index 8ad568b7d0e0..2788674813a8 100644 --- a/docshell/test/browser/browser.ini +++ b/docshell/test/browser/browser.ini @@ -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] diff --git a/docshell/test/browser/browser_bug1415918_beforeunload_options.js b/docshell/test/browser/browser_bug1415918_beforeunload_options.js new file mode 100644 index 000000000000..038039fdf911 --- /dev/null +++ b/docshell/test/browser/browser_bug1415918_beforeunload_options.js @@ -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); +}); + diff --git a/docshell/test/browser/file_bug1415918_beforeunload.html b/docshell/test/browser/file_bug1415918_beforeunload.html new file mode 100644 index 000000000000..c44b62a76f14 --- /dev/null +++ b/docshell/test/browser/file_bug1415918_beforeunload.html @@ -0,0 +1,22 @@ + + + + + + +TEST PAGE + + + + diff --git a/docshell/test/browser/file_bug1415918_beforeunload_2.html b/docshell/test/browser/file_bug1415918_beforeunload_2.html new file mode 100644 index 000000000000..4e8b4df99773 --- /dev/null +++ b/docshell/test/browser/file_bug1415918_beforeunload_2.html @@ -0,0 +1,21 @@ + + + + + + +TEST PAGE + + + + diff --git a/docshell/test/browser/file_bug1415918_beforeunload_3.html b/docshell/test/browser/file_bug1415918_beforeunload_3.html new file mode 100644 index 000000000000..c70ce012a043 --- /dev/null +++ b/docshell/test/browser/file_bug1415918_beforeunload_3.html @@ -0,0 +1,21 @@ + + + + + + +TEST PAGE + + + + diff --git a/docshell/test/browser/file_bug1415918_beforeunload_iframe.html b/docshell/test/browser/file_bug1415918_beforeunload_iframe.html new file mode 100644 index 000000000000..3b8da6028cac --- /dev/null +++ b/docshell/test/browser/file_bug1415918_beforeunload_iframe.html @@ -0,0 +1,18 @@ + + + + + + +FRAME + + diff --git a/docshell/test/browser/file_bug1415918_beforeunload_iframe_2.html b/docshell/test/browser/file_bug1415918_beforeunload_iframe_2.html new file mode 100644 index 000000000000..f1bf327cd035 --- /dev/null +++ b/docshell/test/browser/file_bug1415918_beforeunload_iframe_2.html @@ -0,0 +1,17 @@ + + + + + + +FRAME + + diff --git a/layout/base/nsDocumentViewer.cpp b/layout/base/nsDocumentViewer.cpp index 9a6b6f6d8de9..fdd6c02d3e28 100644 --- a/layout/base/nsDocumentViewer.cpp +++ b/layout/base/nsDocumentViewer.cpp @@ -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 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); } } } diff --git a/toolkit/content/browser-child.js b/toolkit/content/browser-child.js index 0acbb70e1d54..6341a0497653 100644 --- a/toolkit/content/browser-child.js +++ b/toolkit/content/browser-child.js @@ -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}); diff --git a/toolkit/content/widgets/browser.xml b/toolkit/content/widgets/browser.xml index 90b07f83d215..ddfc7e6922f7 100644 --- a/toolkit/content/widgets/browser.xml +++ b/toolkit/content/widgets/browser.xml @@ -1594,13 +1594,23 @@ + + + + + diff --git a/toolkit/content/widgets/remote-browser.xml b/toolkit/content/widgets/remote-browser.xml index 1f7eee0cd7d9..aacf2fff6e0b 100644 --- a/toolkit/content/widgets/remote-browser.xml +++ b/toolkit/content/widgets/remote-browser.xml @@ -285,6 +285,7 @@ +