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 345600 1733ced10f06
parent 345599 3884829f39b0
child 345601 e87b41eb5ac0
push id38218
push usercbook@mozilla.com
push date2017-03-02 15:30 +0000
treeherderautoland@1733ced10f06 [default view] [failures only]
reviewersbillm
bugs1190687
milestone54.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 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,
 };