Bug 391834, don't allow prompts in beforeunload, unload and pagehide events,r=smaug,patch mostly by gavin
authorNeil Deakin <neil@mozilla.com>
Mon, 13 Aug 2012 15:05:34 -0400
changeset 104180 bc02fa8b78492d490b00c1053a204b3212e4f18d
parent 104179 a2b13e2f75ec20b9f4c1aa7177df06c9a17f37fd
child 104181 46f73c07286e4b82d501c39e324131fb0892dcbd
push id37
push usershu@rfrn.org
push dateThu, 16 Aug 2012 01:15:22 +0000
reviewerssmaug, patch
bugs391834
milestone17.0a1
Bug 391834, don't allow prompts in beforeunload, unload and pagehide events,r=smaug,patch mostly by gavin
browser/base/content/tabbrowser.xml
browser/base/content/test/Makefile.in
browser/base/content/test/browser_unloaddialogs.js
browser/components/tabview/test/browser_tabview_bug626455.js
docshell/base/nsIContentViewer.idl
dom/base/nsDOMWindowUtils.cpp
dom/base/nsGlobalWindow.cpp
dom/base/nsGlobalWindow.h
dom/interfaces/base/nsIDOMWindowUtils.idl
layout/base/nsDocumentViewer.cpp
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1611,16 +1611,21 @@
             // We're committed to closing the tab now.
             // Dispatch a notification.
             // We dispatch it before any teardown so that event listeners can
             // inspect the tab that's about to close.
             var evt = document.createEvent("UIEvent");
             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
             // Bug 666801 - WebProgress support for e10s
 #else 
             browser.webProgress.removeProgressListener(filter);
 #endif
             filter.removeProgressListener(this.mTabListeners[aTab._tPos]);
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -194,16 +194,17 @@ endif
                  browser_scope.js \
                  browser_selectTabAtIndex.js \
                  browser_tab_dragdrop.js \
                  browser_tab_dragdrop2.js \
                  browser_tab_dragdrop2_frame1.xul \
                  browser_tabfocus.js \
                  browser_tabs_isActive.js \
                  browser_tabs_owner.js \
+                 browser_unloaddialogs.js \
                  browser_urlbarAutoFillTrimURLs.js \
                  browser_urlbarCopying.js \
                  browser_urlbarEnter.js \
                  browser_urlbarRevert.js \
                  browser_urlbarStop.js \
                  browser_urlbarTrimURLs.js \
                  browser_urlHighlight.js \
                  browser_visibleFindSelection.js \
new file mode 100644
--- /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,<script>" +
+                      "function note(event) { try { alert(event.type); } catch(ex) { return; } throw 'alert appeared'; }" +
+                      "</script>" +
+                      "<body onpagehide='note(event)' onbeforeunload='alert(event.type);' onunload='note(event)'>";
+
+const TEST_URL = TEST_BASE_URL + "Test</body>";
+const TEST_FRAME_URL = TEST_BASE_URL + "Frames</body>";
+
+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,<body>Another Page</body>";
+      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 () {},
+};
--- a/browser/components/tabview/test/browser_tabview_bug626455.js
+++ b/browser/components/tabview/test/browser_tabview_bug626455.js
@@ -57,19 +57,16 @@ function testStayOnPage() {
 
   closeGroupItem(activeGroup);
 }
 
 function testLeavePage() {
   let dialogsAccepted = 0;
 
   whenDialogOpened(function onDialogOpened(dialog) {
-    if (++dialogsAccepted < 3)
-      whenDialogOpened(onDialogOpened);
-
     // Leave page
     dialog.acceptDialog();
   });
 
   whenGroupClosed(activeGroup, finishTest);
   closeGroupItem(activeGroup);
 }
 
--- a/docshell/base/nsIContentViewer.idl
+++ b/docshell/base/nsIContentViewer.idl
@@ -22,17 +22,17 @@ class nsDOMNavigationTiming;
 [ptr] native nsIWidgetPtr(nsIWidget);
 [ptr] native nsIDocumentPtr(nsIDocument);
 [ref] native nsIntRectRef(nsIntRect);
 [ptr] native nsIPresShellPtr(nsIPresShell);
 [ptr] native nsPresContextPtr(nsPresContext);
 [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
 {
 
   [noscript] void init(in nsIWidgetPtr aParentWidget,
                        [const] in nsIntRectRef aBounds);
 
   attribute nsISupports container;
 
@@ -129,37 +129,42 @@ interface nsIContentViewer : nsISupports
   void open(in nsISupports aState, in nsISHEntry aSHEntry);
 
   /**
    * Clears the current history entry.  This is used if we need to clear out
    * the saved presentation state.
    */
   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);
 
   /**
    * Get the history entry that this viewer will save itself into when
    * destroyed.  Can return null
    */
   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
    * rentrant if the user closes the tab while the prompt is showing.
    * See bug 613800.
    */
   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.
   [noscript] void setDocumentInternal(in nsIDocumentPtr aDocument,
                                       in boolean aForceReuseInnerWindow);
   /**
    * Find the view to use as the container view for MakeWindow. Returns
    * null if this will be the root of a view manager hierarchy. In that
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -2282,16 +2282,32 @@ nsDOMWindowUtils::CheckAndClearPaintedSt
     *aResult = false;
     return NS_OK;
   }
 
   *aResult = frame->CheckAndClearPaintedState();
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsDOMWindowUtils::PreventFurtherDialogs()
+{
+  // Permanently disable further dialogs for this window.
+
+  if (!IsUniversalXPConnectCapable()) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
+  nsCOMPtr<nsPIDOMWindow> window = do_QueryReferent(mWindow);
+  NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
+
+  static_cast<nsGlobalWindow*>(window.get())->PreventFurtherDialogs(true);
+  return NS_OK;
+}
+
 static nsresult
 GetFileOrBlob(const nsAString& aName, const jsval& aBlobParts,
               const jsval& aParameters, JSContext* aCx,
               PRUint8 aOptionalArgCount, nsISupports** aResult)
 {
   if (!IsUniversalXPConnectCapable()) {
     return NS_ERROR_DOM_SECURITY_ERR;
   }
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -679,17 +679,18 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalW
     mFocusMethod(0),
     mSerial(0),
 #ifdef DEBUG
     mSetOpenerWindowCalled(false),
 #endif
     mCleanedUp(false),
     mCallCleanUpAfterModalDialogCloses(false),
     mDialogAbuseCount(0),
-    mStopAbuseDialogs(false)
+    mStopAbuseDialogs(false),
+    mDialogsPermanentlyDisabled(false)
 {
   nsLayoutStatics::AddRef();
 
   // Initialize the PRCList (this).
   PR_INIT_CLIST(this);
 
   // Initialize timeout storage
   PR_INIT_CLIST(&mTimeouts);
@@ -2561,16 +2562,32 @@ nsGlobalWindow::DialogsAreBlocked(bool *
     return true;
   }
 
   topWindow = topWindow->GetCurrentInnerWindowInternal();
   if (!topWindow) {
     return true;
   }
 
+  if (topWindow->mDialogsPermanentlyDisabled) {
+    return true;
+  }
+
+  // Dialogs are blocked if the content viewer is hidden
+  if (mDocShell) {
+    nsCOMPtr<nsIContentViewer> cv;
+    mDocShell->GetContentViewer(getter_AddRefs(cv));
+
+    bool isHidden;
+    cv->GetIsHidden(&isHidden);
+    if (isHidden) {
+      return true;
+    }
+  }
+
   *aBeingAbused = topWindow->DialogsAreBeingAbused();
 
   return topWindow->mStopAbuseDialogs && *aBeingAbused;
 }
 
 bool
 nsGlobalWindow::DialogsAreBeingAbused()
 {
@@ -2620,35 +2637,38 @@ nsGlobalWindow::ConfirmDialogIfNeeded()
   bool disableDialog = 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();
+    PreventFurtherDialogs(false);
     return false;
   }
 
   return true;
 }
 
 void
-nsGlobalWindow::PreventFurtherDialogs()
+nsGlobalWindow::PreventFurtherDialogs(bool aPermanent)
 {
   nsGlobalWindow *topWindow = GetScriptableTop();
   if (!topWindow) {
     NS_ERROR("PreventFurtherDialogs() called without a top window?");
     return;
   }
 
   topWindow = topWindow->GetCurrentInnerWindowInternal();
   if (topWindow) {
     topWindow->mStopAbuseDialogs = true;
+    if (aPermanent) {
+      topWindow->mDialogsPermanentlyDisabled = true;
+    }
   }
 }
 
 nsresult
 nsGlobalWindow::PostHandleEvent(nsEventChainPostVisitor& aVisitor)
 {
   NS_PRECONDITION(IsInnerWindow(), "PostHandleEvent is used on outer window!?");
 
@@ -4842,17 +4862,17 @@ nsGlobalWindow::Alert(const nsAString& a
     bool disallowDialog = false;
     nsXPIDLString label;
     nsContentUtils::GetLocalizedString(nsContentUtils::eCOMMON_DIALOG_PROPERTIES,
                                        "ScriptDialogLabel", label);
 
     rv = prompt->AlertCheck(title.get(), final.get(), label.get(),
                             &disallowDialog);
     if (disallowDialog)
-      PreventFurtherDialogs();
+      PreventFurtherDialogs(false);
   } else {
     rv = prompt->Alert(title.get(), final.get());
   }
 
   return rv;
 }
 
 NS_IMETHODIMP
@@ -4909,17 +4929,17 @@ nsGlobalWindow::Confirm(const nsAString&
     bool disallowDialog = false;
     nsXPIDLString label;
     nsContentUtils::GetLocalizedString(nsContentUtils::eCOMMON_DIALOG_PROPERTIES,
                                        "ScriptDialogLabel", label);
 
     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);
   }
 
   return rv;
 }
 
 NS_IMETHODIMP
@@ -4985,17 +5005,17 @@ nsGlobalWindow::Prompt(const nsAString& 
   nsAutoSyncOperation sync(GetCurrentInnerWindowInternal() ? 
                              GetCurrentInnerWindowInternal()->mDoc :
                              nullptr);
   bool ok;
   rv = prompt->Prompt(title.get(), fixedMessage.get(),
                       &inoutValue, label.get(), &disallowDialog, &ok);
 
   if (disallowDialog) {
-    PreventFurtherDialogs();
+    PreventFurtherDialogs(false);
   }
 
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsAdoptingString outValue(inoutValue);
 
   if (ok && outValue) {
     aReturn.Assign(outValue);
--- a/dom/base/nsGlobalWindow.h
+++ b/dom/base/nsGlobalWindow.h
@@ -448,17 +448,17 @@ public:
   bool DialogsAreBeingAbused();
 
   // Ask the user if further dialogs should be blocked, if dialogs are currently
   // being abused. This is used in the cases where we have no modifiable UI to
   // show, in that case we show a separate dialog to ask this question.
   bool ConfirmDialogIfNeeded();
 
   // Prevent further dialogs in this (top level) window
-  void PreventFurtherDialogs();
+  void PreventFurtherDialogs(bool aPermanent);
 
   virtual void SetHasAudioAvailableEventListeners();
 
   nsIScriptContext *GetContextInternal()
   {
     if (mOuterWindow) {
       return GetOuterWindowInternal()->mContext;
     }
@@ -1054,16 +1054,21 @@ protected:
   // to allow disabling of further dialogs from this window.
   TimeStamp                     mLastDialogQuitTime;
 
   // This is set to true once the user has opted-in to preventing further
   // dialogs for this window. Subsequent dialogs may still open if
   // 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<nsDOMMozURLProperty> mURLProperty;
 
   nsTHashtable<nsPtrHashKey<nsDOMEventTargetHelper> > mEventTargetObjects;
 
   nsTArray<PRUint32> mEnabledSensors;
 
   friend class nsDOMScriptableHelper;
   friend class nsDOMWindowUtils;
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -34,17 +34,17 @@ interface nsITransferable;
 interface nsIQueryContentEventResult;
 interface nsIDOMWindow;
 interface nsIDOMBlob;
 interface nsIDOMFile;
 interface nsIFile;
 interface nsIDOMTouch;
 interface nsIDOMClientRect;
 
-[scriptable, uuid(97548ee0-9def-421a-ab2a-c6c98efa9a3c)]
+[scriptable, uuid(5fc61d7b-a303-4f34-adfe-b7828675ba45)]
 interface nsIDOMWindowUtils : nsISupports {
 
   /**
    * Image animation mode of the window. When this attribute's value
    * is changed, the implementation should set all images in the window
    * to the given value. That is, when set to kDontAnimMode, all images
    * will stop animating. The attribute's value must be one of the
    * animationMode values from imgIContainer.
@@ -1178,9 +1178,15 @@ interface nsIDOMWindowUtils : nsISupport
 
   /**
    * Set the scrollport size for the purposes of clamping scroll positions for
    * the root scroll frame of this document to be (aWidth,aHeight) in CSS pixels.
    *
    * 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();
 };
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -4305,16 +4305,23 @@ DocumentViewerImpl::GetHistoryEntry(nsIS
 
 NS_IMETHODIMP
 DocumentViewerImpl::GetIsTabModalPromptAllowed(bool *aAllowed)
 {
   *aAllowed = !(mInPermitUnload || mHidden);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+DocumentViewerImpl::GetIsHidden(bool *aHidden)
+{
+  *aHidden = mHidden;
+  return NS_OK;
+}
+
 void
 DocumentViewerImpl::DestroyPresShell()
 {
   // Break circular reference (or something)
   mPresShell->EndObservingDocument();
 
   nsCOMPtr<nsISelection> selection;
   GetDocumentSelection(getter_AddRefs(selection));