Bug 736688 - Part 6: Add BrowserElementAPI.js, implementing loadstart, loadend, locationchange, and the window.{top,parent,frameElement} override for <iframe mozbrowser>. r=smaug
authorJustin Lebar <justin.lebar@gmail.com>
Wed, 28 Mar 2012 11:36:50 -0700
changeset 93867 199383e4fd2f0371c9915381f033db2cc725cd0a
parent 93866 e530ae7f6b74632fe77b72f7cce1717eff33f22e
child 93868 ee3a013ecc898c79656d262e7b6c719d0a05c6c4
push id886
push userlsblakk@mozilla.com
push dateMon, 04 Jun 2012 19:57:52 +0000
treeherdermozilla-beta@bbd8d5efd6d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs736688
milestone14.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 736688 - Part 6: Add BrowserElementAPI.js, implementing loadstart, loadend, locationchange, and the window.{top,parent,frameElement} override for <iframe mozbrowser>. r=smaug
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
content/html/content/src/nsGenericHTMLFrameElement.cpp
docshell/base/nsDocShell.cpp
docshell/base/nsIDocShell.idl
dom/base/BrowserElementAPI.js
dom/base/BrowserElementAPI.manifest
dom/base/Makefile.in
mobile/android/installer/package-manifest.in
mobile/xul/installer/package-manifest.in
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -292,16 +292,18 @@
 @BINPATH@/components/xul.xpt
 @BINPATH@/components/xuldoc.xpt
 @BINPATH@/components/xultmpl.xpt
 @BINPATH@/components/zipwriter.xpt
 
 ; JavaScript components
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPI.js
+@BINPATH@/components/BrowserElementAPI.manifest
+@BINPATH@/components/BrowserElementAPI.js
 @BINPATH@/components/ContactManager.js
 @BINPATH@/components/ContactManager.manifest
 @BINPATH@/components/FeedProcessor.manifest
 @BINPATH@/components/FeedProcessor.js
 @BINPATH@/components/BrowserFeeds.manifest
 @BINPATH@/components/FeedConverter.js
 @BINPATH@/components/FeedWriter.js
 @BINPATH@/components/fuelApplication.manifest
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -284,16 +284,18 @@
 @BINPATH@/components/xuldoc.xpt
 @BINPATH@/components/xultmpl.xpt
 @BINPATH@/components/zipwriter.xpt
 @BINPATH@/components/telemetry.xpt
 
 ; JavaScript components
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPI.js
+@BINPATH@/components/BrowserElementAPI.manifest
+@BINPATH@/components/BrowserElementAPI.js
 @BINPATH@/components/FeedProcessor.manifest
 @BINPATH@/components/FeedProcessor.js
 @BINPATH@/components/BrowserFeeds.manifest
 @BINPATH@/components/FeedConverter.js
 @BINPATH@/components/FeedWriter.js
 @BINPATH@/components/fuelApplication.manifest
 @BINPATH@/components/fuelApplication.js
 @BINPATH@/components/WebContentConverter.js
--- a/content/html/content/src/nsGenericHTMLFrameElement.cpp
+++ b/content/html/content/src/nsGenericHTMLFrameElement.cpp
@@ -289,10 +289,10 @@ nsGenericHTMLFrameElement::GetReallyIsBr
   principal->GetURI(getter_AddRefs(principalURI));
   if (!nsContentUtils::URIIsChromeOrInPref(principalURI,
                                            "dom.mozBrowserFramesWhitelist")) {
     return NS_OK;
   }
 
   // Otherwise, succeed.
   *aOut = true;
-  return true;
+  return NS_OK;
 }
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -11761,11 +11761,47 @@ nsDocShell::GetIsBrowserFrame(bool *aOut
   NS_ENSURE_ARG_POINTER(aOut);
   *aOut = mIsBrowserFrame;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDocShell::SetIsBrowserFrame(bool aValue)
 {
+  // Disallow transitions from browser frame to not-browser-frame.  Once a
+  // browser frame, always a browser frame.  (Otherwise, observers of
+  // docshell-marked-as-browser-frame would have to distinguish between
+  // newly-created browser frames and frames which went from true to false back
+  // to true.)
+  NS_ENSURE_STATE(!mIsBrowserFrame);
+
+  bool wasBrowserFrame = mIsBrowserFrame;
   mIsBrowserFrame = aValue;
+  if (aValue && !wasBrowserFrame) {
+    nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+    if (os) {
+      os->NotifyObservers(GetAsSupports(this),
+                          "docshell-marked-as-browser-frame", NULL);
+    }
+  }
   return NS_OK;
 }
+
+NS_IMETHODIMP
+nsDocShell::GetContainedInBrowserFrame(bool *aOut)
+{
+    *aOut = false;
+
+    if (mIsBrowserFrame) {
+        *aOut = true;
+        return NS_OK;
+    }
+
+    nsCOMPtr<nsIDocShellTreeItem> parentAsItem;
+    GetSameTypeParent(getter_AddRefs(parentAsItem));
+
+    nsCOMPtr<nsIDocShell> parent = do_QueryInterface(parentAsItem);
+    if (parent) {
+        return parent->GetContainedInBrowserFrame(aOut);
+    }
+
+    return NS_OK;
+}
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -67,17 +67,17 @@ interface nsIRequest;
 interface nsISHEntry;
 interface nsILayoutHistoryState;
 interface nsISecureBrowserUI;
 interface nsIDOMStorage;
 interface nsIPrincipal;
 interface nsIWebBrowserPrint;
 interface nsIVariant;
 
-[scriptable, uuid(DBD39C21-5788-4C68-9D97-0FCEE289BCE1)]
+[scriptable, uuid(c7325422-817e-4321-957a-c0bdd764941d)]
 interface nsIDocShell : nsISupports
 {
   /**
    * Loads a given URI.  This will give priority to loading the requested URI
    * in the object implementing	this interface.  If it can't be loaded here
    * however, the URL dispatcher will go through its normal process of content
    * loading.
    *
@@ -609,9 +609,15 @@ interface nsIDocShell : nsISupports
    * when it initializes the docshell.
    *
    * If so, this docshell should act like a chrome/content boundary for the
    * purposes of window.top and window.parent.
    *
    * See also nsIMozBrowserFrame.
    */
   attribute bool isBrowserFrame;
+
+  /*
+   * Is this docshell contained in an <iframe mozbrowser>, either directly or
+   * indirectly?
+   */
+  readonly attribute bool containedInBrowserFrame;
 };
new file mode 100644
--- /dev/null
+++ b/dom/base/BrowserElementAPI.js
@@ -0,0 +1,283 @@
+/* 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/. */
+
+"use strict";
+
+let Cu = Components.utils;
+let Ci = Components.interfaces;
+let Cc = Components.classes;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+const BROWSER_FRAMES_ENABLED_PREF = "dom.mozBrowserFramesEnabled";
+
+/**
+ * The BrowserElementAPI implements <iframe mozbrowser>.
+ *
+ * We detect windows and docshells contained inside <iframe mozbrowser>s and
+ * alter their behavior so that the page inside the iframe can't tell that it's
+ * framed and the page outside the iframe can observe changes within the iframe
+ * (e.g. loadstart/loadstart, locationchange).
+ */
+
+function BrowserElementAPI() {}
+BrowserElementAPI.prototype = {
+  classID: Components.ID("{5d6fcab3-6c12-4db6-80fb-352df7a41602}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+
+  /**
+   * The keys of this map are the set of chrome event handlers we've observed
+   * which contain a mozbrowser window.
+   *
+   * The values in this map are ignored.
+   */
+  _chromeEventHandlersWatching: new WeakMap(),
+
+  /**
+   * The keys of this map are the set of windows we've observed that are
+   * directly contained in <iframe mozbrowser>s.
+   *
+   * The values in this map are ignored.
+   */
+  _topLevelBrowserWindows: new WeakMap(),
+
+  _browserFramesPrefEnabled: function BA_browserFramesPrefEnabled() {
+    var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+    try {
+      return prefs.getBoolPref(BROWSER_FRAMES_ENABLED_PREF);
+    }
+    catch(e) {
+      return false;
+    }
+  },
+
+  /**
+   * Called on browser start, and also when we observe a change in
+   * the browser-frames-enabled pref.
+   */
+  _init: function BA_init() {
+    if (this._initialized) {
+      return;
+    }
+
+    // If browser frames are disabled, watch the pref so we can enable
+    // ourselves if the pref is flipped.  This is important for tests, if
+    // nothing else.
+    if (!this._browserFramesPrefEnabled()) {
+      var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+      prefs.addObserver(BROWSER_FRAMES_ENABLED_PREF, this, /* ownsWeak = */ true);
+      return;
+    }
+
+    this._initialized = true;
+    this._progressListener._browserElementAPI = this;
+
+    var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+    os.addObserver(this, 'content-document-global-created',  /* ownsWeak = */ true);
+    os.addObserver(this, 'docshell-marked-as-browser-frame', /* ownsWeak = */ true);
+  },
+
+  /**
+   * Called when we observe a docshell-marked-as-browser-frame event, which
+   * happens when a docshell is created inside an <iframe mozbrowser>.
+   *
+   * A docshell may not be un-marked as a browser frame -- this ensures that
+   * this event will never fire twice for the same docshell, which guarantees
+   * that we'll never register duplicate listeners.
+   */
+  _observeDocshellMarkedAsBrowserFrame: function BA_observeDocshellMarkedAsBrowserFrame(docshell) {
+    docshell.QueryInterface(Ci.nsIWebProgress)
+            .addProgressListener(this._progressListener,
+                                 Ci.nsIWebProgress.NOTIFY_LOCATION |
+                                 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
+  },
+
+  /**
+   * Called when a content window is created.  If the window is directly or
+   * indirectly contained in an <iframe mozbrowser>, we'll modify it.
+   */
+  _observeContentGlobalCreated: function BA_observeContentGlobalCreated(win) {
+    var docshell = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIWebNavigation)
+                      .QueryInterface(Ci.nsIDocShell);
+
+    // If this window is not directly or indirectly inside an
+    // <iframe mozbrowser>, BrowserElementAPI does nothing to it.
+    if (!docshell.containedInBrowserFrame) {
+      return;
+    }
+
+    this._initBrowserWindow(win, docshell.isBrowserFrame);
+
+    // If this window is directly contained in an <iframe mozbrowser>, do some
+    // extra work.
+    if (docshell.isBrowserFrame) {
+      this._topLevelBrowserWindows.set(win, true);
+      this._initTopLevelBrowserWindow(win);
+    }
+  },
+
+  /**
+   * Initialize a content window which is indirectly or directly contained by
+   * an <iframe mozbrowser>.
+   *
+   * |isTopLevel| is true iff |win| is directly contained by an
+   * <iframe mozbrowser>.
+   */
+  _initBrowserWindow: function BA_initBrowserWindow(win, isTopLevel) {
+    // XPCNativeWrapper.unwrap gets us the object that content sees; this is
+    // the object object that we must define properties on.  Otherwise, the
+    // properties will be visible only to chrome!
+    var unwrappedWin = XPCNativeWrapper.unwrap(win);
+
+    Object.defineProperty(unwrappedWin, 'top', {
+      get: function() {
+        if (isTopLevel) {
+          return win;
+        }
+        // Call the mozbrowser-aware |top| method we presumably defined on our
+        // parent.
+        return XPCNativeWrapper.unwrap(win.parent).top;
+      }
+    });
+
+    Object.defineProperty(unwrappedWin, 'parent', {
+      get: function() {
+        if (isTopLevel) {
+          return win;
+        }
+        return win.parent;
+      }
+    });
+
+    Object.defineProperty(unwrappedWin, 'frameElement', {
+      get: function() {
+        if (isTopLevel) {
+          return null;
+        }
+        return win.frameElement;
+      }
+    });
+  },
+
+  /**
+   * Initialize a content window directly contained by an <iframe mozbrowser>.
+   */
+  _initTopLevelBrowserWindow: function BA_initTopLevelBrowserWindow(win) {
+    // If we haven't seen this window's chrome event handler before, register
+    // listeners on it.
+    var chromeHandler = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIWebNavigation)
+                           .QueryInterface(Ci.nsIDocShell)
+                           .chromeEventHandler;
+
+    if (chromeHandler && !this._chromeEventHandlersWatching.has(chromeHandler)) {
+      this._chromeEventHandlersWatching.set(chromeHandler, true);
+      this._addChromeEventHandlerListeners(chromeHandler);
+    }
+  },
+
+  /**
+   * Add some listeners to a chrome event handler.  Don't call this twice for
+   * the same chrome event handler or we'll get duplicate listeners!
+   */
+  _addChromeEventHandlerListeners: function BA_addChromeEventHandlerListeners(chromeHandler) {
+    var browserElementAPI = this;
+
+    // Listen for DOMTitleChanged events on top-level <iframe mozbrowser>
+    // windows.  (The chrome event handler handles
+    chromeHandler.addEventListener(
+      'DOMTitleChanged',
+      function(e) {
+        var win = e.target.defaultView;
+        if (browserElementAPI._topLevelBrowserWindows.has(win)) {
+          browserElementAPI._fireCustomEvent('titlechange', e.target.title,
+                                             win, win.frameElement);
+        }
+      },
+      /* useCapture = */ false,
+      /* wantsUntrusted = */ false);
+  },
+
+  /**
+   * Asynchronously fire a vanilla event at the given window's frame element.
+   * (Presumably, the window's frame element is an <iframe mozbrowser>.)
+   *
+   * We'll prepend 'mozbrowser' to the event's name.
+   */
+  _fireEvent: function BA_fireEvent(name, win) {
+    // Because we're chrome, win.frameElement ignores <iframe mozbrowser>
+    // boundaries, as desired.
+    var evt = new win.Event('mozbrowser' + name);
+    win.setTimeout(function() { win.frameElement.dispatchEvent(evt) }, 0);
+  },
+
+  /**
+   * Like _fireEvent, but fire a customevent with the given data, instead of a
+   * vanilla event.
+   */
+  _fireCustomEvent: function BA_fireCustomEvent(name, data, win) {
+    var evt = new win.CustomEvent('mozbrowser' + name, {detail: data});
+    win.setTimeout(function() { win.frameElement.dispatchEvent(evt) }, 0);
+  },
+
+  /**
+   * An nsIWebProgressListener registered on docshells directly contained in an
+   * <iframe mozbrowser>.
+   */
+  _progressListener: {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                           Ci.nsISupportsWeakReference,
+                                           Ci.nsISupports]),
+
+    _getWindow: function(webProgress) {
+      return webProgress.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindow);
+    },
+
+    onLocationChange: function(webProgress, request, location, flags) {
+      this._browserElementAPI._fireCustomEvent('locationchange', location.spec,
+                                               this._getWindow(webProgress));
+    },
+
+    onStateChange: function(webProgress, request, stateFlags, status) {
+      if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+        this._browserElementAPI._fireEvent('loadstart', this._getWindow(webProgress));
+      }
+      if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+        this._browserElementAPI._fireEvent('loadend', this._getWindow(webProgress));
+      }
+    },
+
+    onStatusChange: function(webProgress, request, status, message) {},
+    onProgressChange: function(webProgress, request, curSelfProgress,
+                               maxSelfProgress, curTotalProgress, maxTotalProgress) {},
+    onSecurityChange: function(webProgress, request, aState) {}
+  },
+
+  /**
+   * nsIObserver::Observe
+   */
+  observe: function BA_observe(subject, topic, data) {
+    switch(topic) {
+    case 'app-startup':
+      this._init();
+      break;
+    case 'content-document-global-created':
+      this._observeContentGlobalCreated(subject);
+      break;
+    case 'docshell-marked-as-browser-frame':
+      this._observeDocshellMarkedAsBrowserFrame(subject);
+      break;
+    case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
+      if (data == BROWSER_FRAMES_ENABLED_PREF) {
+        this._init();
+      }
+      break;
+    }
+  },
+};
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([BrowserElementAPI]);
new file mode 100644
--- /dev/null
+++ b/dom/base/BrowserElementAPI.manifest
@@ -0,0 +1,3 @@
+component {5d6fcab3-6c12-4db6-80fb-352df7a41602} BrowserElementAPI.js
+contract @mozilla.org/browser-element-api;1 {5d6fcab3-6c12-4db6-80fb-352df7a41602}
+category app-startup BrowserElementAPI service,@mozilla.org/browser-element-api;1
--- a/dom/base/Makefile.in
+++ b/dom/base/Makefile.in
@@ -49,16 +49,18 @@ FORCE_STATIC_LIB = 1
 
 DIRS = \
   test \
   $(NULL)
 
 EXTRA_PP_COMPONENTS = \
 		ConsoleAPI.js \
 		ConsoleAPI.manifest \
+		BrowserElementAPI.js \
+		BrowserElementAPI.manifest \
 		$(NULL)
 
 EXTRA_JS_MODULES = ConsoleAPIStorage.jsm \
 		$(NULL)
 
 EXTRA_COMPONENTS = \
 	        Webapps.js \
 	        Webapps.manifest \
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -284,16 +284,18 @@
 @BINPATH@/components/xuldoc.xpt
 @BINPATH@/components/xultmpl.xpt
 @BINPATH@/components/zipwriter.xpt
 @BINPATH@/components/openwebapps.xpt
 
 ; JavaScript components
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPI.js
+@BINPATH@/components/BrowserElementAPI.manifest
+@BINPATH@/components/BrowserElementAPI.js
 @BINPATH@/components/FeedProcessor.manifest
 @BINPATH@/components/FeedProcessor.js
 @BINPATH@/components/BrowserFeeds.manifest
 @BINPATH@/components/FeedConverter.js
 @BINPATH@/components/FeedWriter.js
 @BINPATH@/components/fuelApplication.manifest
 @BINPATH@/components/fuelApplication.js
 @BINPATH@/components/WebContentConverter.js
--- a/mobile/xul/installer/package-manifest.in
+++ b/mobile/xul/installer/package-manifest.in
@@ -290,16 +290,18 @@
 @BINPATH@/components/xul.xpt
 @BINPATH@/components/xuldoc.xpt
 @BINPATH@/components/xultmpl.xpt
 @BINPATH@/components/zipwriter.xpt
 
 ; JavaScript components
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPI.js
+@BINPATH@/components/BrowserElementAPI.manifest
+@BINPATH@/components/BrowserElementAPI.js
 @BINPATH@/components/FeedProcessor.manifest
 @BINPATH@/components/FeedProcessor.js
 @BINPATH@/components/BrowserFeeds.manifest
 @BINPATH@/components/FeedConverter.js
 @BINPATH@/components/FeedWriter.js
 @BINPATH@/components/fuelApplication.manifest
 @BINPATH@/components/fuelApplication.js
 @BINPATH@/components/WebContentConverter.js