Bug 1356415 - get CSS selector in content for contextmenu to fix devtools CPOW;r?mixedpuppy draft
authorJulian Descottes <jdescottes@mozilla.com>
Wed, 26 Apr 2017 19:41:19 +0200
changeset 573060 dd7e914a5fe453a0596ba9bf81fe29c7b617f103
parent 573056 38f2765e94df6b7750b442586bb0fa97c64d9b49
child 573061 1a2e0a57129ad428f89a7f32a76eef2d61246528
push id57295
push userjdescottes@mozilla.com
push dateFri, 05 May 2017 09:23:05 +0000
reviewersmixedpuppy
bugs1356415
milestone55.0a1
Bug 1356415 - get CSS selector in content for contextmenu to fix devtools CPOW;r?mixedpuppy MozReview-Commit-ID: 3TYaYcS1W0h
browser/base/content/content.js
browser/base/content/nsContextMenu.js
devtools/client/framework/devtools-browser.js
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -46,16 +46,21 @@ XPCOMUtils.defineLazyGetter(this, "PageM
   let tmp = {};
   Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
   return new tmp.PageMenuChild();
 });
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
   "resource://gre/modules/WebNavigationFrames.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
   "resource:///modules/Feeds.jsm");
+XPCOMUtils.defineLazyGetter(this, "findCssSelector", () => {
+  let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+  let { findCssSelector } = require("devtools/shared/inspector/css-logic");
+  return findCssSelector;
+});
 
 Cu.importGlobalProperties(["URL"]);
 
 // TabChildGlobal
 var global = this;
 
 // Load the form validation popup handler
 var formSubmitObserver = new FormSubmitObserver(content, this);
@@ -159,16 +164,17 @@ var handleContentContextMenu = function(
       } catch (e) {}
     } catch (e) {}
   }
 
   let selectionInfo = BrowserUtils.getSelectionDetails(content);
 
   let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
   let userContextId = loadContext.originAttributes.userContextId;
+  let popupNodeSelectors = getNodeSelectors(event.target);
 
   if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
     let editFlags = SpellCheckHelper.isEditable(event.target, content);
     let spellInfo;
     if (editFlags &
         (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
       spellInfo =
         InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
@@ -178,31 +184,37 @@ var handleContentContextMenu = function(
     // determine what was context-clicked on. Then, update the state of the
     // commands on the context menu.
     docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
             .setCommandNode(event.target);
     event.target.ownerGlobal.updateCommands("contentcontextmenu");
 
     let customMenuItems = PageMenuChild.build(event.target);
     let principal = doc.nodePrincipal;
+
     sendRpcMessage("contextmenu",
                    { editFlags, spellInfo, customMenuItems, addonInfo,
                      principal, docLocation, charSet, baseURI, referrer,
                      referrerPolicy, contentType, contentDisposition,
                      frameOuterWindowID, selectionInfo, disableSetDesktopBg,
-                     loginFillInfo, parentAllowsMixedContent, userContextId },
-                   { event, popupNode: event.target });
+                     loginFillInfo, parentAllowsMixedContent, userContextId,
+                     popupNodeSelectors,
+                   }, {
+                     event,
+                     popupNode: event.target,
+                   });
   } else {
     // Break out to the parent window and pass the add-on info along
     let browser = docShell.chromeEventHandler;
     let mainWin = browser.ownerGlobal;
     mainWin.gContextMenuContentData = {
       isRemote: false,
       event,
       popupNode: event.target,
+      popupNodeSelectors,
       browser,
       addonInfo,
       documentURIObject: doc.documentURIObject,
       docLocation,
       charSet,
       referrer,
       referrerPolicy,
       contentType,
@@ -240,16 +252,38 @@ const MOZILLA_PKIX_ERROR_NOT_YET_VALID_I
 const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
 
 const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
 
 const PREF_SSL_IMPACT = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => {
   return prefs.concat(Services.prefs.getChildList(root));
 }, []);
 
+/**
+ * Retrieve the array of CSS selectors corresponding to the provided node. The first item
+ * of the array is the selector of the node in its owner document. Additional items are
+ * used if the node is inside a frame, each representing the CSS selector for finding the
+ * frame element in its parent document.
+ *
+ * This format is expected by DevTools in order to handle the Inspect Node context menu
+ * item.
+ *
+ * @param  {Node}
+ *         The node for which the CSS selectors should be computed
+ * @return {Array} array of css selectors (strings).
+ */
+function getNodeSelectors(node) {
+  let selectors = [];
+  while (node) {
+    selectors.push(findCssSelector(node));
+    node = node.ownerGlobal.frameElement;
+  }
+
+  return selectors;
+}
 
 function getSerializedSecurityInfo(docShell) {
   let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
                     .getService(Ci.nsISerializationHelper);
 
   let securityInfo = docShell.failedChannel && docShell.failedChannel.securityInfo;
   if (!securityInfo) {
     return "";
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -28,16 +28,17 @@ function openContextMenu(aMessage) {
   if (spellInfo)
     spellInfo.target = aMessage.target.messageManager;
   let documentURIObject = makeURI(data.docLocation,
                                   data.charSet,
                                   makeURI(data.baseURI));
   gContextMenuContentData = { isRemote: true,
                               event: aMessage.objects.event,
                               popupNode: aMessage.objects.popupNode,
+                              popupNodeSelectors: data.popupNodeSelectors,
                               browser,
                               editFlags: data.editFlags,
                               spellInfo,
                               principal: data.principal,
                               customMenuItems: data.customMenuItems,
                               addonInfo: data.addonInfo,
                               documentURIObject,
                               docLocation: data.docLocation,
@@ -607,17 +608,17 @@ nsContextMenu.prototype = {
   openPasswordManager() {
     LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
   },
 
   inspectNode() {
     let gBrowser = this.browser.ownerGlobal.gBrowser;
     let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
     let { gDevToolsBrowser } = require("devtools/client/framework/devtools-browser");
-    return gDevToolsBrowser.inspectNode(gBrowser.selectedTab, this.target);
+    return gDevToolsBrowser.inspectNode(gBrowser.selectedTab, this.targetSelectors);
   },
 
   /**
    * Set various context menu attributes based on the state of the world.
    * Note: If the context menu is on a remote process the supplied parameters
    * will be overwritten with data from gContextMenuContentData.
    *
    * @param {Object} aNode The node that this menu is being opened on.
@@ -685,16 +686,22 @@ nsContextMenu.prototype = {
     }
 
     this.textSelected      = this.selectionInfo.text;
     this.isTextSelected    = this.textSelected.length != 0;
 
     // Remember the node that was clicked.
     this.target = aNode;
 
+    // Remember the CSS selectors corresponding to clicked node. gContextMenuContentData
+    // can be null if the menu was triggered by tests in which case use an empty array.
+    this.targetSelectors = gContextMenuContentData
+                              ? gContextMenuContentData.popupNodeSelectors
+                              : [];
+
     let ownerDoc = this.target.ownerDocument;
     this.ownerDoc = ownerDoc;
 
     let editFlags;
 
     // If this is a remote context menu event, use the information from
     // gContextMenuContentData instead.
     if (this.isRemote) {
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -19,17 +19,16 @@ const Telemetry = require("devtools/clie
 const {gDevTools} = require("./devtools");
 
 // Load target and toolbox lazily as they need gDevTools to be fully initialized
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
 loader.lazyRequireGetter(this, "BrowserMenus", "devtools/client/framework/browser-menus");
-loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "appendStyleSheet", "devtools/client/shared/stylesheet-utils", true);
 
 loader.lazyImporter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm");
 loader.lazyImporter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
 loader.lazyImporter(this, "LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm");
 
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
@@ -302,41 +301,34 @@ var gDevToolsBrowser = exports.gDevTools
     let win = Services.wm.getMostRecentWindow("devtools:webide");
     if (win) {
       win.focus();
     } else {
       Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
     }
   },
 
-  async inspectNode(tab, node) {
+  async inspectNode(tab, nodeSelectors) {
     let target = TargetFactory.forTab(tab);
 
-    // Generate a cross iframes query selector
-    let selectors = [];
-    while (node) {
-      selectors.push(findCssSelector(node));
-      node = node.ownerDocument.defaultView.frameElement;
-    }
-
     let toolbox = await gDevTools.showToolbox(target, "inspector");
     let inspector = toolbox.getCurrentPanel();
 
     // new-node-front tells us when the node has been selected, whether the
     // browser is remote or not.
     let onNewNode = inspector.selection.once("new-node-front");
 
     // Evaluate the cross iframes query selectors
     async function querySelectors(nodeFront) {
-      let selector = selectors.pop();
+      let selector = nodeSelectors.pop();
       if (!selector) {
         return nodeFront;
       }
       nodeFront = await inspector.walker.querySelector(nodeFront, selector);
-      if (selectors.length > 0) {
+      if (nodeSelectors.length > 0) {
         let { nodes } = await inspector.walker.children(nodeFront);
         // This is the NodeFront for the document node inside the iframe
         nodeFront = nodes[0];
       }
       return querySelectors(nodeFront);
     }
     let nodeFront = await inspector.walker.getRootNode();
     nodeFront = await querySelectors(nodeFront);