Bug 1190687 - [webext] webNavigation.onCreatedNavigationTarget on new windows/tabs from context menu and user clicks on links. r=kmag
authorLuca Greco <lgreco@mozilla.com>
Fri, 24 Feb 2017 19:49:38 +0100
changeset 345572 3884829f39b0
parent 345571 53796fc215f8
child 345573 1733ced10f06
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)
reviewerskmag
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 context menu and user clicks on links. r=kmag MozReview-Commit-ID: KYVKkVUSOzR
browser/base/content/browser.js
browser/base/content/content.js
browser/base/content/nsContextMenu.js
browser/base/content/utilityOverlay.js
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js
browser/components/extensions/test/browser/webNav_createdTarget.html
browser/components/extensions/test/browser/webNav_createdTargetSource.html
browser/components/extensions/test/browser/webNav_createdTargetSource_subframe.html
browser/modules/ContentClick.jsm
toolkit/components/extensions/ext-webNavigation.js
toolkit/components/extensions/schemas/web_navigation.json
toolkit/modules/addons/WebNavigation.jsm
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5735,24 +5735,29 @@ function handleLinkClick(event, href, li
       linkNode) {
     let referrerAttrValue = Services.netUtils.parseAttributePolicyString(linkNode.
                             getAttribute("referrerpolicy"));
     if (referrerAttrValue != Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
       referrerPolicy = referrerAttrValue;
     }
   }
 
+  let frameOuterWindowID = doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIDOMWindowUtils)
+                              .outerWindowID;
+
   urlSecurityCheck(href, doc.nodePrincipal);
   let params = {
     charset: doc.characterSet,
     allowMixedContent: persistAllowMixedContentInChildTab,
     referrerURI,
     referrerPolicy,
     noReferrer: BrowserUtils.linkHasNoReferrer(linkNode),
     originPrincipal: doc.nodePrincipal,
+    frameOuterWindowID,
   };
 
   // The new tab/window must use the same userContextId
   if (doc.nodePrincipal.originAttributes.userContextId) {
     params.userContextId = doc.nodePrincipal.originAttributes.userContextId;
   }
 
   openLinkIn(href, where, params);
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -496,20 +496,24 @@ var ClickEventHandler = {
         node) {
       let referrerAttrValue = Services.netUtils.parseAttributePolicyString(node.
                               getAttribute("referrerpolicy"));
       if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
         referrerPolicy = referrerAttrValue;
       }
     }
 
+    let frameOuterWindowID = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+                                     .getInterface(Ci.nsIDOMWindowUtils)
+                                     .outerWindowID;
+
     let json = { button: event.button, shiftKey: event.shiftKey,
                  ctrlKey: event.ctrlKey, metaKey: event.metaKey,
                  altKey: event.altKey, href: null, title: null,
-                 bookmark: false, referrerPolicy,
+                 bookmark: false, frameOuterWindowID, referrerPolicy,
                  triggeringPrincipal: principal,
                  originAttributes: principal ? principal.originAttributes : {},
                  isContentWindowPrivate: PrivateBrowsingUtils.isContentWindowPrivate(ownerDoc.defaultView)};
 
     if (href) {
       try {
         BrowserUtils.urlSecurityCheck(href, principal);
       } catch (e) {
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -946,21 +946,27 @@ nsContextMenu.prototype = {
            this.target.mediaKeys.keySystem != "org.w3.clearkey";
   },
 
   _openLinkInParameters : function (extra) {
     let params = { charset: gContextMenuContentData.charSet,
                    originPrincipal: this.principal,
                    referrerURI: gContextMenuContentData.documentURIObject,
                    referrerPolicy: gContextMenuContentData.referrerPolicy,
+                   frameOuterWindowID: gContextMenuContentData.frameOuterWindowID,
                    noReferrer: this.linkHasNoReferrer };
     for (let p in extra) {
       params[p] = extra[p];
     }
 
+    if (!this.isRemote) {
+      params.frameOuterWindowID = this.target.ownerGlobal
+                                      .QueryInterface(Ci.nsIInterfaceRequestor)
+                                      .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+    }
     // If we want to change userContextId, we must be sure that we don't
     // propagate the referrer.
     if ("userContextId" in params &&
         params.userContextId != gContextMenuContentData.userContextId) {
       params.noReferrer = true;
     }
 
     return params;
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -299,17 +299,40 @@ function openLinkIn(url, where, params) 
     sa.appendElement(userContextIdSupports, /* weak =*/ false);
     sa.appendElement(aPrincipal, /* weak =*/ false);
 
     let features = "chrome,dialog=no,all";
     if (aIsPrivate) {
       features += ",private";
     }
 
-    Services.ww.openWindow(w || window, getBrowserURL(), null, features, sa);
+    const sourceWindow = (w || window);
+    let win;
+    if (params.frameOuterWindowID && sourceWindow) {
+      // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget
+      // event if it contains the expected frameOuterWindowID params.
+      // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is
+      // opening a new window using the keyboard shortcut).
+      const sourceTabBrowser = sourceWindow.gBrowser.selectedBrowser;
+      let delayedStartupObserver = aSubject => {
+        if (aSubject == win) {
+          Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
+          Services.obs.notifyObservers({
+            wrappedJSObject: {
+              url,
+              createdTabBrowser: win.gBrowser.selectedBrowser,
+              sourceTabBrowser,
+              sourceFrameOuterWindowID: params.frameOuterWindowID,
+            },
+          }, "webNavigation-createdNavigationTarget", null);
+        }
+      };
+      Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
+    }
+    win = Services.ww.openWindow(sourceWindow, getBrowserURL(), null, features, sa);
     return;
   }
 
   // We're now committed to loading the link in an existing browser window.
 
   // Raise the target window before loading the URI, since loading it may
   // result in a new frontmost window (e.g. "javascript:window.open('');").
   w.focus();
@@ -401,16 +424,31 @@ function openLinkIn(url, where, params) 
       skipAnimation: aSkipTabAnimation,
       allowMixedContent: aAllowMixedContent,
       noReferrer: aNoReferrer,
       userContextId: aUserContextId,
       originPrincipal: aPrincipal,
       triggeringPrincipal: aPrincipal,
     });
     targetBrowser = tabUsedForLoad.linkedBrowser;
+
+    if (params.frameOuterWindowID && w) {
+      // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget
+      // event if it contains the expected frameOuterWindowID params.
+      // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is
+      // opening a new tab using the keyboard shortcut).
+      Services.obs.notifyObservers({
+        wrappedJSObject: {
+          url,
+          createdTabBrowser: targetBrowser,
+          sourceTabBrowser: w.gBrowser.selectedBrowser,
+          sourceFrameOuterWindowID: params.frameOuterWindowID,
+        },
+      }, "webNavigation-createdNavigationTarget", null);
+    }
     break;
   }
 
   // Focus the content, but only if the browser used for the load is selected.
   if (targetBrowser == w.gBrowser.selectedBrowser) {
     targetBrowser.focus();
   }
 
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -14,16 +14,19 @@ support-files =
   file_iframe_document.sjs
   file_bypass_cache.sjs
   file_language_fr_en.html
   file_language_ja.html
   file_language_tlh.html
   file_dummy.html
   file_inspectedwindow_reload_target.sjs
   file_serviceWorker.html
+  webNav_createdTarget.html
+  webNav_createdTargetSource.html
+  webNav_createdTargetSource_subframe.html
   serviceWorker.js
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
   ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js
 
 [browser_ext_browserAction_area.js]
 [browser_ext_browserAction_context.js]
 [browser_ext_browserAction_disabled.js]
@@ -115,16 +118,17 @@ support-files =
 [browser_ext_tabs_update_url.js]
 [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_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.js
@@ -0,0 +1,285 @@
+/* -*- 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.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");
+}
+
+async function clickContextMenuItem({pageElementSelector, contextMenuItemLabel}) {
+  const contentAreaContextMenu = await openContextMenu(pageElementSelector);
+  const item = contentAreaContextMenu.getElementsByAttribute("label", contextMenuItemLabel);
+  is(item.length, 1, `found contextMenu item for "${contextMenuItemLabel}"`);
+  item[0].click();
+  await closeContextMenu();
+}
+
+add_task(function* test_on_created_navigation_target_from_mouse_click() {
+  const tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, SOURCE_PAGE);
+
+  const extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["webNavigation"],
+    },
+  });
+
+  yield extension.startup();
+
+  const expectedSourceTab = yield extension.awaitMessage("expectedSourceTab");
+
+  info("Open link in a new tab using Ctrl-click");
+
+  yield runTestCase({
+    extension,
+    openNavTarget() {
+      BrowserTestUtils.synthesizeMouseAtCenter("#test-create-new-tab-from-mouse-click",
+                                               {ctrlKey: true, metaKey: true},
+                                               tab.linkedBrowser);
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: 0,
+      url: `${OPENED_PAGE}#new-tab-from-mouse-click`,
+    },
+  });
+
+  info("Open link in a new window using Shift-click");
+
+  yield runTestCase({
+    extension,
+    openNavTarget() {
+      BrowserTestUtils.synthesizeMouseAtCenter("#test-create-new-window-from-mouse-click",
+                                               {shiftKey: true},
+                                               tab.linkedBrowser);
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: 0,
+      url: `${OPENED_PAGE}#new-window-from-mouse-click`,
+    },
+  });
+
+  yield BrowserTestUtils.removeTab(tab);
+
+  yield extension.unload();
+});
+
+add_task(function* test_on_created_navigation_target_from_context_menu() {
+  const tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, SOURCE_PAGE);
+
+  const extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["webNavigation"],
+    },
+  });
+
+  yield extension.startup();
+
+  const expectedSourceTab = yield extension.awaitMessage("expectedSourceTab");
+
+  info("Open link in a new tab from the context menu");
+
+  yield runTestCase({
+    extension,
+    async openNavTarget() {
+      await clickContextMenuItem({
+        pageElementSelector: "#test-create-new-tab-from-context-menu",
+        contextMenuItemLabel: "Open Link in New Tab",
+      });
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: 0,
+      url: `${OPENED_PAGE}#new-tab-from-context-menu`,
+    },
+  });
+
+  info("Open link in a new window from the context menu");
+
+  yield runTestCase({
+    extension,
+    async openNavTarget() {
+      await clickContextMenuItem({
+        pageElementSelector: "#test-create-new-window-from-context-menu",
+        contextMenuItemLabel: "Open Link in New Window",
+      });
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: 0,
+      url: `${OPENED_PAGE}#new-window-from-context-menu`,
+    },
+  });
+
+  yield BrowserTestUtils.removeTab(tab);
+
+  yield extension.unload();
+});
+
+add_task(function* test_on_created_navigation_target_from_mouse_click_subframe() {
+  const tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, SOURCE_PAGE);
+
+  const extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["webNavigation"],
+    },
+  });
+
+  yield extension.startup();
+
+  const expectedSourceTab = yield extension.awaitMessage("expectedSourceTab");
+
+  info("Open a subframe link in a new tab using Ctrl-click");
+
+  yield runTestCase({
+    extension,
+    openNavTarget() {
+      BrowserTestUtils.synthesizeMouseAtCenter(function() {
+        // This code runs as a framescript in the child process and it returns the
+        // target link in the subframe.
+        return this.content.frames[0].document // eslint-disable-line mozilla/no-cpows-in-tests
+          .querySelector("#test-create-new-tab-from-mouse-click-subframe");
+      }, {ctrlKey: true, metaKey: true}, tab.linkedBrowser);
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+      url: `${OPENED_PAGE}#new-tab-from-mouse-click-subframe`,
+    },
+  });
+
+  info("Open a subframe link in a new window using Shift-click");
+
+  yield runTestCase({
+    extension,
+    openNavTarget() {
+      BrowserTestUtils.synthesizeMouseAtCenter(function() {
+        // This code runs as a framescript in the child process and it returns the
+        // target link in the subframe.
+        return this.content.frames[0].document // eslint-disable-line mozilla/no-cpows-in-tests
+                      .querySelector("#test-create-new-window-from-mouse-click-subframe");
+      }, {shiftKey: true}, tab.linkedBrowser);
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+      url: `${OPENED_PAGE}#new-window-from-mouse-click-subframe`,
+    },
+  });
+
+  yield BrowserTestUtils.removeTab(tab);
+
+  yield extension.unload();
+});
+
+add_task(function* test_on_created_navigation_target_from_context_menu_subframe() {
+  const tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, SOURCE_PAGE);
+
+  const extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["webNavigation"],
+    },
+  });
+
+  yield extension.startup();
+
+  const expectedSourceTab = yield extension.awaitMessage("expectedSourceTab");
+
+  info("Open a subframe link in a new tab from the context menu");
+
+  yield runTestCase({
+    extension,
+    async openNavTarget() {
+      await clickContextMenuItem({
+        pageElementSelector() {
+          // This code runs as a framescript in the child process and it returns the
+          // target link in the subframe.
+          return this.content.frames[0] // eslint-disable-line mozilla/no-cpows-in-tests
+            .document.querySelector("#test-create-new-tab-from-context-menu-subframe");
+        },
+        contextMenuItemLabel: "Open Link in New Tab",
+      });
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+      url: `${OPENED_PAGE}#new-tab-from-context-menu-subframe`,
+    },
+  });
+
+  info("Open a subframe link in a new window from the context menu");
+
+  yield runTestCase({
+    extension,
+    async openNavTarget() {
+      await clickContextMenuItem({
+        pageElementSelector() {
+          // This code runs as a framescript in the child process and it returns the
+          // target link in the subframe.
+          return this.content.frames[0] // eslint-disable-line mozilla/no-cpows-in-tests
+            .document.querySelector("#test-create-new-window-from-context-menu-subframe");
+        },
+        contextMenuItemLabel: "Open Link in New Window",
+      });
+    },
+    expectedWebNavProps: {
+      sourceTabId: expectedSourceTab.sourceTabId,
+      sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+      url: `${OPENED_PAGE}#new-window-from-context-menu-subframe`,
+    },
+  });
+
+  yield BrowserTestUtils.removeTab(tab);
+
+  yield extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/webNav_createdTarget.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>WebNavigatio onCreatedNavigationTarget target</title>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <a id="other-link" href="webNav_createdTarget_source.html">Go back to the source page</a>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/webNav_createdTargetSource.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>WebNavigatio onCreatedNavigationTarget source</title>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <ul>
+      <li>
+        <a id="test-create-new-tab-from-context-menu"
+            href="webNav_createdTarget.html#new-tab-from-context-menu">
+          Open a target page in a new tab from the context menu
+        </a>
+      </li>
+      <li>
+        <a id="test-create-new-window-from-context-menu"
+           href="webNav_createdTarget.html#new-window-from-context-menu">
+          Open a target page in a new window from the context menu
+        </a>
+      </li>
+      <li>
+        <a id="test-create-new-tab-from-mouse-click"
+           href="webNav_createdTarget.html#new-tab-from-mouse-click">
+          Open a target page in a new tab from mouse click
+        </a>
+      </li>
+      <li>
+        <a id="test-create-new-window-from-mouse-click"
+           href="webNav_createdTarget.html#new-window-from-mouse-click">
+          Open a target page in a new window from mouse click
+        </a>
+      </li>
+    </ul>
+
+    <iframe src="webNav_createdTargetSource_subframe.html" style="width: 100%; height: 100%;">
+    </iframe>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/webNav_createdTargetSource_subframe.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>WebNavigatio onCreatedNavigationTarget source subframe</title>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <ul>
+      <li>
+        <a id="test-create-new-tab-from-context-menu-subframe"
+            href="webNav_createdTarget.html#new-tab-from-context-menu-subframe">
+          Open a target page in a new tab from the context menu (subframe)
+        </a>
+      </li>
+      <li>
+        <a id="test-create-new-window-from-context-menu-subframe"
+           href="webNav_createdTarget.html#new-window-from-context-menu-subframe">
+          Open a target page in a new window from the context menu (subframe)
+        </a>
+      </li>
+      <li>
+        <a id="test-create-new-tab-from-mouse-click-subframe"
+           href="webNav_createdTarget.html#new-tab-from-mouse-click-subframe">
+          Open a target page in a new tab from mouse click (subframe)
+        </a>
+      </li>
+      <li>
+        <a id="test-create-new-window-from-mouse-click-subframe"
+           href="webNav_createdTarget.html#new-window-from-mouse-click-subframe">
+          Open a target page in a new window from mouse click (subframe)
+        </a>
+      </li>
+    </ul>
+  </body>
+</html>
--- a/browser/modules/ContentClick.jsm
+++ b/browser/modules/ContentClick.jsm
@@ -80,16 +80,17 @@ var ContentClick = {
     let params = {
       charset: browser.characterSet,
       referrerURI: browser.documentURI,
       referrerPolicy: json.referrerPolicy,
       noReferrer: json.noReferrer,
       allowMixedContent: json.allowMixedContent,
       isContentWindowPrivate: json.isContentWindowPrivate,
       originPrincipal: json.originPrincipal,
+      frameOuterWindowID: json.frameOuterWindowID,
     };
 
     // The new tab/window must use the same userContextId.
     if (json.originAttributes.userContextId) {
       params.userContextId = json.originAttributes.userContextId;
     }
 
     window.openLinkIn(json.href, where, params);
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -108,30 +108,41 @@ function WebNavigationEventManager(conte
     let listener = data => {
       if (!data.browser) {
         return;
       }
 
       let data2 = {
         url: data.url,
         timeStamp: Date.now(),
-        frameId: ExtensionManagement.getFrameId(data.windowId),
-        parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
       };
 
       if (eventName == "onErrorOccurred") {
         data2.error = data.error;
       }
 
+      if (data.windowId) {
+        data2.frameId = ExtensionManagement.getFrameId(data.windowId);
+        data2.parentFrameId = ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId);
+      }
+
+      if (data.sourceWindowId) {
+        data2.sourceFrameId = ExtensionManagement.getFrameId(data.sourceWindowId);
+      }
+
       // Fills in tabId typically.
       Object.assign(data2, tabTracker.getBrowserData(data.browser));
       if (data2.tabId < 0) {
         return;
       }
 
+      if (data.sourceTabBrowser) {
+        data2.sourceTabId = tabTracker.getBrowserData(data.sourceTabBrowser).tabId;
+      }
+
       fillTransitionProperties(eventName, data, data2);
 
       fire.async(data2);
     };
 
     WebNavigation[eventName].addListener(listener, filters);
     return () => {
       WebNavigation[eventName].removeListener(listener);
@@ -161,17 +172,17 @@ extensions.registerSchemaAPI("webNavigat
       onTabReplaced: ignoreEvent(context, "webNavigation.onTabReplaced"),
       onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
       onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
       onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
       onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
       onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
       onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
       onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
-      onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
+      onCreatedNavigationTarget: new WebNavigationEventManager(context, "onCreatedNavigationTarget").api(),
       getAllFrames(details) {
         let tab = tabManager.get(details.tabId);
 
         let {innerWindowID, messageManager} = tab.browser;
         let recipient = {innerWindowID};
 
         return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, {recipient})
                       .then((results) => results.map(convertGetFrameResult.bind(null, details.tabId)));
--- a/toolkit/components/extensions/schemas/web_navigation.json
+++ b/toolkit/components/extensions/schemas/web_navigation.json
@@ -279,17 +279,16 @@
             "optional": true,
             "$ref": "EventUrlFilters",
             "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
           }
         ]
       },
       {
         "name": "onCreatedNavigationTarget",
-        "unsupported": true,
         "type": "function",
         "description": "Fired when a new window, or a new tab in an existing window, is created to host a navigation.",
         "parameters": [
           {
             "type": "object",
             "name": "details",
             "properties": {
               "sourceTabId": {"type": "integer", "description": "The ID of the tab in which the navigation is triggered."},
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -16,43 +16,44 @@ Cu.import("resource://gre/modules/Servic
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 
 // Maximum amount of time that can be passed and still consider
 // the data recent (similar to how is done in nsNavHistory,
 // e.g. nsNavHistory::CheckIsRecentEvent, but with a lower threshold value).
 const RECENT_DATA_THRESHOLD = 5 * 1000000;
 
-// TODO:
-// onCreatedNavigationTarget
-
 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();
     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.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.removeDelayedFrameScript("resource://gre/modules/WebNavigationContent.js");
     Services.mm.broadcastAsyncMessage("Extension:DisableWebNavigation");
@@ -87,26 +88,43 @@ var Manager = {
 
   /**
    * Support nsIObserver interface to observe the urlbar autocomplete events used
    * to keep track of the urlbar user interaction.
    */
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
   /**
-   * Observe autocomplete-did-enter-text topic to track the user interaction with
-   * the awesome bar.
+   * Observe autocomplete-did-enter-text (to track the user interaction with the awesomebar)
+   * and webNavigation-createdNavigationTarget (to fire the onCreatedNavigationTarget
+   * related to windows or tabs opened from the main process) topics.
    *
-   * @param {nsIAutoCompleteInput} subject
+   * @param {nsIAutoCompleteInput|Object} subject
    * @param {string} topic
-   * @param {string} data
+   * @param {string|undefined} data
    */
   observe: function(subject, topic, data) {
     if (topic == "autocomplete-did-enter-text") {
       this.onURLBarAutoCompletion(subject);
+    } else if (topic == "webNavigation-createdNavigationTarget") {
+      // The observed notification is coming from privileged JavaScript components running
+      // in the main process (e.g. when a new tab or window is opened using the context menu
+      // or Ctrl/Shift + click on a link).
+      const {
+        createdTabBrowser,
+        url,
+        sourceFrameOuterWindowID,
+        sourceTabBrowser,
+      } = subject.wrappedJSObject;
+
+      this.fire("onCreatedNavigationTarget", createdTabBrowser, {}, {
+        sourceTabBrowser,
+        sourceWindowId: sourceFrameOuterWindowID,
+        url,
+      });
     }
   },
 
   /**
    * Recognize the type of urlbar user interaction (e.g. typing a new url,
    * clicking on an url generated from a searchengine or a keyword, or a
    * bookmark found by the urlbar autocompletion).
    *
@@ -352,17 +370,17 @@ var Manager = {
 const EVENTS = [
   "onBeforeNavigate",
   "onCommitted",
   "onDOMContentLoaded",
   "onCompleted",
   "onErrorOccurred",
   "onReferenceFragmentUpdated",
   "onHistoryStateUpdated",
-  // "onCreatedNavigationTarget",
+  "onCreatedNavigationTarget",
 ];
 
 var WebNavigation = {};
 
 for (let event of EVENTS) {
   WebNavigation[event] = {
     addListener: Manager.addListener.bind(Manager, event),
     removeListener: Manager.removeListener.bind(Manager, event),