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 90554 199383e4fd2f0371c9915381f033db2cc725cd0a
parent 90553 e530ae7f6b74632fe77b72f7cce1717eff33f22e
child 90555 ee3a013ecc898c79656d262e7b6c719d0a05c6c4
push idunknown
push userunknown
push dateunknown
reviewerssmaug
bugs736688
milestone14.0a1
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