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
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),