Bug 967873 - Proxy nsDocumentViewer::PermitUnload to the child process (r=Gijs)
authorBill McCloskey <billm@mozilla.com>
Wed, 01 Jul 2015 15:32:06 -0700
changeset 299074 69e9851fe871c816bf37fe80497c78dc25777487
parent 299073 c98a08ab12b358ec2843e1ea08659c93e475f90e
child 299075 17da3f535e295d8f104e17ac035c3ff176cdbd54
push id5392
push userraliiev@mozilla.com
push dateMon, 14 Dec 2015 20:08:23 +0000
treeherdermozilla-beta@16ce8562a975 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs967873
milestone44.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 967873 - Proxy nsDocumentViewer::PermitUnload to the child process (r=Gijs)
browser/base/content/browser.js
browser/base/content/chatWindow.xul
browser/base/content/sanitize.js
browser/base/content/tabbrowser.xml
docshell/base/nsDocShell.cpp
docshell/base/nsIContentViewer.idl
dom/base/nsGlobalWindow.cpp
dom/html/nsHTMLDocument.cpp
dom/interfaces/base/nsIBrowserDOMWindow.idl
dom/jsurl/nsJSProtocolHandler.cpp
layout/base/nsDocumentViewer.cpp
mobile/android/chrome/content/browser.js
services/fxaccounts/FxAccountsOAuthClient.jsm
toolkit/content/browser-child.js
toolkit/content/widgets/browser.xml
toolkit/content/widgets/remote-browser.xml
toolkit/modules/BrowserUtils.jsm
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4916,16 +4916,20 @@ nsBrowserAccess.prototype = {
       return browser.QueryInterface(Ci.nsIFrameLoaderOwner);
 
     return null;
   },
 
   isTabContentWindow: function (aWindow) {
     return gBrowser.browsers.some(browser => browser.contentWindow == aWindow);
   },
+
+  canClose() {
+    return CanCloseWindow();
+  },
 }
 
 function getTogglableToolbars() {
   let toolbarNodes = Array.slice(gNavToolbox.childNodes);
   toolbarNodes = toolbarNodes.concat(gNavToolbox.externalToolbars);
   toolbarNodes = toolbarNodes.filter(node => node.getAttribute("toolbarname"));
   return toolbarNodes;
 }
@@ -6380,47 +6384,59 @@ var IndexedDBPromptHelper = {
 
     // Set the timeoutId after the popup has been created, and use the long
     // timeout value. If the user doesn't notice the popup after this amount of
     // time then it is most likely not visible and we want to alert the page.
     timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
   }
 };
 
+function CanCloseWindow()
+{
+  // Avoid redundant calls to canClose from showing multiple
+  // PermitUnload dialogs.
+  if (window.skipNextCanClose) {
+    return true;
+  }
+
+  for (let browser of gBrowser.browsers) {
+    let {permitUnload, timedOut} = browser.permitUnload();
+    if (timedOut) {
+      return true;
+    }
+    if (!permitUnload) {
+      return false;
+    }
+  }
+  return true;
+}
+
 function WindowIsClosing()
 {
   if (TabView.isVisible()) {
     TabView.hide();
     return false;
   }
 
   if (!closeWindow(false, warnAboutClosingWindow))
     return false;
 
-  // Bug 967873 - Proxy nsDocumentViewer::PermitUnload to the child process
-  if (gMultiProcessBrowser)
+  // In theory we should exit here and the Window's internal Close
+  // method should trigger canClose on nsBrowserAccess. However, by
+  // that point it's too late to be able to show a prompt for
+  // PermitUnload. So we do it here, when we still can.
+  if (CanCloseWindow()) {
+    // This flag ensures that the later canClose call does nothing.
+    // It's only needed to make tests pass, since they detect the
+    // prompt even when it's not actually shown.
+    window.skipNextCanClose = true;
     return true;
-
-  for (let browser of gBrowser.browsers) {
-    let ds = browser.docShell;
-    // Passing true to permitUnload indicates we plan on closing the window.
-    // This means that once unload is permitted, all further calls to
-    // permitUnload will be ignored. This avoids getting multiple prompts
-    // to unload the page.
-    if (ds.contentViewer && !ds.contentViewer.permitUnload(true)) {
-      // ... however, if the user aborts closing, we need to undo that,
-      // to ensure they get prompted again when we next try to close the window.
-      // We do this on the window's toplevel docshell instead of on the tab, so
-      // that all tabs we iterated before will get this reset.
-      window.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow();
-      return false;
-    }
-  }
-
-  return true;
+  }
+
+  return false;
 }
 
 /**
  * Checks if this is the last full *browser* window around. If it is, this will
  * be communicated like quitting. Otherwise, we warn about closing multiple tabs.
  * @returns true if closing can proceed, false if it got cancelled.
  */
 function warnAboutClosingWindow() {
--- a/browser/base/content/chatWindow.xul
+++ b/browser/base/content/chatWindow.xul
@@ -126,16 +126,21 @@ chatBrowserAccess.prototype = {
   openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aContext) {
     let browser = this._openURIInNewTab(aURI, aWhere);
     return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
   },
 
   isTabContentWindow: function (aWindow) {
     return this.contentWindow == aWindow;
   },
+
+  canClose() {
+    let {BrowserUtils} = Cu.import("resource://gre/modules/BrowserUtils.jsm", {});
+    return BrowserUtils.canCloseWindow(window);
+  },
 };
 
 </script>
 
 #include browser-sets.inc
 
 #ifdef XP_MACOSX
 #include browser-menubar.inc
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -1,9 +1,9 @@
-// -*- indent-tabs-mode: nil; js-indent-level: 4 -*-
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
@@ -562,38 +562,27 @@ Sanitizer.prototype = {
       get canClear()
       {
         return true;
       }
     },
     openWindows: {
       privateStateForNewWindow: "non-private",
       _canCloseWindow: function(aWindow) {
-        // Bug 967873 - Proxy nsDocumentViewer::PermitUnload to the child process
-        if (!aWindow.gMultiProcessBrowser) {
-          // Cargo-culted out of browser.js' WindowIsClosing because we don't care
-          // about TabView or the regular 'warn me before closing windows with N tabs'
-          // stuff here, and more importantly, we want to set aCallerClosesWindow to true
-          // when calling into permitUnload:
-          for (let browser of aWindow.gBrowser.browsers) {
-            let ds = browser.docShell;
-            // 'true' here means we will be closing the window soon, so please don't dispatch
-            // another onbeforeunload event when we do so. If unload is *not* permitted somewhere,
-            // we will reset the flag that this triggers everywhere so that we don't interfere
-            // with the browser after all:
-            if (ds.contentViewer && !ds.contentViewer.permitUnload(true)) {
-              return false;
-            }
-          }
+        if (aWindow.CanCloseWindow()) {
+          // We already showed PermitUnload for the window, so let's
+          // make sure we don't do it again when we actually close the
+          // window.
+          aWindow.skipNextCanClose = true;
+          return true;
         }
-        return true;
       },
       _resetAllWindowClosures: function(aWindowList) {
         for (let win of aWindowList) {
-          win.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow();
+          win.skipNextCanClose = false;
         }
       },
       clear: Task.async(function*() {
         // NB: this closes all *browser* windows, not other windows like the library, about window,
         // browser console, etc.
 
         // Keep track of the time in case we get stuck in la-la-land because of onbeforeunload
         // dialogs
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1185,35 +1185,39 @@
                 }
               });
               this.mCurrentTab.dispatchEvent(event);
 
               this._tabAttrModified(oldTab, ["selected"]);
               this._tabAttrModified(this.mCurrentTab, ["selected"]);
 
               if (oldBrowser != newBrowser &&
-                  oldBrowser.docShell &&
-                  oldBrowser.docShell.contentViewer.inPermitUnload) {
-                // Since the user is switching away from a tab that has
-                // a beforeunload prompt active, we remove the prompt.
-                // This prevents confusing user flows like the following:
-                //   1. User attempts to close Firefox
-                //   2. User switches tabs (ingoring a beforeunload prompt)
-                //   3. User returns to tab, presses "Leave page"
-                let promptBox = this.getTabModalPromptBox(oldBrowser);
-                let prompts = promptBox.listPrompts();
-                // There might not be any prompts here if the tab was closed
-                // while in an onbeforeunload prompt, which will have
-                // destroyed aforementioned prompt already, so check there's
-                // something to remove, first:
-                if (prompts.length) {
-                  // NB: This code assumes that the beforeunload prompt
-                  //     is the top-most prompt on the tab.
-                  prompts[prompts.length - 1].abortPrompt();
-                }
+                  oldBrowser.getInPermitUnload) {
+                oldBrowser.getInPermitUnload(inPermitUnload => {
+                  if (!inPermitUnload) {
+                    return;
+                  }
+                  // Since the user is switching away from a tab that has
+                  // a beforeunload prompt active, we remove the prompt.
+                  // This prevents confusing user flows like the following:
+                  //   1. User attempts to close Firefox
+                  //   2. User switches tabs (ingoring a beforeunload prompt)
+                  //   3. User returns to tab, presses "Leave page"
+                  let promptBox = this.getTabModalPromptBox(oldBrowser);
+                  let prompts = promptBox.listPrompts();
+                  // There might not be any prompts here if the tab was closed
+                  // while in an onbeforeunload prompt, which will have
+                  // destroyed aforementioned prompt already, so check there's
+                  // something to remove, first:
+                  if (prompts.length) {
+                    // NB: This code assumes that the beforeunload prompt
+                    //     is the top-most prompt on the tab.
+                    prompts[prompts.length - 1].abortPrompt();
+                  }
+                });
               }
 
               oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused);
               if (this.isFindBarInitialized(oldTab)) {
                 let findBar = this.getFindBar(oldTab);
                 oldTab._findBarFocused = (!findBar.hidden &&
                   findBar._findField.getAttribute("focused") == "true");
               }
@@ -2119,29 +2123,30 @@
       <method name="removeTab">
         <parameter name="aTab"/>
         <parameter name="aParams"/>
         <body>
           <![CDATA[
             if (aParams) {
               var animate = aParams.animate;
               var byMouse = aParams.byMouse;
+              var skipPermitUnload = aParams.skipPermitUnload;
             }
 
             // Handle requests for synchronously removing an already
             // asynchronously closing tab.
             if (!animate &&
                 aTab.closing) {
               this._endRemoveTab(aTab);
               return;
             }
 
             var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
 
-            if (!this._beginRemoveTab(aTab, false, null, true))
+            if (!this._beginRemoveTab(aTab, false, null, true, skipPermitUnload))
               return;
 
             if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse)
               this.tabContainer._lockTabSizing(aTab);
             else
               this.tabContainer._unlockTabSizing();
 
             if (!animate /* the caller didn't opt in */ ||
@@ -2179,16 +2184,17 @@
         false
       </field>
 
       <method name="_beginRemoveTab">
         <parameter name="aTab"/>
         <parameter name="aTabWillBeMoved"/>
         <parameter name="aCloseWindowWithLastTab"/>
         <parameter name="aCloseWindowFastpath"/>
+        <parameter name="aSkipPermitUnload"/>
         <body>
           <![CDATA[
             if (aTab.closing ||
                 this._windowIsClosing)
               return false;
 
             var browser = this.getBrowserForTab(aTab);
 
@@ -2209,33 +2215,30 @@
                 // cancels the operation.  We are finished here in both cases.
                 this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow);
                 return null;
               }
 
               newTab = true;
             }
 
-            if (!aTab._pendingPermitUnload && !aTabWillBeMoved) {
-              let ds = browser.docShell;
-              if (ds && ds.contentViewer) {
-                // We need to block while calling permitUnload() because it
-                // processes the event queue and may lead to another removeTab()
-                // call before permitUnload() returns.
-                aTab._pendingPermitUnload = true;
-                let permitUnload = ds.contentViewer.permitUnload();
-                delete aTab._pendingPermitUnload;
-                // If we were closed during onbeforeunload, we return false now
-                // so we don't (try to) close the same tab again. Of course, we
-                // also stop if the unload was cancelled by the user:
-                if (aTab.closing || !permitUnload) {
-                  // NB: deliberately keep the _closedDuringPermitUnload set to
-                  // true so we keep exiting early in case of multiple calls.
-                  return false;
-                }
+            if (!aTab._pendingPermitUnload && !aTabWillBeMoved && !aSkipPermitUnload) {
+              // We need to block while calling permitUnload() because it
+              // processes the event queue and may lead to another removeTab()
+              // call before permitUnload() returns.
+              aTab._pendingPermitUnload = true;
+              let {permitUnload} = browser.permitUnload();
+              delete aTab._pendingPermitUnload;
+              // If we were closed during onbeforeunload, we return false now
+              // so we don't (try to) close the same tab again. Of course, we
+              // also stop if the unload was cancelled by the user:
+              if (aTab.closing || !permitUnload) {
+                // NB: deliberately keep the _closedDuringPermitUnload set to
+                // true so we keep exiting early in case of multiple calls.
+                return false;
               }
             }
 
             aTab.closing = true;
             this._removingTabs.push(aTab);
             this._visibleTabs = null; // invalidate cache
 
             // Invalidate hovered tab state tracking for this closing tab.
@@ -4039,23 +4042,29 @@
                 return;
               let titleChanged = this.setTabTitle(tab);
               if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
                 tab.setAttribute("titlechanged", "true");
               break;
             }
             case "DOMWindowClose": {
               if (this.tabs.length == 1) {
+                // We already did PermitUnload in the content process
+                // for this tab (the only one in the window). So we don't
+                // need to do it again for any tabs.
+                window.skipNextCanClose = true;
                 window.close();
                 return;
               }
 
               let tab = this.getTabForBrowser(browser);
               if (tab) {
-                this.removeTab(tab);
+                // Skip running PermitUnload since it already happened in
+                // the content process.
+                this.removeTab(tab, {skipPermitUnload: true});
               }
               break;
             }
             case "contextmenu": {
               let spellInfo = aMessage.data.spellInfo;
               if (spellInfo)
                 spellInfo.target = aMessage.target.messageManager;
               let documentURIObject = makeURI(aMessage.data.docLocation,
@@ -4329,22 +4338,28 @@
     </implementation>
 
     <handlers>
       <handler event="DOMWindowClose" phase="capturing">
         <![CDATA[
           if (!event.isTrusted)
             return;
 
-          if (this.tabs.length == 1)
+          if (this.tabs.length == 1) {
+            // We already did PermitUnload in nsGlobalWindow::Close
+            // for this tab. There are no other tabs we need to do
+            // PermitUnload for.
+            window.skipNextCanClose = true;
             return;
+          }
 
           var tab = this._getTabForContentWindow(event.target);
           if (tab) {
-            this.removeTab(tab);
+            // Skip running PermitUnload since it already happened.
+            this.removeTab(tab, {skipPermitUnload: true});
             event.preventDefault();
           }
         ]]>
       </handler>
       <handler event="DOMWillOpenModalDialog" phase="capturing">
         <![CDATA[
           if (!event.isTrusted)
             return;
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -7759,17 +7759,17 @@ nsDocShell::CreateAboutBlankContentViewe
     // with about:blank. And also ensure we fire the unload events
     // in the current document.
 
     // Unload gets fired first for
     // document loaded from the session history.
     mTiming->NotifyBeforeUnload();
 
     bool okToUnload;
-    rv = mContentViewer->PermitUnload(false, &okToUnload);
+    rv = mContentViewer->PermitUnload(&okToUnload);
 
     if (NS_SUCCEEDED(rv) && !okToUnload) {
       // The user chose not to unload the page, interrupt the load.
       return NS_ERROR_FAILURE;
     }
 
     mSavingOldViewer = aTryToSaveOldPresentation &&
                        CanSavePresentation(LOAD_NORMAL, nullptr, nullptr);
@@ -10092,17 +10092,17 @@ nsDocShell::InternalLoad(nsIURI* aURI,
   bool timeBeforeUnload = aFileName.IsVoid();
   if (mTiming && timeBeforeUnload) {
     mTiming->NotifyBeforeUnload();
   }
   // Check if the page doesn't want to be unloaded. The javascript:
   // protocol handler deals with this for javascript: URLs.
   if (!isJavaScript && aFileName.IsVoid() && mContentViewer) {
     bool okToUnload;
-    rv = mContentViewer->PermitUnload(false, &okToUnload);
+    rv = mContentViewer->PermitUnload(&okToUnload);
 
     if (NS_SUCCEEDED(rv) && !okToUnload) {
       // The user chose not to unload the page, interrupt the
       // load.
       return NS_OK;
     }
   }
 
--- a/docshell/base/nsIContentViewer.idl
+++ b/docshell/base/nsIContentViewer.idl
@@ -26,67 +26,52 @@ class nsDOMNavigationTiming;
 [ptr] native nsIWidgetPtr(nsIWidget);
 [ref] native nsIntRectRef(nsIntRect);
 [ptr] native nsIPresShellPtr(nsIPresShell);
 [ptr] native nsPresContextPtr(nsPresContext);
 [ptr] native nsViewPtr(nsView);
 [ptr] native nsDOMNavigationTimingPtr(nsDOMNavigationTiming);
 [ref] native nsIContentViewerTArray(nsTArray<nsCOMPtr<nsIContentViewer> >);
 
-[scriptable, builtinclass, uuid(702e0a92-7d63-490e-b5ee-d247e6bd4588)]
+[scriptable, builtinclass, uuid(91b6c1f3-fc5f-43a9-88f4-9286bd19387f)]
 interface nsIContentViewer : nsISupports
 {
 
   [noscript] void init(in nsIWidgetPtr aParentWidget,
                        [const] in nsIntRectRef aBounds);
 
   attribute nsIDocShell container;
 
   [noscript,notxpcom,nostdcall] void loadStart(in nsIDocument aDoc);
   void loadComplete(in nsresult aStatus);
 
   /**
    * 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.
-   *
-   * @param aCallerClosesWindow indicates that the current caller will close the
-   *        window. If the method returns true, all subsequent calls will be
-   *        ignored.
    */
-  boolean permitUnload([optional] in boolean aCallerClosesWindow);
+  boolean permitUnload();
 
   /**
    * Exposes whether we're blocked in a call to permitUnload.
    */
   readonly attribute boolean inPermitUnload;
 
   /**
    * As above, but this passes around the aShouldPrompt 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(in boolean aCallerClosesWindow,
-                                                    inout boolean aShouldPrompt);
+  [noscript,nostdcall] boolean permitUnloadInternal(inout boolean aShouldPrompt);
 
   /**
    * Exposes whether we're in the process of firing the beforeunload event.
    * In this case, the corresponding docshell will not allow navigation.
    */
   readonly attribute boolean beforeUnloadFiring;
 
-  /**
-   * Works in tandem with permitUnload, if the caller decides not to close the
-   * window it indicated it will, it is the caller's responsibility to reset
-   * that with this method.
-   *
-   * @Note this method is only meant to be called on documents for which the
-   *  caller has indicated that it will close the window. If that is not the case
-   *  the behavior of this method is undefined.
-   */
-  void resetCloseWindow();
   void pageHide(in boolean isUnload);
 
   /**
    * All users of a content viewer are responsible for calling both
    * close() and destroy(), in that order. 
    *
    * close() should be called when the load of a new page for the next
    * content viewer begins, and destroy() should be called when the next
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -8749,30 +8749,41 @@ public:
 
 };
 
 bool
 nsGlobalWindow::CanClose()
 {
   MOZ_ASSERT(IsOuterWindow());
 
+  if (mIsChrome) {
+    nsCOMPtr<nsIBrowserDOMWindow> bwin;
+    nsIDOMChromeWindow* chromeWin = static_cast<nsGlobalChromeWindow*>(this);
+    chromeWin->GetBrowserDOMWindow(getter_AddRefs(bwin));
+
+    bool canClose = true;
+    if (bwin && NS_SUCCEEDED(bwin->CanClose(&canClose))) {
+      return canClose;
+    }
+  }
+
   if (!mDocShell) {
     return true;
   }
 
   // Ask the content viewer whether the toplevel window can close.
   // If the content viewer returns false, it is responsible for calling
   // Close() as soon as it is possible for the window to close.
   // This allows us to not close the window while printing is happening.
 
   nsCOMPtr<nsIContentViewer> cv;
   mDocShell->GetContentViewer(getter_AddRefs(cv));
   if (cv) {
     bool canClose;
-    nsresult rv = cv->PermitUnload(false, &canClose);
+    nsresult rv = cv->PermitUnload(&canClose);
     if (NS_SUCCEEDED(rv) && !canClose)
       return false;
 
     rv = cv->RequestWindowClose(&canClose);
     if (NS_SUCCEEDED(rv) && !canClose)
       return false;
   }
 
--- a/dom/html/nsHTMLDocument.cpp
+++ b/dom/html/nsHTMLDocument.cpp
@@ -1514,17 +1514,17 @@ nsHTMLDocument::Open(JSContext* cx,
 
   // Stop current loads targeted at the window this document is in.
   if (mScriptGlobalObject) {
     nsCOMPtr<nsIContentViewer> cv;
     shell->GetContentViewer(getter_AddRefs(cv));
 
     if (cv) {
       bool okToUnload;
-      if (NS_SUCCEEDED(cv->PermitUnload(false, &okToUnload)) && !okToUnload) {
+      if (NS_SUCCEEDED(cv->PermitUnload(&okToUnload)) && !okToUnload) {
         // We don't want to unload, so stop here, but don't throw an
         // exception.
         nsCOMPtr<nsIDocument> ret = this;
         return ret.forget();
       }
     }
 
     nsCOMPtr<nsIWebNavigation> webnav(do_QueryInterface(shell));
--- a/dom/interfaces/base/nsIBrowserDOMWindow.idl
+++ b/dom/interfaces/base/nsIBrowserDOMWindow.idl
@@ -12,17 +12,17 @@ interface nsIFrameLoaderOwner;
 [scriptable, uuid(e774db14-79ac-4156-a7a3-aa3fd0a22c10)]
 
 interface nsIOpenURIInFrameParams : nsISupports
 {
   attribute DOMString referrer;
   attribute boolean isPrivate;
 };
 
-[scriptable, uuid(99f5a347-722c-4337-bd38-f14ec94801b3)]
+[scriptable, uuid(31da1ce2-aec4-4c26-ac66-d622935c3bf4)]
 
 /**
  * The C++ source has access to the browser script source through
  * nsIBrowserDOMWindow. It is intended to be attached to the chrome DOMWindow
  * of a toplevel browser window (a XUL window). A DOMWindow that does not
  * happen to be a browser chrome window will simply have no access to any such
  * interface.
  */
@@ -94,11 +94,19 @@ interface nsIBrowserDOMWindow : nsISuppo
   nsIFrameLoaderOwner openURIInFrame(in nsIURI aURI, in nsIOpenURIInFrameParams params,
                                      in short aWhere, in short aContext);
 
   /**
    * @param  aWindow the window to test.
    * @return whether the window is the main content window for any
    *         currently open tab in this toplevel browser window.
    */
-  boolean      isTabContentWindow(in nsIDOMWindow aWindow);
+  boolean isTabContentWindow(in nsIDOMWindow aWindow);
+
+  /**
+   * This function is responsible for calling
+   * nsIContentViewer::PermitUnload on each frame in the window. It
+   * returns true if closing the window is allowed. See canClose() in
+   * BrowserUtils.jsm for a simple implementation of this method.
+   */
+  boolean canClose();
 };
 
--- a/dom/jsurl/nsJSProtocolHandler.cpp
+++ b/dom/jsurl/nsJSProtocolHandler.cpp
@@ -744,17 +744,17 @@ nsJSChannel::EvaluateScript()
         NS_QueryNotificationCallbacks(mStreamChannel, docShell);
         if (docShell) {
             nsCOMPtr<nsIContentViewer> cv;
             docShell->GetContentViewer(getter_AddRefs(cv));
 
             if (cv) {
                 bool okToUnload;
 
-                if (NS_SUCCEEDED(cv->PermitUnload(false, &okToUnload)) &&
+                if (NS_SUCCEEDED(cv->PermitUnload(&okToUnload)) &&
                     !okToUnload) {
                     // The user didn't want to unload the current
                     // page, translate this into an undefined
                     // return from the javascript: URL...
                     mStatus = NS_ERROR_DOM_RETVAL_UNDEFINED;
                 }
             }
         }
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -405,17 +405,16 @@ protected:
 #endif // NS_PRINTING
 
   /* character set member data */
   int32_t mHintCharsetSource;
   nsCString mHintCharset;
   nsCString mForceCharacterSet;
   
   bool mIsPageMode;
-  bool mCallerIsClosingWindow;
   bool mInitializedForPrintPreview;
   bool mHidden;
 };
 
 class nsPrintEventDispatcher
 {
 public:
   explicit nsPrintEventDispatcher(nsIDocument* aTop) : mTop(aTop)
@@ -456,17 +455,16 @@ NS_NewContentViewer()
 }
 
 void nsDocumentViewer::PrepareToStartLoad()
 {
   mStopped          = false;
   mLoaded           = false;
   mAttachedToParent = false;
   mDeferredWindowClose = false;
-  mCallerIsClosingWindow = false;
 
 #ifdef NS_PRINTING
   mPrintIsPending        = false;
   mPrintDocIsFullyLoaded = false;
   mClosingWhilePrinting  = false;
 
   // Make sure we have destroyed it and cleared the data member
   if (mPrintEngine) {
@@ -1044,37 +1042,33 @@ nsDocumentViewer::LoadComplete(nsresult 
     mCachedPrintWebProgressListner = nullptr;
   }
 #endif
 
   return rv;
 }
 
 NS_IMETHODIMP
-nsDocumentViewer::PermitUnload(bool aCallerClosesWindow,
-                               bool *aPermitUnload)
+nsDocumentViewer::PermitUnload(bool *aPermitUnload)
 {
   bool shouldPrompt = true;
-  return PermitUnloadInternal(aCallerClosesWindow, &shouldPrompt,
-                              aPermitUnload);
+  return PermitUnloadInternal(&shouldPrompt, aPermitUnload);
 }
 
 
 nsresult
-nsDocumentViewer::PermitUnloadInternal(bool aCallerClosesWindow,
-                                       bool *aShouldPrompt,
+nsDocumentViewer::PermitUnloadInternal(bool *aShouldPrompt,
                                        bool *aPermitUnload)
 {
   AutoDontWarnAboutSyncXHR disableSyncXHRWarning;
 
   *aPermitUnload = true;
 
   if (!mDocument
    || mInPermitUnload
-   || mCallerIsClosingWindow
    || mInPermitUnloadPrompt) {
     return NS_OK;
   }
 
   static bool sIsBeforeUnloadDisabled;
   static bool sBeforeUnloadRequiresInteraction;
   static bool sBeforeUnloadPrefsCached = false;
 
@@ -1240,26 +1234,22 @@ nsDocumentViewer::PermitUnloadInternal(b
 
       nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(item));
 
       if (docShell) {
         nsCOMPtr<nsIContentViewer> cv;
         docShell->GetContentViewer(getter_AddRefs(cv));
 
         if (cv) {
-          cv->PermitUnloadInternal(aCallerClosesWindow, aShouldPrompt,
-                                   aPermitUnload);
+          cv->PermitUnloadInternal(aShouldPrompt, aPermitUnload);
         }
       }
     }
   }
 
-  if (aCallerClosesWindow && *aPermitUnload)
-    mCallerIsClosingWindow = true;
-
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDocumentViewer::GetBeforeUnloadFiring(bool* aInEvent)
 {
   *aInEvent = mInPermitUnload;
   return NS_OK;
@@ -1268,45 +1258,16 @@ nsDocumentViewer::GetBeforeUnloadFiring(
 NS_IMETHODIMP
 nsDocumentViewer::GetInPermitUnload(bool* aInEvent)
 {
   *aInEvent = mInPermitUnloadPrompt;
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsDocumentViewer::ResetCloseWindow()
-{
-  mCallerIsClosingWindow = false;
-
-  nsCOMPtr<nsIDocShell> docShell(mContainer);
-  if (docShell) {
-    int32_t childCount;
-    docShell->GetChildCount(&childCount);
-
-    for (int32_t i = 0; i < childCount; ++i) {
-      nsCOMPtr<nsIDocShellTreeItem> item;
-      docShell->GetChildAt(i, getter_AddRefs(item));
-
-      nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(item));
-
-      if (docShell) {
-        nsCOMPtr<nsIContentViewer> cv;
-        docShell->GetContentViewer(getter_AddRefs(cv));
-
-        if (cv) {
-          cv->ResetCloseWindow();
-        }
-      }
-    }
-  }
-  return NS_OK;
-}
-
-NS_IMETHODIMP
 nsDocumentViewer::PageHide(bool aIsUnload)
 {
   AutoDontWarnAboutSyncXHR disableSyncXHRWarning;
 
   mHidden = true;
 
   if (!mDocument) {
     return NS_ERROR_NULL_POINTER;
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -56,16 +56,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
 
 if (AppConstants.MOZ_SAFE_BROWSING) {
   XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
                                     "resource://gre/modules/SafeBrowsing.jsm");
 }
 
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+                                  "resource://gre/modules/BrowserUtils.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
                                   "resource://gre/modules/Sanitizer.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
                                   "resource://gre/modules/Prompt.jsm");
@@ -3391,16 +3394,20 @@ nsBrowserAccess.prototype = {
   openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aContext) {
     let browser = this._getBrowser(aURI, null, aWhere, aContext);
     return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
   },
 
   isTabContentWindow: function(aWindow) {
     return BrowserApp.getBrowserForWindow(aWindow) != null;
   },
+
+  canClose() {
+    return BrowserUtils.canCloseWindow(window);
+  },
 };
 
 
 // track the last known screen size so that new tabs
 // get created with the right size rather than being 1x1
 var gScreenWidth = 1;
 var gScreenHeight = 1;
 
--- a/services/fxaccounts/FxAccountsOAuthClient.jsm
+++ b/services/fxaccounts/FxAccountsOAuthClient.jsm
@@ -190,16 +190,35 @@ this.FxAccountsOAuthClient.prototype = {
               err = new Error("OAuth flow failed. Keys were not returned");
             } else {
               result = {
                 code: data.code,
                 state: data.state
               };
             }
 
+            // if the message asked to close the tab
+            if (data.closeWindow && target) {
+              // for e10s reasons the best way is to use the TabBrowser to close the tab.
+              let tabbrowser = target.getTabBrowser();
+
+              if (tabbrowser) {
+                let tab = tabbrowser.getTabForBrowser(target);
+
+                if (tab) {
+                  tabbrowser.removeTab(tab);
+                  log.debug("OAuth flow closed the tab.");
+                } else {
+                  log.debug("OAuth flow failed to close the tab. Tab not found in TabBrowser.");
+                }
+              } else {
+                log.debug("OAuth flow failed to close the tab. TabBrowser not found.");
+              }
+            }
+
             if (err) {
               log.debug(err.message);
               if (this.onError) {
                 this.onError(err);
               }
             } else {
               log.debug("OAuth flow completed.");
               if (this.onComplete) {
@@ -209,35 +228,16 @@ this.FxAccountsOAuthClient.prototype = {
                   this.onComplete(result);
                 }
               }
             }
 
             // onComplete will be called for this client only once
             // calling onComplete again will result in a failure of the OAuth flow
             this.tearDown();
-
-            // if the message asked to close the tab
-            if (data.closeWindow && target) {
-              // for e10s reasons the best way is to use the TabBrowser to close the tab.
-              let tabbrowser = target.getTabBrowser();
-
-              if (tabbrowser) {
-                let tab = tabbrowser.getTabForBrowser(target);
-
-                if (tab) {
-                  tabbrowser.removeTab(tab);
-                  log.debug("OAuth flow closed the tab.");
-                } else {
-                  log.debug("OAuth flow failed to close the tab. Tab not found in TabBrowser.");
-                }
-              } else {
-                log.debug("OAuth flow failed to close the tab. TabBrowser not found.");
-              }
-            }
             break;
         }
       }
     };
 
     this._channelCallback = listener.bind(this);
     this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
     this._channel.listen(this._channelCallback);
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -572,16 +572,32 @@ var AutoCompletePopup = {
   selectBy: function(reverse, page) {
     this._index = sendSyncMessage("FormAutoComplete:SelectBy", {
       reverse: reverse,
       page: page
     });
   }
 }
 
+addMessageListener("InPermitUnload", msg => {
+  let inPermitUnload = docShell.contentViewer && docShell.contentViewer.inPermitUnload;
+  sendAsyncMessage("InPermitUnload", {id: msg.data.id, inPermitUnload});
+});
+
+addMessageListener("PermitUnload", msg => {
+  sendAsyncMessage("PermitUnload", {id: msg.data.id, kind: "start"});
+
+  let permitUnload = true;
+  if (docShell && docShell.contentViewer) {
+    permitUnload = docShell.contentViewer.permitUnload();
+  }
+
+  sendAsyncMessage("PermitUnload", {id: msg.data.id, kind: "end", permitUnload});
+});
+
 // We may not get any responses to Browser:Init if the browser element
 // is torn down too quickly.
 var outerWindowID = content.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils)
                            .outerWindowID;
 var initData = sendSyncMessage("Browser:Init", {outerWindowID: outerWindowID});
 if (initData.length) {
   docShell.useGlobalHistory = initData[0].useGlobalHistory;
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -1068,18 +1068,18 @@
             window.addEventListener("mousemove", this, true);
             window.addEventListener("mousedown", this, true);
             window.addEventListener("mouseup", this, true);
             window.addEventListener("contextmenu", this, true);
             window.addEventListener("keydown", this, true);
             window.addEventListener("keypress", this, true);
             window.addEventListener("keyup", this, true);
          ]]>
-       </body>
-     </method>
+        </body>
+      </method>
 
       <method name="handleEvent">
         <parameter name="aEvent"/>
         <body>
         <![CDATA[
           if (this._scrolling) {
             switch(aEvent.type) {
               case "mousemove": {
@@ -1222,16 +1222,40 @@
               this._remoteFinder.swapBrowser(this);
             if (aOtherBrowser._remoteFinder)
               aOtherBrowser._remoteFinder.swapBrowser(aOtherBrowser);
           }
         ]]>
         </body>
       </method>
 
+      <method name="getInPermitUnload">
+        <parameter name="aCallback"/>
+        <body>
+        <![CDATA[
+          if (!this.docShell || !this.docShell.contentViewer) {
+            aCallback(false);
+            return;
+          }
+          aCallback(this.docShell.contentViewer.inPermitUnload);
+        ]]>
+        </body>
+      </method>
+
+      <method name="permitUnload">
+        <body>
+        <![CDATA[
+          if (!this.docShell || !this.docShell.contentViewer) {
+            return true;
+          }
+          return {permitUnload: this.docShell.contentViewer.permitUnload(), timedOut: false};
+        ]]>
+        </body>
+      </method>
+
       <!-- This will go away if the binding has been removed for some reason. -->
       <field name="_alive">true</field>
     </implementation>
 
     <handlers>
       <handler event="keypress" keycode="VK_F7" group="system">
         <![CDATA[
           if (event.defaultPrevented || !event.isTrusted)
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -229,16 +229,99 @@
             frameLoader.tabParent.docShellIsActive = val;
             return val;
           ]]>
         </setter>
       </property>
 
       <field name="mDestroyed">false</field>
 
+      <field name="_permitUnloadId">0</field>
+
+      <method name="getInPermitUnload">
+        <parameter name="aCallback"/>
+        <body>
+        <![CDATA[
+          let id = this._permitUnloadId++;
+          let mm = this.messageManager;
+          mm.sendAsyncMessage("InPermitUnload", {id});
+          mm.addMessageListener("InPermitUnload", function listener(msg) {
+            if (msg.data.id != id) {
+              return;
+            }
+	    aCallback(msg.data.inPermitUnload);
+          });
+        ]]>
+        </body>
+      </method>
+
+      <method name="permitUnload">
+        <body>
+        <![CDATA[
+          const Cc = Components.classes;
+          const Ci = Components.interfaces;
+
+          const kTimeout = 5000;
+
+          let finished = false;
+          let responded = false;
+          let permitUnload;
+          let id = this._permitUnloadId++;
+          let mm = this.messageManager;
+
+          let msgListener = msg => {
+            if (msg.data.id != id) {
+              return;
+            }
+            if (msg.data.kind == "start") {
+              responded = true;
+              return;
+            }
+            done(msg.data.permitUnload);
+          };
+
+          let observer = subject => {
+            if (subject == mm) {
+              done(true);
+            }
+          };
+
+          function done(result) {
+            finished = true;
+            permitUnload = result;
+            mm.removeMessageListener("PermitUnload", msgListener);
+            Services.obs.removeObserver(observer, "message-manager-close");
+          }
+
+          mm.sendAsyncMessage("PermitUnload", {id});
+          mm.addMessageListener("PermitUnload", msgListener);
+          Services.obs.addObserver(observer, "message-manager-close", false);
+
+          let timedOut = false;
+          function timeout() {
+            if (!responded) {
+              timedOut = true;
+            }
+
+            // Dispatch something to ensure that the main thread wakes up.
+            Services.tm.mainThread.dispatch(function() {}, Ci.nsIThread.DISPATCH_NORMAL);
+          }
+
+          let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+          timer.initWithCallback(timeout, kTimeout, timer.TYPE_ONE_SHOT);
+
+          while (!finished && !timedOut) {
+            Services.tm.currentThread.processNextEvent(true);
+          }
+
+          return {permitUnload, timedOut};
+        ]]>
+        </body>
+      </method>
+
       <constructor>
         <![CDATA[
           /*
            * Don't try to send messages from this function. The message manager for
            * the <browser> element may not be initialized yet.
            */
 
           let jsm = "resource://gre/modules/RemoteWebNavigation.jsm";
--- a/toolkit/modules/BrowserUtils.jsm
+++ b/toolkit/modules/BrowserUtils.jsm
@@ -396,10 +396,26 @@ this.BrowserUtils = {
     }
 
     if (url && !url.host) {
       url = null;
     }
 
     return { text: selectionStr, docSelectionIsCollapsed: collapsed,
              linkURL: url ? url.spec : null, linkText: url ? linkText : "" };
-  }
+  },
+
+  // Iterates through every docshell in the window and calls PermitUnload.
+  canCloseWindow(window) {
+    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIWebNavigation);
+    let node = docShell.QueryInterface(Ci.nsIDocShellTreeItem);
+    for (let i = 0; i < node.childCount; ++i) {
+      let docShell = node.getChildAt(i).QueryInterface(Ci.nsIDocShell);
+      let contentViewer = docShell.contentViewer;
+      if (contentViewer && !contentViewer.permitUnload()) {
+        return false;
+      }
+    }
+
+    return true;
+  },
 };