From 1a89394d0d751b717f0285aba01b03446d52b3f1 Mon Sep 17 00:00:00 2001 From: Neil Deakin Date: Mon, 13 Aug 2012 15:05:34 -0400 Subject: [PATCH] Bug 391834, don't allow prompts in beforeunload, unload and pagehide events,r=smaug,patch mostly by gavin --- browser/base/content/tabbrowser.xml | 5 + browser/base/content/test/Makefile.in | 1 + .../content/test/browser_unloaddialogs.js | 134 ++++++++++++++++++ .../tabview/test/browser_tabview_bug626455.js | 3 - docshell/base/nsIContentViewer.idl | 13 +- dom/base/nsDOMWindowUtils.cpp | 16 +++ dom/base/nsGlobalWindow.cpp | 32 ++++- dom/base/nsGlobalWindow.h | 7 +- dom/interfaces/base/nsIDOMWindowUtils.idl | 8 +- layout/base/nsDocumentViewer.cpp | 7 + 10 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 browser/base/content/test/browser_unloaddialogs.js diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 5c5bea09037..10ef434ef8d 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1616,6 +1616,11 @@ evt.initUIEvent("TabClose", true, false, window, aTabWillBeMoved ? 1 : 0); aTab.dispatchEvent(evt); + // Prevent this tab from showing further dialogs, since we're closing it + var windowUtils = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + windowUtils.preventFurtherDialogs(); + // Remove the tab's filter and progress listener. const filter = this.mTabFilters[aTab._tPos]; #ifdef MOZ_E10S_COMPAT diff --git a/browser/base/content/test/Makefile.in b/browser/base/content/test/Makefile.in index 29b13a2c972..bc7593106c1 100644 --- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -199,6 +199,7 @@ _BROWSER_FILES = \ browser_tabfocus.js \ browser_tabs_isActive.js \ browser_tabs_owner.js \ + browser_unloaddialogs.js \ browser_urlbarAutoFillTrimURLs.js \ browser_urlbarCopying.js \ browser_urlbarEnter.js \ diff --git a/browser/base/content/test/browser_unloaddialogs.js b/browser/base/content/test/browser_unloaddialogs.js new file mode 100644 index 00000000000..b8dca544752 --- /dev/null +++ b/browser/base/content/test/browser_unloaddialogs.js @@ -0,0 +1,134 @@ +function notify(event) +{ + if (event.target.location == "about:blank") + return; + + var eventname = event.type; + if (eventname == "pagehide") + details.pagehides++; + else if (eventname == "beforeunload") + details.beforeunloads++; + else if (eventname == "unload") + details.unloads++; +} + +var details; + +var gUseFrame = false; + +const windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator); + +const TEST_BASE_URL = "data:text/html," + + ""; + +const TEST_URL = TEST_BASE_URL + "Test"; +const TEST_FRAME_URL = TEST_BASE_URL + "Frames"; + +function test() +{ + waitForExplicitFinish(); + windowMediator.addListener(promptListener); + runTest(); +} + +function runTest() +{ + details = { + testNumber : 0, + beforeunloads : 0, + pagehides : 0, + unloads : 0, + prompts : 0 + }; + + var tab = gBrowser.addTab(TEST_URL); + gBrowser.selectedTab = tab; + tab.linkedBrowser.addEventListener("pageshow", shown, true); + + tab.linkedBrowser.addEventListener("pagehide", notify, true); + tab.linkedBrowser.addEventListener("beforeunload", notify, true); + tab.linkedBrowser.addEventListener("unload", notify, true); +} + +function shown(event) +{ + if (details.testNumber == 0) { + var browser; + var iframe; + if (gUseFrame) { + iframe = event.target.createElement("iframe"); + iframe.src = TEST_FRAME_URL; + event.target.documentElement.appendChild(iframe); + browser = iframe.contentWindow; + } + else { + browser = gBrowser.selectedTab.linkedBrowser; + details.testNumber = 1; // Move onto to the next step immediately + } + } + + if (details.testNumber == 1) { + // Test going to another page + executeSoon(function () { + const urlToLoad = "data:text/html,Another Page"; + if (gUseFrame) { + event.target.location = urlToLoad; + } + else { + gBrowser.selectedBrowser.loadURI(urlToLoad); + } + }); + } + else if (details.testNumber == 2) { + is(details.pagehides, 1, "pagehides after next page") + is(details.beforeunloads, 1, "beforeunloads after next page") + is(details.unloads, 1, "unloads after next page") + is(details.prompts, 1, "prompts after next page") + + executeSoon(function () gUseFrame ? gBrowser.goBack() : event.target.defaultView.back()); + } + else if (details.testNumber == 3) { + is(details.pagehides, 2, "pagehides after back") + is(details.beforeunloads, 2, "beforeunloads after back") + // No cache, so frame is unloaded + is(details.unloads, gUseFrame ? 2 : 1, "unloads after back") + is(details.prompts, 1, "prompts after back") + + // Test closing the tab + gBrowser.selectedBrowser.removeEventListener("pageshow", shown, true); + gBrowser.removeTab(gBrowser.selectedTab); + + // When the frame is present, there is are two beforeunload and prompts, + // one for the frame and the other for the parent. + is(details.pagehides, 3, "pagehides after close") + is(details.beforeunloads, gUseFrame ? 4 : 3, "beforeunloads after close") + is(details.unloads, gUseFrame ? 3 : 2, "unloads after close") + is(details.prompts, gUseFrame ? 3 : 2, "prompts after close") + + // Now run the test again using a child frame. + if (gUseFrame) { + windowMediator.removeListener(promptListener); + finish(); + } + else { + gUseFrame = true; + runTest(); + } + + return; + } + + details.testNumber++; +} + +var promptListener = { + onWindowTitleChange: function () {}, + onOpenWindow: function (win) { + details.prompts++; + let domWin = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); + executeSoon(function () { domWin.close() }); + }, + onCloseWindow: function () {}, +}; diff --git a/browser/components/tabview/test/browser_tabview_bug626455.js b/browser/components/tabview/test/browser_tabview_bug626455.js index 402542c9c75..e6ffaa255f6 100644 --- a/browser/components/tabview/test/browser_tabview_bug626455.js +++ b/browser/components/tabview/test/browser_tabview_bug626455.js @@ -62,9 +62,6 @@ function testLeavePage() { let dialogsAccepted = 0; whenDialogOpened(function onDialogOpened(dialog) { - if (++dialogsAccepted < 3) - whenDialogOpened(onDialogOpened); - // Leave page dialog.acceptDialog(); }); diff --git a/docshell/base/nsIContentViewer.idl b/docshell/base/nsIContentViewer.idl index cafe7e85408..7e57ec7f9d7 100644 --- a/docshell/base/nsIContentViewer.idl +++ b/docshell/base/nsIContentViewer.idl @@ -27,7 +27,7 @@ class nsDOMNavigationTiming; [ptr] native nsIViewPtr(nsIView); [ptr] native nsDOMNavigationTimingPtr(nsDOMNavigationTiming); -[scriptable, builtinclass, uuid(26b2380b-4a1a-46cd-b7d8-7600e41c1688)] +[scriptable, builtinclass, uuid(b9d92b8b-5623-4079-ae11-36bb341f322e)] interface nsIContentViewer : nsISupports { @@ -134,10 +134,10 @@ interface nsIContentViewer : nsISupports */ void clearHistoryEntry(); - /* + /** * Change the layout to view the document with page layout (like print preview), but * dynamic and editable (like Galley layout). - */ + */ void setPageMode(in boolean aPageMode, in nsIPrintSettings aPrintSettings); /** @@ -146,7 +146,7 @@ interface nsIContentViewer : nsISupports */ readonly attribute nsISHEntry historyEntry; - /* + /** * Indicates when we're in a state where content shouldn't be allowed to * trigger a tab-modal prompt (as opposed to a window-modal prompt) because * we're part way through some operation (eg beforeunload) that shouldn't be @@ -155,6 +155,11 @@ interface nsIContentViewer : nsISupports */ readonly attribute boolean isTabModalPromptAllowed; + /** + * Returns whether this content viewer is in a hidden state. + */ + readonly attribute boolean isHidden; + [noscript] readonly attribute nsIPresShellPtr presShell; [noscript] readonly attribute nsPresContextPtr presContext; // aDocument must not be null. diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp index 93cd845f021..2ab6a171caa 100644 --- a/dom/base/nsDOMWindowUtils.cpp +++ b/dom/base/nsDOMWindowUtils.cpp @@ -2287,6 +2287,22 @@ nsDOMWindowUtils::CheckAndClearPaintedState(nsIDOMElement* aElement, bool* aResu return NS_OK; } +NS_IMETHODIMP +nsDOMWindowUtils::PreventFurtherDialogs() +{ + // Permanently disable further dialogs for this window. + + if (!IsUniversalXPConnectCapable()) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr window = do_QueryReferent(mWindow); + NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); + + static_cast(window.get())->PreventFurtherDialogs(true); + return NS_OK; +} + static nsresult GetFileOrBlob(const nsAString& aName, const jsval& aBlobParts, const jsval& aParameters, JSContext* aCx, diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index 258753469dc..d4d05cec895 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -684,7 +684,8 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalWindow *aOuterWindow) mCleanedUp(false), mCallCleanUpAfterModalDialogCloses(false), mDialogAbuseCount(0), - mStopAbuseDialogs(false) + mStopAbuseDialogs(false), + mDialogsPermanentlyDisabled(false) { nsLayoutStatics::AddRef(); @@ -2566,6 +2567,22 @@ nsGlobalWindow::DialogsAreBlocked(bool *aBeingAbused) return true; } + if (topWindow->mDialogsPermanentlyDisabled) { + return true; + } + + // Dialogs are blocked if the content viewer is hidden + if (mDocShell) { + nsCOMPtr cv; + mDocShell->GetContentViewer(getter_AddRefs(cv)); + + bool isHidden; + cv->GetIsHidden(&isHidden); + if (isHidden) { + return true; + } + } + *aBeingAbused = topWindow->DialogsAreBeingAbused(); return topWindow->mStopAbuseDialogs && *aBeingAbused; @@ -2625,7 +2642,7 @@ nsGlobalWindow::ConfirmDialogIfNeeded() "ScriptDialogPreventTitle", title); promptSvc->Confirm(this, title.get(), label.get(), &disableDialog); if (disableDialog) { - PreventFurtherDialogs(); + PreventFurtherDialogs(false); return false; } @@ -2633,7 +2650,7 @@ nsGlobalWindow::ConfirmDialogIfNeeded() } void -nsGlobalWindow::PreventFurtherDialogs() +nsGlobalWindow::PreventFurtherDialogs(bool aPermanent) { nsGlobalWindow *topWindow = GetScriptableTop(); if (!topWindow) { @@ -2644,6 +2661,9 @@ nsGlobalWindow::PreventFurtherDialogs() topWindow = topWindow->GetCurrentInnerWindowInternal(); if (topWindow) { topWindow->mStopAbuseDialogs = true; + if (aPermanent) { + topWindow->mDialogsPermanentlyDisabled = true; + } } } @@ -4847,7 +4867,7 @@ nsGlobalWindow::Alert(const nsAString& aString) rv = prompt->AlertCheck(title.get(), final.get(), label.get(), &disallowDialog); if (disallowDialog) - PreventFurtherDialogs(); + PreventFurtherDialogs(false); } else { rv = prompt->Alert(title.get(), final.get()); } @@ -4914,7 +4934,7 @@ nsGlobalWindow::Confirm(const nsAString& aString, bool* aReturn) rv = prompt->ConfirmCheck(title.get(), final.get(), label.get(), &disallowDialog, aReturn); if (disallowDialog) - PreventFurtherDialogs(); + PreventFurtherDialogs(false); } else { rv = prompt->Confirm(title.get(), final.get(), aReturn); } @@ -4990,7 +5010,7 @@ nsGlobalWindow::Prompt(const nsAString& aMessage, const nsAString& aInitial, &inoutValue, label.get(), &disallowDialog, &ok); if (disallowDialog) { - PreventFurtherDialogs(); + PreventFurtherDialogs(false); } NS_ENSURE_SUCCESS(rv, rv); diff --git a/dom/base/nsGlobalWindow.h b/dom/base/nsGlobalWindow.h index 853cb9e509a..00520a8015b 100644 --- a/dom/base/nsGlobalWindow.h +++ b/dom/base/nsGlobalWindow.h @@ -453,7 +453,7 @@ public: bool ConfirmDialogIfNeeded(); // Prevent further dialogs in this (top level) window - void PreventFurtherDialogs(); + void PreventFurtherDialogs(bool aPermanent); virtual void SetHasAudioAvailableEventListeners(); @@ -1059,6 +1059,11 @@ protected: // mDialogAbuseCount gets reset. bool mStopAbuseDialogs; + // This flag gets set when dialogs should be permanently disabled for this + // window (e.g. when we are closing the tab and therefore are guaranteed to be + // destroying this window). + bool mDialogsPermanentlyDisabled; + nsRefPtr mURLProperty; nsTHashtable > mEventTargetObjects; diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl index c98782f955b..cbd369c3cbc 100644 --- a/dom/interfaces/base/nsIDOMWindowUtils.idl +++ b/dom/interfaces/base/nsIDOMWindowUtils.idl @@ -39,7 +39,7 @@ interface nsIFile; interface nsIDOMTouch; interface nsIDOMClientRect; -[scriptable, uuid(97548ee0-9def-421a-ab2a-c6c98efa9a3c)] +[scriptable, uuid(5fc61d7b-a303-4f34-adfe-b7828675ba45)] interface nsIDOMWindowUtils : nsISupports { /** @@ -1183,4 +1183,10 @@ interface nsIDOMWindowUtils : nsISupports { * The caller of this method must have UniversalXPConnect privileges. */ void setScrollPositionClampingScrollPortSize(in float aWidth, in float aHeight); + + /** + * Prevent this window (and any child windows) from displaying any further + * dialogs (e.g. window.alert()). + */ + void preventFurtherDialogs(); }; diff --git a/layout/base/nsDocumentViewer.cpp b/layout/base/nsDocumentViewer.cpp index eb05e93d1c3..7871cb88d76 100644 --- a/layout/base/nsDocumentViewer.cpp +++ b/layout/base/nsDocumentViewer.cpp @@ -4310,6 +4310,13 @@ DocumentViewerImpl::GetIsTabModalPromptAllowed(bool *aAllowed) return NS_OK; } +NS_IMETHODIMP +DocumentViewerImpl::GetIsHidden(bool *aHidden) +{ + *aHidden = mHidden; + return NS_OK; +} + void DocumentViewerImpl::DestroyPresShell() {