Bug 1301514 - Destroy browser API frame scripts during swap. r=kanru draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Thu, 08 Sep 2016 16:00:12 -0500
changeset 411813 db74f1450598bcb42566cdf91e1931c97845fa51
parent 411812 14ed24ff7b79e2836627ccb9cf4b414e89dfdf88
child 530831 0175215d4f30b263200d8440ce026a047f17ee7f
push id29000
push userbmo:jryans@gmail.com
push dateThu, 08 Sep 2016 21:09:28 +0000
reviewerskanru
bugs1301514
milestone51.0a1
Bug 1301514 - Destroy browser API frame scripts during swap. r=kanru When swapping content from <iframe mozbrowser> to <xul:browser>, we now stop the frame scripts that implement the content side of the browser API since they are no longer needed and can cause issues if they remain active. MozReview-Commit-ID: JrecxA4MI93
dom/base/nsFrameLoader.cpp
dom/base/nsFrameLoader.h
dom/base/test/chrome/window_swapFrameLoaders.xul
dom/browser-element/BrowserElementChild.js
dom/browser-element/BrowserElementChildPreload.js
dom/browser-element/BrowserElementCopyPaste.js
dom/browser-element/BrowserElementPanning.js
dom/browser-element/BrowserElementPanningAPZDisabled.js
dom/browser-element/BrowserElementParent.js
dom/browser-element/BrowserElementPromptService.jsm
dom/browser-element/nsIBrowserElementAPI.idl
dom/html/nsBrowserElement.cpp
dom/html/nsBrowserElement.h
dom/html/nsGenericHTMLFrameElement.cpp
dom/interfaces/html/nsIMozBrowserFrame.idl
--- a/dom/base/nsFrameLoader.cpp
+++ b/dom/base/nsFrameLoader.cpp
@@ -1027,16 +1027,24 @@ nsFrameLoader::SwapWithOtherRemoteLoader
     aOther->mRemoteBrowser->GetBrowserDOMWindow();
   nsCOMPtr<nsIBrowserDOMWindow> browserDOMWindow =
     mRemoteBrowser->GetBrowserDOMWindow();
 
   if (!!otherBrowserDOMWindow != !!browserDOMWindow) {
     return NS_ERROR_NOT_IMPLEMENTED;
   }
 
+  // Destroy browser frame scripts for content leaving a frame with browser API
+  if (OwnerIsMozBrowserOrAppFrame() && !aOther->OwnerIsMozBrowserOrAppFrame()) {
+    DestroyBrowserFrameScripts();
+  }
+  if (!OwnerIsMozBrowserOrAppFrame() && aOther->OwnerIsMozBrowserOrAppFrame()) {
+    aOther->DestroyBrowserFrameScripts();
+  }
+
   aOther->mRemoteBrowser->SetBrowserDOMWindow(browserDOMWindow);
   mRemoteBrowser->SetBrowserDOMWindow(otherBrowserDOMWindow);
 
   // Native plugin windows used by this remote content need to be reparented.
   if (nsPIDOMWindowOuter* newWin = ourDoc->GetWindow()) {
     RefPtr<nsIWidget> newParent = nsGlobalWindow::Cast(newWin)->GetMainWidget();
     const ManagedContainer<mozilla::plugins::PPluginWidgetParent>& plugins =
       aOther->mRemoteBrowser->ManagedPPluginWidgetParent();
@@ -1401,16 +1409,24 @@ nsFrameLoader::SwapWithOtherLoader(nsFra
   }
 
   // OK.  First begin to swap the docshells in the two nsIFrames
   rv = ourFrameFrame->BeginSwapDocShells(otherFrame);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
+  // Destroy browser frame scripts for content leaving a frame with browser API
+  if (OwnerIsMozBrowserOrAppFrame() && !aOther->OwnerIsMozBrowserOrAppFrame()) {
+    DestroyBrowserFrameScripts();
+  }
+  if (!OwnerIsMozBrowserOrAppFrame() && aOther->OwnerIsMozBrowserOrAppFrame()) {
+    aOther->DestroyBrowserFrameScripts();
+  }
+
   // Now move the docshells to the right docshell trees.  Note that this
   // resets their treeowners to null.
   ourParentItem->RemoveChild(ourDocshell);
   otherParentItem->RemoveChild(otherDocshell);
   if (ourType == nsIDocShellTreeItem::typeContent) {
     ourOwner->ContentShellRemoved(ourDocshell);
     otherOwner->ContentShellRemoved(otherDocshell);
   }
@@ -3357,33 +3373,46 @@ nsFrameLoader::GetLoadContext(nsILoadCon
   }
   loadContext.forget(aLoadContext);
   return NS_OK;
 }
 
 void
 nsFrameLoader::InitializeBrowserAPI()
 {
-  if (OwnerIsMozBrowserOrAppFrame()) {
-    if (!IsRemoteFrame()) {
-      nsresult rv = EnsureMessageManager();
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return;
-      }
-      if (mMessageManager) {
-        mMessageManager->LoadFrameScript(
-          NS_LITERAL_STRING("chrome://global/content/BrowserElementChild.js"),
-          /* allowDelayedLoad = */ true,
-          /* aRunInGlobalScope */ true);
-      }
+  if (!OwnerIsMozBrowserOrAppFrame()) {
+    return;
+  }
+  if (!IsRemoteFrame()) {
+    nsresult rv = EnsureMessageManager();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return;
+    }
+    if (mMessageManager) {
+      mMessageManager->LoadFrameScript(
+        NS_LITERAL_STRING("chrome://global/content/BrowserElementChild.js"),
+        /* allowDelayedLoad = */ true,
+        /* aRunInGlobalScope */ true);
     }
-    nsCOMPtr<nsIMozBrowserFrame> browserFrame = do_QueryInterface(mOwnerContent);
-    if (browserFrame) {
-      browserFrame->InitializeBrowserAPI();
-    }
+  }
+  nsCOMPtr<nsIMozBrowserFrame> browserFrame = do_QueryInterface(mOwnerContent);
+  if (browserFrame) {
+    browserFrame->InitializeBrowserAPI();
+  }
+}
+
+void
+nsFrameLoader::DestroyBrowserFrameScripts()
+{
+  if (!OwnerIsMozBrowserOrAppFrame()) {
+    return;
+  }
+  nsCOMPtr<nsIMozBrowserFrame> browserFrame = do_QueryInterface(mOwnerContent);
+  if (browserFrame) {
+    browserFrame->DestroyBrowserFrameScripts();
   }
 }
 
 NS_IMETHODIMP
 nsFrameLoader::StartPersistence(uint64_t aOuterWindowID,
                                 nsIWebBrowserPersistDocumentReceiver* aRecv)
 {
   if (!aRecv) {
--- a/dom/base/nsFrameLoader.h
+++ b/dom/base/nsFrameLoader.h
@@ -328,16 +328,17 @@ private:
              ? nsGkAtoms::type : nsGkAtoms::mozframetype;
   }
 
   // Update the permission manager's app-id refcount based on mOwnerContent's
   // own-or-containing-app.
   void ResetPermissionManagerStatus();
 
   void InitializeBrowserAPI();
+  void DestroyBrowserFrameScripts();
 
   nsresult GetNewTabContext(mozilla::dom::MutableTabContext* aTabContext,
                             nsIURI* aURI = nullptr,
                             const nsACString& aPackageId = EmptyCString());
 
   enum TabParentChange {
     eTabParentRemoved,
     eTabParentChanged
--- a/dom/base/test/chrome/window_swapFrameLoaders.xul
+++ b/dom/base/test/chrome/window_swapFrameLoaders.xul
@@ -5,17 +5,17 @@
 https://bugzilla.mozilla.org/show_bug.cgi?id=1242644
 Test swapFrameLoaders with different frame types and remoteness
 -->
 <window title="Mozilla Bug 1242644"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
 
   <script type="application/javascript"><![CDATA[
-  ["SimpleTest", "SpecialPowers", "info", "is"].forEach(key => {
+  ["SimpleTest", "SpecialPowers", "info", "is", "ok"].forEach(key => {
     window[key] = window.opener[key];
   })
   const { interfaces: Ci } = Components;
 
   const NS = {
     xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
     html: "http://www.w3.org/1999/xhtml",
   }
@@ -112,16 +112,23 @@ Test swapFrameLoaders with different fra
       info(`Adding frame B, type ${typeB}, remote ${remote}, height ${heightB}`);
       let frameB = yield addFrame(typeB, remote, heightB);
 
       let frameScriptFactory = function(name) {
         return `function() {
           addMessageListener("ping", function() {
             sendAsyncMessage("pong", "${name}");
           });
+          addMessageListener("check-browser-api", function() {
+            let exists = "api" in this;
+            sendAsyncMessage("check-browser-api", {
+              exists,
+              running: exists && !this.api._shuttingDown,
+            });
+          });
         }`;
       }
 
       // Load frame script into each frame
       {
         let mmA = frameA.frameLoader.messageManager;
         let mmB = frameB.frameLoader.messageManager;
 
@@ -208,14 +215,43 @@ Test swapFrameLoaders with different fra
         is(pongA, "B", "Frame A message manager acquired after swap gets reply B after swap");
 
         info("Ping message manager for frame B");
         mmB.sendAsyncMessage("ping");
         let [ { data: pongB } ] = yield inflightB;
         is(pongB, "A", "Frame B message manager acquired after swap gets reply A after swap");
       }
 
+      // Verify browser API frame scripts destroyed if swapped out of browser frame
+      if (frameA.hasAttribute("mozbrowser") != frameB.hasAttribute("mozbrowser")) {
+        let mmA = frameA.frameLoader.messageManager;
+        let mmB = frameB.frameLoader.messageManager;
+
+        let inflightA = once(mmA, "check-browser-api");
+        let inflightB = once(mmB, "check-browser-api");
+
+        info("Check browser API for frame A");
+        mmA.sendAsyncMessage("check-browser-api");
+        let [ { data: apiA } ] = yield inflightA;
+        if (frameA.hasAttribute("mozbrowser")) {
+          ok(apiA.exists && apiA.running, "Frame A browser API exists and is running");
+        } else {
+          ok(apiA.exists && !apiA.running, "Frame A browser API did exist but is now destroyed");
+        }
+
+        info("Check browser API for frame B");
+        mmB.sendAsyncMessage("check-browser-api");
+        let [ { data: apiB } ] = yield inflightB;
+        if (frameB.hasAttribute("mozbrowser")) {
+          ok(apiB.exists && apiB.running, "Frame B browser API exists and is running");
+        } else {
+          ok(apiB.exists && !apiB.running, "Frame B browser API did exist but is now destroyed");
+        }
+      } else {
+        info("Frames have matching mozbrowser state, skipping browser API destruction check");
+      }
+
       frameA.remove();
       frameB.remove();
     }
   });
   ]]></script>
 </window>
--- a/dom/browser-element/BrowserElementChild.js
+++ b/dom/browser-element/BrowserElementChild.js
@@ -72,14 +72,35 @@ if (!BrowserElementIsReady) {
   } else {
     if (Services.prefs.getIntPref("dom.w3c_touch_events.enabled") != 0) {
       if (docShell.asyncPanZoomEnabled === false) {
         ContentPanningAPZDisabled.init();
       }
       ContentPanning.init();
     }
   }
+
+  function onDestroy() {
+    removeMessageListener("browser-element-api:destroy", onDestroy);
+
+    if (api) {
+      api.destroy();
+    }
+    if ("ContentPanning" in this) {
+      ContentPanning.destroy();
+    }
+    if ("ContentPanningAPZDisabled" in this) {
+      ContentPanningAPZDisabled.destroy();
+    }
+    if ("CopyPasteAssistent" in this) {
+      CopyPasteAssistent.destroy();
+    }
+
+    BrowserElementIsReady = false;
+  }
+  addMessageListener("browser-element-api:destroy", onDestroy);
+
   BrowserElementIsReady = true;
 } else {
   debug("BE already loaded, abort");
 }
 
 sendAsyncMessage('browser-element-api:call', { 'msg_name': 'hello' });
--- a/dom/browser-element/BrowserElementChildPreload.js
+++ b/dom/browser-element/BrowserElementChildPreload.js
@@ -25,19 +25,19 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyModuleGetter(this, "ManifestFinder",
                                   "resource://gre/modules/ManifestFinder.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ManifestObtainer",
                                   "resource://gre/modules/ManifestObtainer.jsm");
 
 
 var kLongestReturnedString = 128;
 
-const Timer = Components.Constructor("@mozilla.org/timer;1",
-                                     "nsITimer",
-                                     "initWithCallback");
+var Timer = Components.Constructor("@mozilla.org/timer;1",
+                                   "nsITimer",
+                                   "initWithCallback");
 
 function sendAsyncMsg(msg, data) {
   // Ensure that we don't send any messages before BrowserElementChild.js
   // finishes loading.
   if (!BrowserElementIsReady) {
     return;
   }
 
@@ -61,24 +61,54 @@ function sendSyncMsg(msg, data) {
   }
 
   data.msg_name = msg;
   return sendSyncMessage('browser-element-api:call', data);
 }
 
 var CERTIFICATE_ERROR_PAGE_PREF = 'security.alternate_certificate_error_page';
 
-const OBSERVED_EVENTS = [
+var OBSERVED_EVENTS = [
   'xpcom-shutdown',
   'audio-playback',
   'activity-done',
   'invalid-widget',
   'will-launch-app'
 ];
 
+var LISTENED_EVENTS = [
+  { type: "DOMTitleChanged", useCapture: true, wantsUntrusted: false },
+  { type: "DOMLinkAdded", useCapture: true, wantsUntrusted: false },
+  { type: "MozScrolledAreaChanged", useCapture: true, wantsUntrusted: false },
+  { type: "MozDOMFullscreen:Request", useCapture: true, wantsUntrusted: false },
+  { type: "MozDOMFullscreen:NewOrigin", useCapture: true, wantsUntrusted: false },
+  { type: "MozDOMFullscreen:Exit", useCapture: true, wantsUntrusted: false },
+  { type: "DOMMetaAdded", useCapture: true, wantsUntrusted: false },
+  { type: "DOMMetaChanged", useCapture: true, wantsUntrusted: false },
+  { type: "DOMMetaRemoved", useCapture: true, wantsUntrusted: false },
+  { type: "scrollviewchange", useCapture: true, wantsUntrusted: false },
+  { type: "click", useCapture: false, wantsUntrusted: false },
+  // This listens to unload events from our message manager, but /not/ from
+  // the |content| window.  That's because the window's unload event doesn't
+  // bubble, and we're not using a capturing listener.  If we'd used
+  // useCapture == true, we /would/ hear unload events from the window, which
+  // is not what we want!
+  { type: "unload", useCapture: false, wantsUntrusted: false },
+];
+
+// We are using the system group for those events so if something in the
+// content called .stopPropagation() this will still be called.
+var LISTENED_SYSTEM_EVENTS = [
+  { type: "DOMWindowClose", useCapture: false },
+  { type: "DOMWindowCreated", useCapture: false },
+  { type: "DOMWindowResize", useCapture: false },
+  { type: "contextmenu", useCapture: false },
+  { type: "scroll", useCapture: false },
+];
+
 /**
  * The BrowserElementChild implements one half of <iframe mozbrowser>.
  * (The other half is, unsurprisingly, BrowserElementParent.)
  *
  * This script is injected into an <iframe mozbrowser> via
  * nsIMessageManager::LoadFrameScript().
  *
  * Our job here is to listen for events within this frame and bubble them up to
@@ -172,160 +202,179 @@ BrowserElementChild.prototype = {
     // A cache of the menuitem dom objects keyed by the id we generate
     // and pass to the embedder
     this._ctxHandlers = {};
     // Counter of contextmenu events fired
     this._ctxCounter = 0;
 
     this._shuttingDown = false;
 
-    addEventListener('DOMTitleChanged',
-                     this._titleChangedHandler.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener('DOMLinkAdded',
-                     this._linkAddedHandler.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener('MozScrolledAreaChanged',
-                     this._mozScrollAreaChanged.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener("MozDOMFullscreen:Request",
-                     this._mozRequestedDOMFullscreen.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener("MozDOMFullscreen:NewOrigin",
-                     this._mozFullscreenOriginChange.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener("MozDOMFullscreen:Exit",
-                     this._mozExitDomFullscreen.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener('DOMMetaAdded',
-                     this._metaChangedHandler.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener('DOMMetaChanged',
-                     this._metaChangedHandler.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener('DOMMetaRemoved',
-                     this._metaChangedHandler.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener('scrollviewchange',
-                     this._ScrollViewChangeHandler.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-
-    addEventListener('click',
-                     this._ClickHandler.bind(this),
-                     /* useCapture = */ false,
-                     /* wantsUntrusted = */ false);
-
-    // This listens to unload events from our message manager, but /not/ from
-    // the |content| window.  That's because the window's unload event doesn't
-    // bubble, and we're not using a capturing listener.  If we'd used
-    // useCapture == true, we /would/ hear unload events from the window, which
-    // is not what we want!
-    addEventListener('unload',
-                     this._unloadHandler.bind(this),
-                     /* useCapture = */ false,
-                     /* wantsUntrusted = */ false);
+    LISTENED_EVENTS.forEach(event => {
+      addEventListener(event.type, this, event.useCapture, event.wantsUntrusted);
+    });
 
     // Registers a MozAfterPaint handler for the very first paint.
     this._addMozAfterPaintHandler(function () {
       sendAsyncMsg('firstpaint');
     });
 
+    addMessageListener("browser-element-api:call", this);
+
+    let els = Cc["@mozilla.org/eventlistenerservice;1"]
+                .getService(Ci.nsIEventListenerService);
+    LISTENED_SYSTEM_EVENTS.forEach(event => {
+      els.addSystemEventListener(global, event.type, this, event.useCapture);
+    });
+
+    OBSERVED_EVENTS.forEach((aTopic) => {
+      Services.obs.addObserver(this, aTopic, false);
+    });
+
+    this.forwarder.init();
+  },
+
+  /**
+   * Shut down the frame's side of the browser API.  This is called when:
+   *   - our TabChildGlobal starts to die
+   *   - the content is moved to frame without the browser API
+   * This is not called when the page inside |content| unloads.
+   */
+  destroy: function() {
+    debug("Destroying");
+    this._shuttingDown = true;
+
+    BrowserElementPromptService.unmapWindowToBrowserElementChild(content);
+
+    docShell.QueryInterface(Ci.nsIWebProgress)
+            .removeProgressListener(this._progressListener);
+
+    LISTENED_EVENTS.forEach(event => {
+      removeEventListener(event.type, this, event.useCapture, event.wantsUntrusted);
+    });
+
+    this._deactivateNextPaintListener();
+
+    removeMessageListener("browser-element-api:call", this);
+
+    let els = Cc["@mozilla.org/eventlistenerservice;1"]
+                .getService(Ci.nsIEventListenerService);
+    LISTENED_SYSTEM_EVENTS.forEach(event => {
+      els.removeSystemEventListener(global, event.type, this, event.useCapture);
+    });
+
+    OBSERVED_EVENTS.forEach((aTopic) => {
+      Services.obs.removeObserver(this, aTopic);
+    });
+
+    this.forwarder.uninit();
+    this.forwarder = null;
+  },
+
+  handleEvent: function(event) {
+    switch (event.type) {
+      case "DOMTitleChanged":
+        this._titleChangedHandler(event);
+        break;
+      case "DOMLinkAdded":
+        this._linkAddedHandler(event);
+        break;
+      case "MozScrolledAreaChanged":
+        this._mozScrollAreaChanged(event);
+        break;
+      case "MozDOMFullscreen:Request":
+        this._mozRequestedDOMFullscreen(event);
+        break;
+      case "MozDOMFullscreen:NewOrigin":
+        this._mozFullscreenOriginChange(event);
+        break;
+      case "MozDOMFullscreen:Exit":
+        this._mozExitDomFullscreen(event);
+        break;
+      case "DOMMetaAdded":
+        this._metaChangedHandler(event);
+        break;
+      case "DOMMetaChanged":
+        this._metaChangedHandler(event);
+        break;
+      case "DOMMetaRemoved":
+        this._metaChangedHandler(event);
+        break;
+      case "scrollviewchange":
+        this._ScrollViewChangeHandler(event);
+        break;
+      case "click":
+        this._ClickHandler(event);
+        break;
+      case "unload":
+        this.destroy(event);
+        break;
+      case "DOMWindowClose":
+        this._windowCloseHandler(event);
+        break;
+      case "DOMWindowCreated":
+        this._windowCreatedHandler(event);
+        break;
+      case "DOMWindowResize":
+        this._windowResizeHandler(event);
+        break;
+      case "contextmenu":
+        this._contextmenuHandler(event);
+        break;
+      case "scroll":
+        this._scrollEventHandler(event);
+        break;
+    }
+  },
+
+  receiveMessage: function(message) {
     let self = this;
 
     let mmCalls = {
       "purge-history": this._recvPurgeHistory,
       "get-screenshot": this._recvGetScreenshot,
       "get-contentdimensions": this._recvGetContentDimensions,
       "set-visible": this._recvSetVisible,
       "get-visible": this._recvVisible,
       "send-mouse-event": this._recvSendMouseEvent,
       "send-touch-event": this._recvSendTouchEvent,
       "get-can-go-back": this._recvCanGoBack,
       "get-can-go-forward": this._recvCanGoForward,
-      "mute": this._recvMute.bind(this),
-      "unmute": this._recvUnmute.bind(this),
-      "get-muted": this._recvGetMuted.bind(this),
-      "set-volume": this._recvSetVolume.bind(this),
-      "get-volume": this._recvGetVolume.bind(this),
+      "mute": this._recvMute,
+      "unmute": this._recvUnmute,
+      "get-muted": this._recvGetMuted,
+      "set-volume": this._recvSetVolume,
+      "get-volume": this._recvGetVolume,
       "go-back": this._recvGoBack,
       "go-forward": this._recvGoForward,
       "reload": this._recvReload,
       "stop": this._recvStop,
       "zoom": this._recvZoom,
       "unblock-modal-prompt": this._recvStopWaiting,
       "fire-ctx-callback": this._recvFireCtxCallback,
       "owner-visibility-change": this._recvOwnerVisibilityChange,
       "entered-fullscreen": this._recvEnteredFullscreen,
-      "exit-fullscreen": this._recvExitFullscreen.bind(this),
-      "activate-next-paint-listener": this._activateNextPaintListener.bind(this),
-      "set-input-method-active": this._recvSetInputMethodActive.bind(this),
-      "deactivate-next-paint-listener": this._deactivateNextPaintListener.bind(this),
-      "find-all": this._recvFindAll.bind(this),
-      "find-next": this._recvFindNext.bind(this),
-      "clear-match": this._recvClearMatch.bind(this),
+      "exit-fullscreen": this._recvExitFullscreen,
+      "activate-next-paint-listener": this._activateNextPaintListener,
+      "set-input-method-active": this._recvSetInputMethodActive,
+      "deactivate-next-paint-listener": this._deactivateNextPaintListener,
+      "find-all": this._recvFindAll,
+      "find-next": this._recvFindNext,
+      "clear-match": this._recvClearMatch,
       "execute-script": this._recvExecuteScript,
       "get-audio-channel-volume": this._recvGetAudioChannelVolume,
       "set-audio-channel-volume": this._recvSetAudioChannelVolume,
       "get-audio-channel-muted": this._recvGetAudioChannelMuted,
       "set-audio-channel-muted": this._recvSetAudioChannelMuted,
       "get-is-audio-channel-active": this._recvIsAudioChannelActive,
       "get-web-manifest": this._recvGetWebManifest,
     }
 
-    addMessageListener("browser-element-api:call", function(aMessage) {
-      if (aMessage.data.msg_name in mmCalls) {
-        return mmCalls[aMessage.data.msg_name].apply(self, arguments);
-      }
-    });
-
-    let els = Cc["@mozilla.org/eventlistenerservice;1"]
-                .getService(Ci.nsIEventListenerService);
-
-    // We are using the system group for those events so if something in the
-    // content called .stopPropagation() this will still be called.
-    els.addSystemEventListener(global, 'DOMWindowClose',
-                               this._windowCloseHandler.bind(this),
-                               /* useCapture = */ false);
-    els.addSystemEventListener(global, 'DOMWindowCreated',
-                               this._windowCreatedHandler.bind(this),
-                               /* useCapture = */ true);
-    els.addSystemEventListener(global, 'DOMWindowResize',
-                               this._windowResizeHandler.bind(this),
-                               /* useCapture = */ false);
-    els.addSystemEventListener(global, 'contextmenu',
-                               this._contextmenuHandler.bind(this),
-                               /* useCapture = */ false);
-    els.addSystemEventListener(global, 'scroll',
-                               this._scrollEventHandler.bind(this),
-                               /* useCapture = */ false);
-
-    OBSERVED_EVENTS.forEach((aTopic) => {
-      Services.obs.addObserver(this, aTopic, false);
-    });
-
-    this.forwarder.init();
+    if (message.data.msg_name in mmCalls) {
+      return mmCalls[message.data.msg_name].apply(self, arguments);
+    }
   },
 
   _paintFrozenTimer: null,
   observe: function(subject, topic, data) {
     // Ignore notifications not about our document.  (Note that |content| /can/
     // be null; see bug 874900.)
 
     if (topic !== 'activity-done' &&
@@ -371,30 +420,16 @@ BrowserElementChild.prototype = {
   },
 
   notify: function(timer) {
     docShell.contentViewer.resumePainting();
     this._paintFrozenTimer.cancel();
     this._paintFrozenTimer = null;
   },
 
-  /**
-   * Called when our TabChildGlobal starts to die.  This is not called when the
-   * page inside |content| unloads.
-   */
-  _unloadHandler: function() {
-    this._shuttingDown = true;
-    OBSERVED_EVENTS.forEach((aTopic) => {
-      Services.obs.removeObserver(this, aTopic);
-    });
-
-    this.forwarder.uninit();
-    this.forwarder = null;
-  },
-
   get _windowUtils() {
     return content.document.defaultView
                   .QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindowUtils);
   },
 
   _tryGetInnerWindowID: function(win) {
     let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
--- a/dom/browser-element/BrowserElementCopyPaste.js
+++ b/dom/browser-element/BrowserElementCopyPaste.js
@@ -18,21 +18,41 @@ var CopyPasteAssistent = {
   COMMAND_MAP: {
     'cut': 'cmd_cut',
     'copy': 'cmd_copyAndCollapseToEnd',
     'paste': 'cmd_paste',
     'selectall': 'cmd_selectAll'
   },
 
   init: function() {
-    addEventListener('mozcaretstatechanged',
-                     this._caretStateChangedHandler.bind(this),
-                     /* useCapture = */ true,
-                     /* wantsUntrusted = */ false);
-    addMessageListener('browser-element-api:call', this._browserAPIHandler.bind(this));
+    addEventListener("mozcaretstatechanged", this,
+                     /* useCapture = */ true, /* wantsUntrusted = */ false);
+    addMessageListener("browser-element-api:call", this);
+  },
+
+  destroy: function() {
+    removeEventListener("mozcaretstatechanged", this,
+                        /* useCapture = */ true, /* wantsUntrusted = */ false);
+    removeMessageListener("browser-element-api:call", this);
+  },
+
+  handleEvent: function(event) {
+    switch (event.type) {
+      case "mozcaretstatechanged":
+        this._caretStateChangedHandler(event);
+        break;
+    }
+  },
+
+  receiveMessage: function(message) {
+    switch (message.name) {
+      case "browser-element-api:call":
+        this._browserAPIHandler(message);
+        break;
+    }
   },
 
   _browserAPIHandler: function(e) {
     switch (e.data.msg_name) {
       case 'copypaste-do-command':
         if (this._isCommandEnabled(e.data.command)) {
           docShell.doCommand(this.COMMAND_MAP[e.data.command]);
         }
--- a/dom/browser-element/BrowserElementPanning.js
+++ b/dom/browser-element/BrowserElementPanning.js
@@ -12,37 +12,67 @@ function debug(msg) {
 }
 
 debug("loaded");
 
 var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu }  = Components;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Geometry.jsm");
 
-const kObservedEvents = [
+var kObservedEvents = [
   "BEC:ShownModalPrompt",
   "Activity:Success",
   "Activity:Error"
 ];
 
-const ContentPanning = {
+var ContentPanning = {
   init: function cp_init() {
-    addEventListener("unload",
-		     this._unloadHandler.bind(this),
-		     /* useCapture = */ false,
-		     /* wantsUntrusted = */ false);
-
-    addMessageListener("Viewport:Change", this._recvViewportChange.bind(this));
-    addMessageListener("Gesture:DoubleTap", this._recvDoubleTap.bind(this));
-    addEventListener("visibilitychange", this._handleVisibilityChange.bind(this));
+    addEventListener("unload", this,
+                     /* useCapture = */ false, /* wantsUntrusted = */ false);
+    addMessageListener("Viewport:Change", this);
+    addMessageListener("Gesture:DoubleTap", this);
+    addEventListener("visibilitychange", this);
     kObservedEvents.forEach((topic) => {
       Services.obs.addObserver(this, topic, false);
     });
   },
 
+  destroy: function() {
+    removeEventListener("unload", this,
+                        /* useCapture = */ false, /* wantsUntrusted = */ false);
+    removeMessageListener("Viewport:Change", this);
+    removeMessageListener("Gesture:DoubleTap", this);
+    removeEventListener("visibilitychange", this);
+    kObservedEvents.forEach((topic) => {
+      Services.obs.removeObserver(this, topic, false);
+    });
+  },
+
+  handleEvent: function(event) {
+    switch (event.type) {
+      case "unload":
+        this._unloadHandler(event);
+        break;
+      case "visibilitychange":
+        this._handleVisibilityChange(event);
+        break;
+    }
+  },
+
+  receiveMessage: function(message) {
+    switch (message.name) {
+      case "Viewport:Change":
+        this._recvViewportChange(message);
+        break;
+      case "Gesture:DoubleTap":
+        this._recvDoubleTap(message);
+        break;
+    }
+  },
+
   observe: function cp_observe(subject, topic, data) {
     this._resetHover();
   },
 
   get _domUtils() {
     delete this._domUtils;
     return this._domUtils = Cc['@mozilla.org/inspector/dom-utils;1']
                               .getService(Ci.inIDOMUtils);
@@ -171,17 +201,17 @@ const ContentPanning = {
 
   _unloadHandler: function() {
     kObservedEvents.forEach((topic) => {
       Services.obs.removeObserver(this, topic);
     });
   }
 };
 
-const ElementTouchHelper = {
+var ElementTouchHelper = {
   anyElementFromPoint: function(aWindow, aX, aY) {
     let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
     let elem = cwu.elementFromPoint(aX, aY, true, true);
 
     let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
     let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement;
     while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
       let rect = elem.getBoundingClientRect();
--- a/dom/browser-element/BrowserElementPanningAPZDisabled.js
+++ b/dom/browser-element/BrowserElementPanningAPZDisabled.js
@@ -9,29 +9,45 @@
 dump("############################### browserElementPanningAPZDisabled.js loaded\n");
 
 var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu }  = Components;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Geometry.jsm");
 
 var global = this;
 
-const ContentPanningAPZDisabled = {
+var ContentPanningAPZDisabled = {
   // Are we listening to touch or mouse events?
   watchedEventsType: '',
 
   // Are mouse events being delivered to this content along with touch
   // events, in violation of spec?
   hybridEvents: false,
 
   init: function cp_init() {
-    this._setupListenersForPanning();
+    let events = this._getEventsList();
+    let els = Cc["@mozilla.org/eventlistenerservice;1"]
+                .getService(Ci.nsIEventListenerService);
+    events.forEach(type => {
+      // Using the system group for mouse/touch events to avoid
+      // missing events if .stopPropagation() has been called.
+      els.addSystemEventListener(global, type, this, /* useCapture = */ false);
+    });
   },
 
-  _setupListenersForPanning: function cp_setupListenersForPanning() {
+  destroy: function () {
+    let events = this._getEventsList();
+    let els = Cc["@mozilla.org/eventlistenerservice;1"]
+                .getService(Ci.nsIEventListenerService);
+    events.forEach(type => {
+      els.removeSystemEventListener(global, type, this, /* useCapture = */ false);
+    });
+  },
+
+  _getEventsList: function () {
     let events;
 
     if (content.TouchEvent) {
       events = ['touchstart', 'touchend', 'touchmove'];
       this.watchedEventsType = 'touch';
 #ifdef MOZ_WIDGET_GONK
       // The gonk widget backend does not deliver mouse events per
       // spec.  Third-party content isn't exposed to this behavior,
@@ -43,26 +59,17 @@ const ContentPanningAPZDisabled = {
       this.hybridEvents = isParentProcess;
 #endif
     } else {
       // Touch events aren't supported, so fall back on mouse.
       events = ['mousedown', 'mouseup', 'mousemove'];
       this.watchedEventsType = 'mouse';
     }
 
-    let els = Cc["@mozilla.org/eventlistenerservice;1"]
-                .getService(Ci.nsIEventListenerService);
-
-    events.forEach(function(type) {
-      // Using the system group for mouse/touch events to avoid
-      // missing events if .stopPropagation() has been called.
-      els.addSystemEventListener(global, type,
-                                 this.handleEvent.bind(this),
-                                 /* useCapture = */ false);
-    }.bind(this));
+    return events;
   },
 
   handleEvent: function cp_handleEvent(evt) {
     // Ignore events targeting an oop <iframe mozbrowser> since those will be
     // handle by the BrowserElementPanning.js instance in the child process.
     if (evt.target instanceof Ci.nsIMozBrowserFrame) {
       return;
     }
@@ -473,33 +480,33 @@ const ContentPanningAPZDisabled = {
     // If there is a scroll action, let's do a manual kinetic panning action.
     if (this.panning) {
       KineticPanning.start(this);
     }
   },
 };
 
 // Min/max velocity of kinetic panning. This is in pixels/millisecond.
-const kMinVelocity = 0.2;
-const kMaxVelocity = 6;
+var kMinVelocity = 0.2;
+var kMaxVelocity = 6;
 
 // Constants that affect the "friction" of the scroll pane.
-const kExponentialC = 1000;
-const kPolynomialC = 100 / 1000000;
+var kExponentialC = 1000;
+var kPolynomialC = 100 / 1000000;
 
 // How often do we change the position of the scroll pane?
 // Too often and panning may jerk near the end.
 // Too little and panning will be choppy. In milliseconds.
-const kUpdateInterval = 16;
+var kUpdateInterval = 16;
 
 // The numbers of momentums to use for calculating the velocity of the pan.
 // Those are taken from the end of the action
-const kSamples = 5;
+var kSamples = 5;
 
-const KineticPanning = {
+var KineticPanning = {
   _position: new Point(0, 0),
   _velocity: new Point(0, 0),
   _acceleration: new Point(0, 0),
 
   get active() {
     return this.target !== null;
   },
 
--- a/dom/browser-element/BrowserElementParent.js
+++ b/dom/browser-element/BrowserElementParent.js
@@ -258,16 +258,17 @@ BrowserElementParent.prototype = {
   classDescription: "BrowserElementAPI implementation",
   classID: Components.ID("{9f171ac4-0939-4ef8-b360-3408aedc3060}"),
   contractID: "@mozilla.org/dom/browser-element-api;1",
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserElementAPI,
                                          Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
   setFrameLoader: function(frameLoader) {
+    debug("Setting frameLoader");
     this._frameLoader = frameLoader;
     this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement;
     if (!this._frameElement) {
       debug("No frame element?");
       return;
     }
     // Listen to visibilitychange on the iframe's owner window, and forward
     // changes down to the child.  We want to do this while registering as few
@@ -297,16 +298,21 @@ BrowserElementParent.prototype = {
     BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this);
     this._setupMessageListener();
     this._registerAppManifest();
 
     this.proxyCallHandler.init(
       this._frameElement, this._frameLoader.messageManager);
   },
 
+  destroyFrameScripts() {
+    debug("Destroying frame scripts");
+    this._mm.sendAsyncMessage("browser-element-api:destroy");
+  },
+
   _runPendingAPICall: function() {
     if (!this._pendingAPICalls) {
       return;
     }
     for (let i = 0; i < this._pendingAPICalls.length; i++) {
       try {
         this._pendingAPICalls[i]();
       } catch (e) {
--- a/dom/browser-element/BrowserElementPromptService.jsm
+++ b/dom/browser-element/BrowserElementPromptService.jsm
@@ -49,17 +49,17 @@ BrowserElementPrompt.prototype = {
 
   confirmCheck: function(title, text, checkMsg, checkState) {
     return this.confirm(title, text);
   },
 
   // Each button is described by an object with the following schema
   // {
   //   string messageType,  // 'builtin' or 'custom'
-  //   string message, // 'ok', 'cancel', 'yes', 'no', 'save', 'dontsave', 
+  //   string message, // 'ok', 'cancel', 'yes', 'no', 'save', 'dontsave',
   //                   // 'revert' or a string from caller if messageType was 'custom'.
   // }
   //
   // Expected result from embedder:
   // {
   //   int button, // Index of the button that user pressed.
   //   boolean checked, // True if the check box is checked.
   // }
@@ -626,16 +626,19 @@ this.BrowserElementPromptService = {
               .getInterface(Ci.nsIDOMWindowUtils)
               .outerWindowID;
   },
 
   _browserElementChildMap: {},
   mapWindowToBrowserElementChild: function(win, browserElementChild) {
     this._browserElementChildMap[this._getOuterWindowID(win)] = browserElementChild;
   },
+  unmapWindowToBrowserElementChild: function(win) {
+    delete this._browserElementChildMap[this._getOuterWindowID(win)];
+  },
 
   getBrowserElementChildForWindow: function(win) {
     // We only have a mapping for <iframe mozbrowser>s, not their inner
     // <iframes>, so we look up win.top below.  window.top (when called from
     // script) respects <iframe mozbrowser> boundaries.
     return this._browserElementChildMap[this._getOuterWindowID(win.top)];
   },
 
--- a/dom/browser-element/nsIBrowserElementAPI.idl
+++ b/dom/browser-element/nsIBrowserElementAPI.idl
@@ -30,16 +30,21 @@ interface nsIBrowserElementNextPaintList
 interface nsIBrowserElementAPI : nsISupports
 {
   const long FIND_CASE_SENSITIVE = 0;
   const long FIND_CASE_INSENSITIVE = 1;
 
   const long FIND_FORWARD = 0;
   const long FIND_BACKWARD = 1;
 
+  /**
+   * Notify frame scripts that support the API to destroy.
+   */
+  void destroyFrameScripts();
+
   void setFrameLoader(in nsIFrameLoader frameLoader);
 
   void setVisible(in boolean visible);
   nsIDOMDOMRequest getVisible();
   void setActive(in boolean active);
   boolean getActive();
 
   void sendMouseEvent(in DOMString type,
--- a/dom/html/nsBrowserElement.cpp
+++ b/dom/html/nsBrowserElement.cpp
@@ -70,16 +70,25 @@ nsBrowserElement::InitBrowserElementAPI(
     if (NS_WARN_IF(!mBrowserElementAPI)) {
       return;
     }
   }
   mBrowserElementAPI->SetFrameLoader(frameLoader);
 }
 
 void
+nsBrowserElement::DestroyBrowserElementFrameScripts()
+{
+  if (!mBrowserElementAPI) {
+    return;
+  }
+  mBrowserElementAPI->DestroyFrameScripts();
+}
+
+void
 nsBrowserElement::SetVisible(bool aVisible, ErrorResult& aRv)
 {
   NS_ENSURE_TRUE_VOID(IsBrowserElementOrThrow(aRv));
 
   nsresult rv = mBrowserElementAPI->SetVisible(aVisible);
 
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
--- a/dom/html/nsBrowserElement.h
+++ b/dom/html/nsBrowserElement.h
@@ -125,16 +125,17 @@ public:
                  nsTArray<RefPtr<dom::BrowserElementAudioChannel>>& aAudioChannels,
                  ErrorResult& aRv);
 
 protected:
   NS_IMETHOD_(already_AddRefed<nsFrameLoader>) GetFrameLoader() = 0;
   NS_IMETHOD GetParentApplication(mozIApplication** aApplication) = 0;
 
   void InitBrowserElementAPI();
+  void DestroyBrowserElementFrameScripts();
   nsCOMPtr<nsIBrowserElementAPI> mBrowserElementAPI;
   nsTArray<RefPtr<dom::BrowserElementAudioChannel>> mBrowserElementAudioChannels;
 
 private:
   bool IsBrowserElementOrThrow(ErrorResult& aRv);
   bool IsNotWidgetOrThrow(ErrorResult& aRv);
   bool mOwnerIsWidget;
 };
--- a/dom/html/nsGenericHTMLFrameElement.cpp
+++ b/dom/html/nsGenericHTMLFrameElement.cpp
@@ -721,8 +721,16 @@ nsGenericHTMLFrameElement::AllowCreateFr
 
 NS_IMETHODIMP
 nsGenericHTMLFrameElement::InitializeBrowserAPI()
 {
   MOZ_ASSERT(mFrameLoader);
   InitBrowserElementAPI();
   return NS_OK;
 }
+
+NS_IMETHODIMP
+nsGenericHTMLFrameElement::DestroyBrowserFrameScripts()
+{
+  MOZ_ASSERT(mFrameLoader);
+  DestroyBrowserElementFrameScripts();
+  return NS_OK;
+}
--- a/dom/interfaces/html/nsIMozBrowserFrame.idl
+++ b/dom/interfaces/html/nsIMozBrowserFrame.idl
@@ -86,13 +86,18 @@ interface nsIMozBrowserFrame : nsIDOMMoz
    * Create a remote (i.e., out-of-process) frame loader attached to the given
    * tab parent.
    *
    * It is an error to call this method if we already have a frame loader.
    */
   void createRemoteFrameLoader(in nsITabParent aTabParent);
 
   /**
-   * Initialize the API, and add frame message listener to listen to API
+   * Initialize the API, and add frame message listener that supports API
    * invocations.
    */
   [noscript] void initializeBrowserAPI();
+
+  /**
+   * Notify frame scripts that support the API to destroy.
+   */
+  [noscript] void destroyBrowserFrameScripts();
 };