From 16a2846c1a0c5914c8fef3fe0b781f96dc2443e5 Mon Sep 17 00:00:00 2001 From: Johnny Stenback Date: Thu, 16 Sep 2010 18:24:14 -0700 Subject: [PATCH] Fixing bug 61098. Give users a way out of inifinite modal dialog loops. Original patch by Nochum Sossonko. r=jonas@sicking.cc a=beta7+ --- build/automation.py.in | 1 + dom/base/nsGlobalWindow.cpp | 257 ++++++++++++++--- dom/base/nsGlobalWindow.h | 51 ++++ dom/tests/mochitest/bugs/Makefile.in | 1 + dom/tests/mochitest/bugs/test_bug504862.html | 20 +- dom/tests/mochitest/bugs/test_bug61098.html | 270 ++++++++++++++++++ .../chrome/global/commonDialogs.properties | 2 + 7 files changed, 552 insertions(+), 50 deletions(-) create mode 100644 dom/tests/mochitest/bugs/test_bug61098.html diff --git a/build/automation.py.in b/build/automation.py.in index 02caaa898c14..d3b6d2ceb1a6 100644 --- a/build/automation.py.in +++ b/build/automation.py.in @@ -336,6 +336,7 @@ user_pref("dom.disable_open_during_load", false); user_pref("dom.max_script_run_time", 0); // no slow script dialogs user_pref("dom.max_chrome_script_run_time", 0); user_pref("dom.popup_maximum", -1); +user_pref("dom.successive_dialog_time_limit", 0); user_pref("signed.applets.codebase_principal_support", true); user_pref("security.warn_submit_insecure", false); user_pref("browser.shell.checkDefaultBrowser", false); diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index 43983bbc8b5e..4facb46b7ac4 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -690,13 +690,15 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalWindow *aOuterWindow) mPendingStorageEventsObsolete(nsnull), mTimeoutsSuspendDepth(0), mFocusMethod(0), - mSerial(0) + mSerial(0), #ifdef DEBUG - , mSetOpenerWindowCalled(PR_FALSE) + mSetOpenerWindowCalled(PR_FALSE), #endif - , mCleanedUp(PR_FALSE) - , mCallCleanUpAfterModalDialogCloses(PR_FALSE) - , mWindowID(gNextWindowID++) + mCleanedUp(PR_FALSE), + mCallCleanUpAfterModalDialogCloses(PR_FALSE), + mWindowID(gNextWindowID++), + mDialogAbuseCount(0), + mDialogDisabled(PR_FALSE) { nsLayoutStatics::AddRef(); @@ -2346,6 +2348,105 @@ nsGlobalWindow::PreHandleEvent(nsEventChainPreVisitor& aVisitor) return NS_OK; } +bool +nsGlobalWindow::DialogOpenAttempted() +{ + nsGlobalWindow *topWindow = GetTop(); + if (!topWindow) { + NS_ERROR("DialogOpenAttempted() called without a top window?"); + + return false; + } + + topWindow = topWindow->GetCurrentInnerWindowInternal(); + if (!topWindow || + topWindow->mLastDialogQuitTime.IsNull() || + nsContentUtils::IsCallerTrustedForCapability("UniversalXPConnect")) { + return false; + } + + TimeDuration dialogDuration(TimeStamp::Now() - + topWindow->mLastDialogQuitTime); + + if (dialogDuration.ToSeconds() < + nsContentUtils::GetIntPref("dom.successive_dialog_time_limit", + SUCCESSIVE_DIALOG_TIME_LIMIT)) { + topWindow->mDialogAbuseCount++; + + return (topWindow->GetPopupControlState() > openAllowed || + topWindow->mDialogAbuseCount > MAX_DIALOG_COUNT); + } + + topWindow->mDialogAbuseCount = 0; + + return false; +} + +bool +nsGlobalWindow::AreDialogsBlocked() +{ + nsGlobalWindow *topWindow = GetTop(); + if (!topWindow) { + NS_ERROR("AreDialogsBlocked() called without a top window?"); + + return true; + } + + topWindow = topWindow->GetCurrentInnerWindowInternal(); + + return !topWindow || + (topWindow->mDialogDisabled && + (topWindow->GetPopupControlState() > openAllowed || + topWindow->mDialogAbuseCount >= MAX_DIALOG_COUNT)); +} + +bool +nsGlobalWindow::ConfirmDialogAllowed() +{ + NS_ENSURE_TRUE(mDocShell, false); + nsCOMPtr promptSvc = + do_GetService("@mozilla.org/embedcomp/prompt-service;1"); + + if (!DialogOpenAttempted() || !promptSvc) { + return true; + } + + // Reset popup state while opening a modal dialog, and firing events + // about the dialog, to prevent the current state from being active + // the whole time a modal dialog is open. + nsAutoPopupStatePusher popupStatePusher(openAbused, PR_TRUE); + + PRBool disableDialog = PR_FALSE; + nsXPIDLString label, title; + nsContentUtils::GetLocalizedString(nsContentUtils::eCOMMON_DIALOG_PROPERTIES, + "ScriptDialogLabel", label); + nsContentUtils::GetLocalizedString(nsContentUtils::eCOMMON_DIALOG_PROPERTIES, + "ScriptDialogPreventTitle", title); + promptSvc->Confirm(this, title.get(), label.get(), &disableDialog); + if (disableDialog) { + PreventFurtherDialogs(); + return false; + } + + return true; +} + +void +nsGlobalWindow::PreventFurtherDialogs() +{ + nsGlobalWindow *topWindow = GetTop(); + if (!topWindow) { + NS_ERROR("PreventFurtherDialogs() called without a top window?"); + + return; + } + + topWindow = topWindow->GetCurrentInnerWindowInternal(); + + if (topWindow) + topWindow->mDialogDisabled = PR_TRUE; +} + nsresult nsGlobalWindow::PostHandleEvent(nsEventChainPostVisitor& aVisitor) { @@ -2673,8 +2774,6 @@ nsGlobalWindow::GetTop(nsIDOMWindow** aTop) { FORWARD_TO_OUTER(GetTop, (aTop), NS_ERROR_NOT_INITIALIZED); - nsresult ret = NS_OK; - *aTop = nsnull; if (mDocShell) { nsCOMPtr docShellAsItem(do_QueryInterface(mDocShell)); @@ -2682,12 +2781,12 @@ nsGlobalWindow::GetTop(nsIDOMWindow** aTop) docShellAsItem->GetSameTypeRootTreeItem(getter_AddRefs(root)); if (root) { - nsCOMPtr globalObject(do_GetInterface(root)); - CallQueryInterface(globalObject.get(), aTop); + nsCOMPtr top(do_GetInterface(root)); + top.swap(*aTop); } } - return ret; + return NS_OK; } NS_IMETHODIMP @@ -4274,6 +4373,13 @@ nsGlobalWindow::Alert(const nsAString& aString) { FORWARD_TO_OUTER(Alert, (aString), NS_ERROR_NOT_INITIALIZED); + if (AreDialogsBlocked()) + return NS_ERROR_NOT_AVAILABLE; + + // We have to capture this now so as not to get confused with the + // popup state we push next + PRBool shouldEnableDisableDialog = DialogOpenAttempted(); + // Reset popup state while opening a modal dialog, and firing events // about the dialog, to prevent the current state from being active // the whole time a modal dialog is open. @@ -4299,10 +4405,29 @@ nsGlobalWindow::Alert(const nsAString& aString) nsContentUtils::StripNullChars(*str, final); nsresult rv; - nsCOMPtr promptSvc = do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv); + nsCOMPtr promptSvc = + do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv); NS_ENSURE_SUCCESS(rv, rv); - return promptSvc->Alert(this, title.get(), final.get()); + EnterModalState(); + + if (shouldEnableDisableDialog) { + PRBool disallowDialog = PR_FALSE; + nsXPIDLString label; + nsContentUtils::GetLocalizedString(nsContentUtils::eCOMMON_DIALOG_PROPERTIES, + "ScriptDialogLabel", label); + + rv = promptSvc->AlertCheck(this, title.get(), final.get(), label.get(), + &disallowDialog); + if (disallowDialog) + PreventFurtherDialogs(); + } else { + rv = promptSvc->Alert(this, title.get(), final.get()); + } + + LeaveModalState(); + + return rv; } NS_IMETHODIMP @@ -4310,6 +4435,13 @@ nsGlobalWindow::Confirm(const nsAString& aString, PRBool* aReturn) { FORWARD_TO_OUTER(Confirm, (aString, aReturn), NS_ERROR_NOT_INITIALIZED); + if (AreDialogsBlocked()) + return NS_ERROR_NOT_AVAILABLE; + + // We have to capture this now so as not to get confused with the popup state + // we push next + PRBool shouldEnableDisableDialog = DialogOpenAttempted(); + // Reset popup state while opening a modal dialog, and firing events // about the dialog, to prevent the current state from being active // the whole time a modal dialog is open. @@ -4330,10 +4462,29 @@ nsGlobalWindow::Confirm(const nsAString& aString, PRBool* aReturn) nsContentUtils::StripNullChars(aString, final); nsresult rv; - nsCOMPtr promptSvc = do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv); + nsCOMPtr promptSvc = + do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv); NS_ENSURE_SUCCESS(rv, rv); - return promptSvc->Confirm(this, title.get(), final.get(), aReturn); + EnterModalState(); + + if (shouldEnableDisableDialog) { + PRBool disallowDialog = PR_FALSE; + nsXPIDLString label; + nsContentUtils::GetLocalizedString(nsContentUtils::eCOMMON_DIALOG_PROPERTIES, + "ScriptDialogLabel", label); + + rv = promptSvc->ConfirmCheck(this, title.get(), final.get(), label.get(), + &disallowDialog, aReturn); + if (disallowDialog) + PreventFurtherDialogs(); + } else { + rv = promptSvc->Confirm(this, title.get(), final.get(), aReturn); + } + + LeaveModalState(); + + return rv; } NS_IMETHODIMP @@ -4342,6 +4493,13 @@ nsGlobalWindow::Prompt(const nsAString& aMessage, const nsAString& aInitial, { SetDOMStringToNull(aReturn); + if (AreDialogsBlocked()) + return NS_ERROR_NOT_AVAILABLE; + + // We have to capture this now so as not to get confused with the popup state + // we push next + PRBool shouldEnableDisableDialog = DialogOpenAttempted(); + // Reset popup state while opening a modal dialog, and firing events // about the dialog, to prevent the current state from being active // the whole time a modal dialog is open. @@ -4361,15 +4519,32 @@ nsGlobalWindow::Prompt(const nsAString& aMessage, const nsAString& aInitial, nsContentUtils::StripNullChars(aInitial, fixedInitial); nsresult rv; - nsCOMPtr promptSvc = do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv); + nsCOMPtr promptSvc = + do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv); NS_ENSURE_SUCCESS(rv, rv); // Pass in the default value, if any. PRUnichar *inoutValue = ToNewUnicode(fixedInitial); + PRBool disallowDialog = PR_FALSE; - PRBool ok, dummy; + nsXPIDLString label; + if (shouldEnableDisableDialog) { + nsContentUtils::GetLocalizedString(nsContentUtils::eCOMMON_DIALOG_PROPERTIES, + "ScriptDialogLabel", label); + } + + EnterModalState(); + + PRBool ok; rv = promptSvc->Prompt(this, title.get(), fixedMessage.get(), - &inoutValue, nsnull, &dummy, &ok); + &inoutValue, label.get(), &disallowDialog, &ok); + + LeaveModalState(); + + if (disallowDialog) { + PreventFurtherDialogs(); + } + NS_ENSURE_SUCCESS(rv, rv); nsAdoptingString outValue(inoutValue); @@ -4623,6 +4798,9 @@ nsGlobalWindow::Print() #ifdef NS_PRINTING FORWARD_TO_OUTER(Print, (), NS_ERROR_NOT_INITIALIZED); + if (AreDialogsBlocked() || !ConfirmDialogAllowed()) + return NS_ERROR_NOT_AVAILABLE; + nsCOMPtr webBrowserPrint; if (NS_SUCCEEDED(GetInterface(NS_GET_IID(nsIWebBrowserPrint), getter_AddRefs(webBrowserPrint)))) { @@ -5855,17 +6033,14 @@ nsGlobalWindow::ReallyCloseWindow() void nsGlobalWindow::EnterModalState() { - nsCOMPtr top; - GetTop(getter_AddRefs(top)); + nsGlobalWindow* topWin = GetTop(); - if (!top) { + if (!topWin) { NS_ERROR("Uh, EnterModalState() called w/o a reachable top window?"); return; } - nsGlobalWindow* topWin = - static_cast(static_cast(top.get())); if (topWin->mModalStateDepth == 0) { NS_ASSERTION(!mSuspendedDoc, "Shouldn't have mSuspendedDoc here!"); @@ -5959,20 +6134,13 @@ private: void nsGlobalWindow::LeaveModalState() { - nsCOMPtr top; - GetTop(getter_AddRefs(top)); + nsGlobalWindow *topWin = GetTop(); - if (!top) { + if (!topWin) { NS_ERROR("Uh, LeaveModalState() called w/o a reachable top window?"); - return; } - nsGlobalWindow *topWin = - static_cast - (static_cast - (top.get())); - topWin->mModalStateDepth--; if (topWin->mModalStateDepth == 0) { @@ -5994,23 +6162,25 @@ nsGlobalWindow::LeaveModalState() if (cx && (scx = GetScriptContextFromJSContext(cx))) { scx->LeaveModalState(); } + + // Remember the time of the last dialog quit. + nsGlobalWindow *inner = topWin->GetCurrentInnerWindowInternal(); + if (inner) + inner->mLastDialogQuitTime = TimeStamp::Now(); } PRBool nsGlobalWindow::IsInModalState() { - nsCOMPtr top; - GetTop(getter_AddRefs(top)); + nsGlobalWindow *topWin = GetTop(); - if (!top) { + if (!topWin) { NS_ERROR("Uh, IsInModalState() called w/o a reachable top window?"); return PR_FALSE; } - return static_cast - (static_cast - (top.get()))->mModalStateDepth != 0; + return topWin->mModalStateDepth != 0; } // static @@ -6316,7 +6486,12 @@ nsGlobalWindow::ShowModalDialog(const nsAString& aURI, nsIVariant *aArgs, { *aRetVal = nsnull; - NS_ENSURE_TRUE(mDocShell, NS_ERROR_FAILURE); + // Before bringing up the window/dialog, unsuppress painting and flush + // pending reflows. + EnsureReflowFlushAndPaint(); + + if (AreDialogsBlocked() || !ConfirmDialogAllowed()) + return NS_ERROR_NOT_AVAILABLE; nsCOMPtr dlgWin; nsAutoString options(NS_LITERAL_STRING("-moz-internal-modal=1,status=1")); @@ -6325,10 +6500,7 @@ nsGlobalWindow::ShowModalDialog(const nsAString& aURI, nsIVariant *aArgs, options.AppendLiteral(",scrollbars=1,centerscreen=1,resizable=0"); - // Before bringing up the window, unsuppress painting and flush - // pending reflows. - EnsureReflowFlushAndPaint(); - + EnterModalState(); nsresult rv = OpenInternal(aURI, EmptyString(), options, PR_FALSE, // aDialog PR_TRUE, // aContentModal @@ -6338,6 +6510,7 @@ nsGlobalWindow::ShowModalDialog(const nsAString& aURI, nsIVariant *aArgs, GetPrincipal(), // aCalleePrincipal nsnull, // aJSCallerContext getter_AddRefs(dlgWin)); + LeaveModalState(); NS_ENSURE_SUCCESS(rv, rv); diff --git a/dom/base/nsGlobalWindow.h b/dom/base/nsGlobalWindow.h index 0d8f4ccea078..2b14526cfab2 100644 --- a/dom/base/nsGlobalWindow.h +++ b/dom/base/nsGlobalWindow.h @@ -43,6 +43,8 @@ #ifndef nsGlobalWindow_h___ #define nsGlobalWindow_h___ +#include "mozilla/XPCOM.h" // for TimeStamp/TimeDuration + // Local Includes // Helper Classes #include "nsCOMPtr.h" @@ -109,6 +111,14 @@ #define DEFAULT_HOME_PAGE "www.mozilla.org" #define PREF_BROWSER_STARTUP_HOMEPAGE "browser.startup.homepage" +// Amount of time allowed between alert/prompt/confirm before enabling +// the stop dialog checkbox. +#define SUCCESSIVE_DIALOG_TIME_LIMIT 3 // 3 sec + +// During click or mousedown events (and others, see nsDOMEvent) we allow modal +// dialogs up to this limit, even if they were disabled. +#define MAX_DIALOG_COUNT 10 + class nsIDOMBarProp; class nsIDocument; class nsPresContext; @@ -244,6 +254,9 @@ class nsGlobalWindow : public nsPIDOMWindow, public PRCListStr { public: + typedef mozilla::TimeStamp TimeStamp; + typedef mozilla::TimeDuration TimeDuration; + // public methods nsPIDOMWindow* GetPrivateParent(); // callback for close event @@ -395,6 +408,32 @@ public: return FromSupports(wrapper->Native()); } + inline nsGlobalWindow *GetTop() + { + nsCOMPtr top; + GetTop(getter_AddRefs(top)); + if (top) + return static_cast(static_cast(top.get())); + return nsnull; + } + + // Call this when a modal dialog is about to be opened. Returns + // true if we've reached the state in this top level window where we + // ask the user if further dialogs should be blocked. + bool DialogOpenAttempted(); + + // Returns true if dialogs have already been blocked for this + // window. + bool AreDialogsBlocked(); + + // Ask the user if further dialogs should be blocked. This is used + // in the cases where we have no modifiable UI to show, in that case + // we show a separate dialog when asking this question. + bool ConfirmDialogAllowed(); + + // Prevent further dialogs in this (top level) window + void PreventFurtherDialogs(); + nsIScriptContext *GetContextInternal() { if (mOuterWindow) { @@ -864,6 +903,18 @@ protected: // this window. PRUint64 mWindowID; + // In the case of a "trusted" dialog (@see PopupControlState), we + // set this counter to ensure a max of MAX_DIALOG_LIMIT + PRUint32 mDialogAbuseCount; + + // This holds the time when the last modal dialog was shown, if two + // dialogs are shown within CONCURRENT_DIALOG_TIME_LIMIT the + // checkbox is shown. In the case of ShowModalDialog another Confirm + // dialog will be shown, the result of the checkbox/confirm dialog + // will be stored in mDialogDisabled variable. + TimeStamp mLastDialogQuitTime; + PRPackedBool mDialogDisabled; + friend class nsDOMScriptableHelper; friend class nsDOMWindowUtils; friend class PostMessageEvent; diff --git a/dom/tests/mochitest/bugs/Makefile.in b/dom/tests/mochitest/bugs/Makefile.in index bfdd8b6f1d30..a8d6e8c02da7 100644 --- a/dom/tests/mochitest/bugs/Makefile.in +++ b/dom/tests/mochitest/bugs/Makefile.in @@ -123,6 +123,7 @@ _TEST_FILES = \ test_bug585240.html \ test_bug585819.html \ test_bug369306.html \ + test_bug61098.html \ $(NULL) libs:: $(_TEST_FILES) diff --git a/dom/tests/mochitest/bugs/test_bug504862.html b/dom/tests/mochitest/bugs/test_bug504862.html index cecebe1fc686..9f916bf6adb7 100644 --- a/dom/tests/mochitest/bugs/test_bug504862.html +++ b/dom/tests/mochitest/bugs/test_bug504862.html @@ -9,27 +9,31 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=504862 - + Mozilla Bug 504862 diff --git a/dom/tests/mochitest/bugs/test_bug61098.html b/dom/tests/mochitest/bugs/test_bug61098.html new file mode 100644 index 000000000000..eec8c6bd1afe --- /dev/null +++ b/dom/tests/mochitest/bugs/test_bug61098.html @@ -0,0 +1,270 @@ + + + + + Test for Bug 61098 + + + + + + + + +Mozilla Bug 61098 +

+

+ +
+
+ + + diff --git a/toolkit/locales/en-US/chrome/global/commonDialogs.properties b/toolkit/locales/en-US/chrome/global/commonDialogs.properties index 062766c1db72..b04ff39594c8 100644 --- a/toolkit/locales/en-US/chrome/global/commonDialogs.properties +++ b/toolkit/locales/en-US/chrome/global/commonDialogs.properties @@ -14,6 +14,8 @@ Revert=&Revert DontSave=&Don't Save ScriptDlgGenericHeading=[JavaScript Application] ScriptDlgHeading=The page at %S says: +ScriptDialogLabel=Prevent this page from creating additional dialogs +ScriptDialogPreventTitle=Confirm Dialog Preference # LOCALIZATION NOTE (EnterLoginForRealm, EnterLoginForProxy): # %1 is an untrusted string provided by a remote server. It could try to # take advantage of sentence structure in order to mislead the user (see