Bug 1190687 - [webext] webNavigation.onCreatedNavigationTarget on new windows/tabs from window.open. r=billm
authorLuca Greco <lgreco@mozilla.com>
Fri, 24 Feb 2017 19:49:49 +0100
changeset 345573 1733ced10f06
parent 345572 3884829f39b0
child 345574 e87b41eb5ac0
push id31440
push userkwierso@gmail.com
push date2017-03-02 21:09 +0000
treeherdermozilla-central@c54c652af04c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1190687
milestone54.0a1
Bug 1190687 - [webext] webNavigation.onCreatedNavigationTarget on new windows/tabs from window.open. r=billm

MozReview-Commit-ID: KFtRP1eSI05
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js
toolkit/components/windowwatcher/nsWindowWatcher.cpp
toolkit/modules/addons/WebNavigation.jsm
toolkit/modules/addons/WebNavigationContent.js
toolkit/modules/addons/WebNavigationFrames.jsm
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -119,16 +119,17 @@ support-files =
 [browser_ext_topwindowid.js]
 [browser_ext_url_overrides_all.js]
 [browser_ext_url_overrides_home.js]
 [browser_ext_url_overrides_newtab.js]
 [browser_ext_webRequest.js]
 [browser_ext_webNavigation_frameId0.js]
 [browser_ext_webNavigation_getFrames.js]
 [browser_ext_webNavigation_onCreatedNavigationTarget.js]
+[browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js]
 [browser_ext_webNavigation_urlbar_transitions.js]
 [browser_ext_windows.js]
 [browser_ext_windows_create.js]
 tags = fullscreen
 [browser_ext_windows_create_params.js]
 [browser_ext_windows_create_tabId.js]
 [browser_ext_windows_create_url.js]
 [browser_ext_windows_events.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js
@@ -0,0 +1,169 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const BASE_URL = "http://mochi.test:8888/browser/browser/components/extensions/test/browser";
+const SOURCE_PAGE = `${BASE_URL}/webNav_createdTargetSource.html`;
+const OPENED_PAGE = `${BASE_URL}/webNav_createdTarget.html`;
+
+async function background() {
+  const tabs = await browser.tabs.query({active: true, currentWindow: true});
+  const sourceTabId = tabs[0].id;
+
+  const sourceTabFrames = await browser.webNavigation.getAllFrames({tabId: sourceTabId});
+
+  browser.webNavigation.onCreatedNavigationTarget.addListener((msg) => {
+    browser.test.sendMessage("webNavOnCreated", msg);
+  });
+
+  browser.webNavigation.onCompleted.addListener(async (msg) => {
+    // NOTE: checking the url is currently necessary because of Bug 1252129
+    // ( Filter out webNavigation events related to new window initialization phase).
+    if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+      await browser.tabs.remove(msg.tabId);
+      browser.test.sendMessage("webNavOnCompleted", msg);
+    }
+  });
+
+  browser.tabs.onCreated.addListener((tab) => {
+    browser.test.sendMessage("tabsOnCreated", tab.id);
+  });
+
+  browser.test.onMessage.addListener(({type, code}) => {
+    if (type === "execute-contentscript") {
+      browser.tabs.executeScript(sourceTabId, {code: code});
+    }
+  });
+
+  browser.test.sendMessage("expectedSourceTab", {
+    sourceTabId, sourceTabFrames,
+  });
+}
+
+async function runTestCase({extension, openNavTarget, expectedWebNavProps}) {
+  await openNavTarget();
+
+  const webNavMsg = await extension.awaitMessage("webNavOnCreated");
+  const createdTabId = await extension.awaitMessage("tabsOnCreated");
+  const completedNavMsg = await extension.awaitMessage("webNavOnCompleted");
+
+  let {sourceTabId, sourceFrameId, url} = expectedWebNavProps;
+
+  is(webNavMsg.tabId, createdTabId, "Got the expected tabId property");
+  is(webNavMsg.sourceTabId, sourceTabId, "Got the expected sourceTabId property");
+  is(webNavMsg.sourceFrameId, sourceFrameId, "Got the expected sourceFrameId property");
+  is(webNavMsg.url, url, "Got the expected url property");
+
+  is(completedNavMsg.tabId, createdTabId, "Got the expected webNavigation.onCompleted tabId property");
+  is(completedNavMsg.url, url, "Got the expected webNavigation.onCompleted url property");
+}
+
+add_task(function* test_on_created_navigation_target_from_window_open() {
+  const tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, SOURCE_PAGE);
+
+  gBrowser.selectedTab = tab1;
+
+  const extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["webNavigation", "tabs", "<all_urls>"],
+    },
+  });
+
+  yield extension.startup();
+
+  const expectedSourceTab = yield extension.awaitMessage("expectedSourceTab");
+
+  info("open an url in a new tab from a window.open call");
+
+  yield runTestCase({
+    extension,
+    openNavTarget() {
+      extension.sendMessage({
+        type: "execute-contentscript",
+        code: `window.open("${OPENED_PAGE}#new-tab-from-window-open"); true;`,
+      });
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: 0,
+      url: `${OPENED_PAGE}#new-tab-from-window-open`,
+    },
+  });
+
+  info("open an url in a new window from a window.open call");
+
+  yield runTestCase({
+    extension,
+    openNavTarget() {
+      extension.sendMessage({
+        type: "execute-contentscript",
+        code: `window.open("${OPENED_PAGE}#new-win-from-window-open", "_blank", "toolbar=0"); true;`,
+      });
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: 0,
+      url: `${OPENED_PAGE}#new-win-from-window-open`,
+    },
+  });
+
+  yield BrowserTestUtils.removeTab(tab1);
+
+  yield extension.unload();
+});
+
+add_task(function* test_on_created_navigation_target_from_window_open_subframe() {
+  const tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, SOURCE_PAGE);
+
+  gBrowser.selectedTab = tab1;
+
+  const extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["webNavigation", "tabs", "<all_urls>"],
+    },
+  });
+
+  yield extension.startup();
+
+  const expectedSourceTab = yield extension.awaitMessage("expectedSourceTab");
+
+  info("open an url in a new tab from subframe window.open call");
+
+  yield runTestCase({
+    extension,
+    openNavTarget() {
+      extension.sendMessage({
+        type: "execute-contentscript",
+        code: `document.querySelector('iframe').contentWindow.open("${OPENED_PAGE}#new-tab-from-window-open-subframe"); true;`,
+      });
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+      url: `${OPENED_PAGE}#new-tab-from-window-open-subframe`,
+    },
+  });
+
+  info("open an url in a new window from subframe window.open call");
+
+  yield runTestCase({
+    extension,
+    openNavTarget() {
+      extension.sendMessage({
+        type: "execute-contentscript",
+        code: `document.querySelector('iframe').contentWindow.open("${OPENED_PAGE}#new-win-from-window-open-subframe", "_blank", "toolbar=0"); true;`,
+      });
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+      url: `${OPENED_PAGE}#new-win-from-window-open-subframe`,
+    },
+  });
+
+  yield BrowserTestUtils.removeTab(tab1);
+
+  yield extension.unload();
+});
--- a/toolkit/components/windowwatcher/nsWindowWatcher.cpp
+++ b/toolkit/components/windowwatcher/nsWindowWatcher.cpp
@@ -15,16 +15,17 @@
 #include "nsIAuthPrompt2.h"
 #include "nsISimpleEnumerator.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsJSUtils.h"
 #include "plstr.h"
 
 #include "nsDocShell.h"
 #include "nsGlobalWindow.h"
+#include "nsHashPropertyBag.h"
 #include "nsIBaseWindow.h"
 #include "nsIBrowserDOMWindow.h"
 #include "nsIDocShell.h"
 #include "nsIDocShellLoadInfo.h"
 #include "nsIDocShellTreeItem.h"
 #include "nsIDocShellTreeOwner.h"
 #include "nsIDocumentLoader.h"
 #include "nsIDocument.h"
@@ -1204,16 +1205,39 @@ nsWindowWatcher::OpenWindowInternal(mozI
       obsSvc->NotifyObservers(*aResult, "toplevel-window-ready", nullptr);
     }
   }
 
   // Before loading the URI we want to be 100% sure that we use the correct
   // userContextId.
   MOZ_ASSERT(CheckUserContextCompatibility(newDocShell));
 
+  // If this tab or window has been opened by a window.open call, we have to provide
+  // all the data needed to send a webNavigation.onCreatedNavigationTarget event.
+  if (aCalledFromJS && parentDocShell && newDocShellItem) {
+    nsCOMPtr<nsIObserverService> obsSvc =
+      mozilla::services::GetObserverService();
+
+    if (obsSvc) {
+      RefPtr<nsHashPropertyBag> props = new nsHashPropertyBag();
+
+      if (uriToLoad) {
+        // The url notified in the webNavigation.onCreatedNavigationTarget event.
+        props->SetPropertyAsACString(NS_LITERAL_STRING("url"),
+                                     uriToLoad->GetSpecOrDefault());
+      }
+
+      props->SetPropertyAsInterface(NS_LITERAL_STRING("sourceTabDocShell"), parentDocShell);
+      props->SetPropertyAsInterface(NS_LITERAL_STRING("createdTabDocShell"), newDocShellItem);
+
+      obsSvc->NotifyObservers(static_cast<nsIPropertyBag2*>(props),
+                              "webNavigation-createdNavigationTarget-from-js", nullptr);
+    }
+  }
+
   if (uriToLoad && aNavigate) {
     newDocShell->LoadURI(
       uriToLoad,
       loadInfo,
       windowIsNew ?
         static_cast<uint32_t>(nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD) :
         static_cast<uint32_t>(nsIWebNavigation::LOAD_FLAGS_NONE),
       true);
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -24,44 +24,53 @@ const RECENT_DATA_THRESHOLD = 5 * 100000
 var Manager = {
   // Map[string -> Map[listener -> URLFilter]]
   listeners: new Map(),
 
   init() {
     // Collect recent tab transition data in a WeakMap:
     //   browser -> tabTransitionData
     this.recentTabTransitionData = new WeakMap();
+
+    // Collect the pending created navigation target events that still have to
+    // pair the message received from the source tab to the one received from
+    // the new tab.
+    this.createdNavigationTargetByOuterWindowId = new Map();
+
     Services.obs.addObserver(this, "autocomplete-did-enter-text", true);
 
     Services.obs.addObserver(this, "webNavigation-createdNavigationTarget", false);
 
     Services.mm.addMessageListener("Content:Click", this);
     Services.mm.addMessageListener("Extension:DOMContentLoaded", this);
     Services.mm.addMessageListener("Extension:StateChange", this);
     Services.mm.addMessageListener("Extension:DocumentChange", this);
     Services.mm.addMessageListener("Extension:HistoryChange", this);
+    Services.mm.addMessageListener("Extension:CreatedNavigationTarget", this);
 
     Services.mm.loadFrameScript("resource://gre/modules/WebNavigationContent.js", true);
   },
 
   uninit() {
     // Stop collecting recent tab transition data and reset the WeakMap.
     Services.obs.removeObserver(this, "autocomplete-did-enter-text");
-    this.recentTabTransitionData = new WeakMap();
-
     Services.obs.removeObserver(this, "webNavigation-createdNavigationTarget");
 
     Services.mm.removeMessageListener("Content:Click", this);
     Services.mm.removeMessageListener("Extension:StateChange", this);
     Services.mm.removeMessageListener("Extension:DocumentChange", this);
     Services.mm.removeMessageListener("Extension:HistoryChange", this);
     Services.mm.removeMessageListener("Extension:DOMContentLoaded", this);
+    Services.mm.removeMessageListener("Extension:CreatedNavigationTarget", this);
 
     Services.mm.removeDelayedFrameScript("resource://gre/modules/WebNavigationContent.js");
     Services.mm.broadcastAsyncMessage("Extension:DisableWebNavigation");
+
+    this.recentTabTransitionData = new WeakMap();
+    this.createdNavigationTargetByOuterWindowId.clear();
   },
 
   addListener(type, listener, filters) {
     if (this.listeners.size == 0) {
       this.init();
     }
 
     if (!this.listeners.has(type)) {
@@ -273,30 +282,65 @@ var Manager = {
 
       case "Extension:DOMContentLoaded":
         this.onLoad(target, data);
         break;
 
       case "Content:Click":
         this.onContentClick(target, data);
         break;
+
+      case "Extension:CreatedNavigationTarget":
+        this.onCreatedNavigationTarget(target, data);
+        break;
     }
   },
 
   onContentClick(target, data) {
     // We are interested only on clicks to links which are not "add to bookmark" commands
     if (data.href && !data.bookmark) {
       let ownerWin = target.ownerGlobal;
       let where = ownerWin.whereToOpenLink(data);
       if (where == "current") {
         this.setRecentTabTransitionData({link: true});
       }
     }
   },
 
+  onCreatedNavigationTarget(browser, data) {
+    const {isSourceTab, createdWindowId, sourceWindowId, url} = data;
+
+    // We are going to potentially received two message manager messages for a single
+    // onCreatedNavigationTarget event that is happening in the child process,
+    // we are going to use the generate uuid to pair them together.
+    const pairedMessage = this.createdNavigationTargetByOuterWindowId.get(createdWindowId);
+
+    if (!pairedMessage) {
+      this.createdNavigationTargetByOuterWindowId.set(createdWindowId, {browser, data});
+      return;
+    }
+
+    this.createdNavigationTargetByOuterWindowId.delete(createdWindowId);
+
+    let sourceTabBrowser;
+    let createdTabBrowser;
+
+    if (isSourceTab) {
+      sourceTabBrowser = browser;
+      createdTabBrowser = pairedMessage.browser;
+    } else {
+      sourceTabBrowser = pairedMessage.browser;
+      createdTabBrowser = browser;
+    }
+
+    this.fire("onCreatedNavigationTarget", createdTabBrowser, {}, {
+      sourceTabBrowser, sourceWindowId, url,
+    });
+  },
+
   onStateChange(browser, data) {
     let stateFlags = data.stateFlags;
     if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
       let url = data.requestURL;
       if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
         this.fire("onBeforeNavigate", browser, data, {url});
       } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
         if (Components.isSuccessCode(data.status)) {
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -18,16 +18,65 @@ function loadListener(event) {
   sendAsyncMessage("Extension:DOMContentLoaded", {windowId, parentWindowId, url});
 }
 
 addEventListener("DOMContentLoaded", loadListener);
 addMessageListener("Extension:DisableWebNavigation", () => {
   removeEventListener("DOMContentLoaded", loadListener);
 });
 
+var CreatedNavigationTargetListener = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+  init() {
+    Services.obs.addObserver(this, "webNavigation-createdNavigationTarget-from-js", false);
+  },
+  uninit() {
+    Services.obs.removeObserver(this, "webNavigation-createdNavigationTarget-from-js");
+  },
+
+  observe(subject, topic, data) {
+    if (!(subject instanceof Ci.nsIPropertyBag2)) {
+      return;
+    }
+
+    let props = subject.QueryInterface(Ci.nsIPropertyBag2);
+
+    const createdDocShell = props.getPropertyAsInterface("createdTabDocShell", Ci.nsIDocShell);
+    const sourceDocShell = props.getPropertyAsInterface("sourceTabDocShell", Ci.nsIDocShell);
+
+    const isSourceTabDescendant = WebNavigationFrames.isDescendantDocShell(sourceDocShell, docShell);
+
+    if (docShell !== createdDocShell && docShell !== sourceDocShell &&
+        !isSourceTabDescendant) {
+      // if the createdNavigationTarget is not related to this docShell
+      // (this docShell is not the newly created docShell, it is not the source docShell,
+      // and the source docShell is not a descendant of it)
+      // there is nothing to do here and return early.
+      return;
+    }
+
+    const isSourceTab = docShell === sourceDocShell || isSourceTabDescendant;
+    const sourceWindowId = WebNavigationFrames.getDocShellWindowId(sourceDocShell);
+    const createdWindowId = WebNavigationFrames.getDocShellWindowId(createdDocShell);
+
+    let url;
+    if (props.hasKey("url")) {
+      url = props.getPropertyAsACString("url");
+    }
+
+    sendAsyncMessage("Extension:CreatedNavigationTarget", {
+      url,
+      sourceWindowId,
+      createdWindowId,
+      isSourceTab,
+    });
+  },
+};
+
 var FormSubmitListener = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsIFormSubmitObserver,
                                          Ci.nsISupportsWeakReference]),
   init() {
     this.formSubmitWindows = new WeakSet();
     Services.obs.addObserver(FormSubmitListener, "earlyformsubmit", false);
   },
@@ -251,22 +300,25 @@ var WebProgressListener = {
     Ci.nsIWebProgressListener2,
     Ci.nsISupportsWeakReference,
   ]),
 };
 
 var disabled = false;
 WebProgressListener.init();
 FormSubmitListener.init();
+CreatedNavigationTargetListener.init();
 addEventListener("unload", () => {
   if (!disabled) {
     disabled = true;
     WebProgressListener.uninit();
     FormSubmitListener.uninit();
+    CreatedNavigationTargetListener.uninit();
   }
 });
 addMessageListener("Extension:DisableWebNavigation", () => {
   if (!disabled) {
     disabled = true;
     WebProgressListener.uninit();
     FormSubmitListener.uninit();
+    CreatedNavigationTargetListener.uninit();
   }
 });
--- a/toolkit/modules/addons/WebNavigationFrames.jsm
+++ b/toolkit/modules/addons/WebNavigationFrames.jsm
@@ -15,16 +15,27 @@ function getWindowId(window) {
                .getInterface(Ci.nsIDOMWindowUtils)
                .outerWindowID;
 }
 
 function getParentWindowId(window) {
   return getWindowId(window.parent);
 }
 
+function getDocShellWindowId(docShell) {
+  if (!docShell) {
+    return undefined;
+  }
+
+  return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                 .getInterface(Ci.nsIDOMWindow)
+                 .getInterface(Ci.nsIDOMWindowUtils)
+                 .outerWindowID;
+}
+
 /**
  * Retrieve the DOMWindow associated to the docShell passed as parameter.
  *
  * @param    {nsIDocShell}  docShell - the docShell that we want to get the DOMWindow from.
  * @returns  {nsIDOMWindow}          - the DOMWindow associated to the docShell.
  */
 function docShellToWindow(docShell) {
   return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -113,18 +124,24 @@ function findDocShell(frameId, rootDocSh
     if (frameId == getFrameId(docShellToWindow(docShell))) {
       return docShell;
     }
   }
 
   return null;
 }
 
+function isDescendantDocShell(targetDocShell, rootDocShell) {
+  return (rootDocShell === targetDocShell.sameTypeRootTreeItem
+                                         .QueryInterface(Ci.nsIDocShell));
+}
+
 var WebNavigationFrames = {
   iterateDocShellTree,
+  isDescendantDocShell,
 
   findDocShell,
 
   getFrame(docShell, frameId) {
     let result = findDocShell(frameId, docShell);
     if (result) {
       return convertDocShellToFrameDetail(result);
     }
@@ -134,9 +151,10 @@ var WebNavigationFrames = {
   getFrameId,
 
   getAllFrames(docShell) {
     return Array.from(iterateDocShellTree(docShell), convertDocShellToFrameDetail);
   },
 
   getWindowId,
   getParentWindowId,
+  getDocShellWindowId,
 };