Bug 1246088 - Safely access contentWindow in iframe and embed; r=bgrins draft
authorPatrick Brosset <pbrosset@mozilla.com>
Wed, 09 Mar 2016 11:54:12 +0100
changeset 338572 e2a69eead5a64287fcf5575ca6f09464edf121ca
parent 338571 43cc5a5867878f8d2baf029cdc6d57e593b14f00
child 338573 7c2445645a123c00c79e3ca8829632d71dee0079
push id12531
push userpbrosset@mozilla.com
push dateWed, 09 Mar 2016 11:11:09 +0000
reviewersbgrins
bugs1246088
milestone48.0a1
Bug 1246088 - Safely access contentWindow in iframe and embed; r=bgrins MozReview-Commit-ID: 6GRsMsJPn5x
devtools/client/inspector/test/browser.ini
devtools/client/inspector/test/browser_inspector_highlighter-embed.js
devtools/client/inspector/test/doc_inspector_embed.html
devtools/shared/layout/utils.js
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   doc_inspector_breadcrumbs.html
   doc_inspector_delete-selected-node-01.html
   doc_inspector_delete-selected-node-02.html
+  doc_inspector_embed.html
   doc_inspector_gcli-inspect-command.html
   doc_inspector_highlight_after_transition.html
   doc_inspector_highlighter-comments.html
   doc_inspector_highlighter-geometry_01.html
   doc_inspector_highlighter-geometry_02.html
   doc_inspector_highlighter_csstransform.html
   doc_inspector_highlighter_dom.html
   doc_inspector_highlighter_inline.html
@@ -45,16 +46,17 @@ support-files =
 [browser_inspector_highlighter-01.js]
 [browser_inspector_highlighter-02.js]
 [browser_inspector_highlighter-03.js]
 [browser_inspector_highlighter-04.js]
 [browser_inspector_highlighter-by-type.js]
 [browser_inspector_highlighter-comments.js]
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
+[browser_inspector_highlighter-embed.js]
 [browser_inspector_highlighter-geometry_01.js]
 [browser_inspector_highlighter-geometry_02.js]
 [browser_inspector_highlighter-geometry_03.js]
 [browser_inspector_highlighter-geometry_04.js]
 [browser_inspector_highlighter-geometry_05.js]
 [browser_inspector_highlighter-hover_01.js]
 [browser_inspector_highlighter-hover_02.js]
 [browser_inspector_highlighter-hover_03.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-embed.js
@@ -0,0 +1,30 @@
+/* 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";
+
+// Test that the highlighter can go inside <embed> elements
+
+const TEST_URL = URL_ROOT + "doc_inspector_embed.html";
+
+add_task(function*() {
+  let {inspector} = yield openInspectorForURL(TEST_URL);
+
+  info("Get a node inside the <embed> element and select/highlight it");
+  let body = yield getEmbeddedBody(inspector);
+  yield selectAndHighlightNode(body, inspector);
+
+  let selectedNode = inspector.selection.nodeFront;
+  is(selectedNode.tagName.toLowerCase(), "body", "The selected node is <body>");
+  ok(selectedNode.baseURI.endsWith("doc_inspector_menu.html"),
+     "The selected node is the <body> node inside the <embed> element");
+});
+
+function* getEmbeddedBody({walker}) {
+  let embed = yield walker.querySelector(walker.rootNode, "embed");
+  let {nodes} = yield walker.children(embed);
+  let contentDoc = nodes[0];
+  let body = yield walker.querySelector(contentDoc, "body");
+  return body;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_embed.html
@@ -0,0 +1,6 @@
+<!doctype html><html><head><meta charset="UTF-8"></head><body>
+<object>
+  <embed src="doc_inspector_menu.html" type="application/html"
+         width="422" height="258"></embed>
+</object>
+</body></html>
--- a/devtools/shared/layout/utils.js
+++ b/devtools/shared/layout/utils.js
@@ -1,15 +1,15 @@
 /* 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";
 
-const { Ci } = require("chrome");
+const { Ci, Cc } = require("chrome");
 const { memoize } = require("sdk/lang/functional");
 
 loader.lazyRequireGetter(this, "setIgnoreLayoutChanges",
   "devtools/server/actors/layout", true);
 exports.setIgnoreLayoutChanges = (...args) =>
   this.setIgnoreLayoutChanges(...args);
 
 /**
@@ -150,18 +150,17 @@ function getFrameOffsets(boundaryWindow,
       break;
     }
 
     // We are in an iframe.
     // We take into account the parent iframe position and its
     // offset (borders and padding).
     let frameRect = frameElement.getBoundingClientRect();
 
-    let [offsetTop, offsetLeft] =
-      getIframeContentOffset(frameElement);
+    let [offsetTop, offsetLeft] = getFrameContentOffset(frameElement);
 
     xOffset += frameRect.left + offsetLeft;
     yOffset += frameRect.top + offsetTop;
 
     frameWin = getParentWindow(frameWin);
   }
 
   return [xOffset * scale, yOffset * scale];
@@ -283,18 +282,17 @@ function getRect(boundaryWindow, aNode, 
       break;
     }
 
     // We are in an iframe.
     // We take into account the parent iframe position and its
     // offset (borders and padding).
     let frameRect = frameElement.getBoundingClientRect();
 
-    let [offsetTop, offsetLeft] =
-      getIframeContentOffset(frameElement);
+    let [offsetTop, offsetLeft] = getFrameContentOffset(frameElement);
 
     rect.top += frameRect.top + offsetTop;
     rect.left += frameRect.left + offsetLeft;
 
     frameWin = getParentWindow(frameWin);
   }
 
   return rect;
@@ -358,46 +356,73 @@ function getNodeBounds(boundaryWindow, n
     p2: {x: xOffset + width, y: yOffset},
     p3: {x: xOffset + width, y: yOffset + height},
     p4: {x: xOffset, y: yOffset + height}
   };
 }
 exports.getNodeBounds = getNodeBounds;
 
 /**
- * Returns iframe content offset (iframe border + padding).
+ * Same as doing iframe.contentWindow but works with all types of container
+ * elements that act like frames (e.g. <embed>), where 'contentWindow' isn't a
+ * property that can be accessed.
+ * This uses the inIDeepTreeWalker instead.
+ * @param {DOMNode} aFrame
+ * @return {Window}
+ */
+function safelyGetContentWindow(aFrame) {
+  if (aFrame.contentWindow) {
+    return aFrame.contentWindow;
+  }
+
+  let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]
+               .createInstance(Ci.inIDeepTreeWalker);
+  walker.showSubDocuments = true;
+  walker.showDocumentsAsNodes = true;
+  walker.init(aFrame, Ci.nsIDOMNodeFilter.SHOW_ALL);
+  walker.currentNode = aFrame;
+
+  let document = walker.nextNode();
+  if (!document || !document.defaultView) {
+    throw new Error("Couldn't get the content window inside aFrame " + aFrame);
+  }
+
+  return document.defaultView;
+}
+
+/**
+ * Returns a frame's content offset (frame border + padding).
  * Note: this function shouldn't need to exist, had the platform provided a
- * suitable API for determining the offset between the iframe's content and
+ * suitable API for determining the offset between the frame's content and
  * its bounding client rect. Bug 626359 should provide us with such an API.
  *
- * @param {DOMNode} aIframe
- *        The iframe.
+ * @param {DOMNode} aFrame
+ *        The frame.
  * @return {Array} [offsetTop, offsetLeft]
- *         offsetTop is the distance from the top of the iframe and the top of
+ *         offsetTop is the distance from the top of the frame and the top of
  *         the content document.
- *         offsetLeft is the distance from the left of the iframe and the left
+ *         offsetLeft is the distance from the left of the frame and the left
  *         of the content document.
  */
-function getIframeContentOffset(aIframe) {
-  let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
+function getFrameContentOffset(aFrame) {
+  let style = safelyGetContentWindow(aFrame).getComputedStyle(aFrame, null);
 
   // In some cases, the computed style is null
   if (!style) {
     return [0, 0];
   }
 
   let paddingTop = parseInt(style.getPropertyValue("padding-top"));
   let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
 
   let borderTop = parseInt(style.getPropertyValue("border-top-width"));
   let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
 
   return [borderTop + paddingTop, borderLeft + paddingLeft];
 }
-exports.getIframeContentOffset = getIframeContentOffset;
 
 /**
  * Find an element from the given coordinates. This method descends through
  * frames to find the element the user clicked inside frames.
  *
  * @param {DOMDocument} aDocument
  *        The document to look into.
  * @param {Number} aX
@@ -407,24 +432,24 @@ exports.getIframeContentOffset = getIfra
  *         was found
  */
 function getElementFromPoint(aDocument, aX, aY) {
   let node = aDocument.elementFromPoint(aX, aY);
   if (node && node.contentDocument) {
     if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
       let rect = node.getBoundingClientRect();
 
-      // Gap between the iframe and its content window.
-      let [offsetTop, offsetLeft] = getIframeContentOffset(node);
+      // Gap between the frame and its content window.
+      let [offsetTop, offsetLeft] = getFrameContentOffset(node);
 
       aX -= rect.left + offsetLeft;
       aY -= rect.top + offsetTop;
 
       if (aX < 0 || aY < 0) {
-        // Didn't reach the content document, still over the iframe.
+        // Didn't reach the content document, still over the frame.
         return node;
       }
     }
     if (node instanceof Ci.nsIDOMHTMLIFrameElement ||
         node instanceof Ci.nsIDOMHTMLFrameElement) {
       let subnode = getElementFromPoint(node.contentDocument, aX, aY);
       if (subnode) {
         node = subnode;