diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 3c6a4f8e8d88..91959ad92fd1 100755 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -5740,6 +5740,10 @@ function handleLinkClick(event, href, linkNode) { } } + let frameOuterWindowID = doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + urlSecurityCheck(href, doc.nodePrincipal); let params = { charset: doc.characterSet, @@ -5748,6 +5752,7 @@ function handleLinkClick(event, href, linkNode) { referrerPolicy, noReferrer: BrowserUtils.linkHasNoReferrer(linkNode), originPrincipal: doc.nodePrincipal, + frameOuterWindowID, }; // The new tab/window must use the same userContextId diff --git a/browser/base/content/content.js b/browser/base/content/content.js index d19bd46638fe..1be217b95de6 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -501,10 +501,14 @@ var ClickEventHandler = { } } + 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)}; diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js index f716e1b8d8bb..6f22c20168c8 100644 --- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -951,11 +951,17 @@ nsContextMenu.prototype = { 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 && diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js index 9c02ce086805..94c1c98b0db8 100644 --- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -304,7 +304,30 @@ function openLinkIn(url, where, params) { 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; } @@ -406,6 +429,21 @@ function openLinkIn(url, where, params) { 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; } diff --git a/browser/components/extensions/test/browser/browser-common.ini b/browser/components/extensions/test/browser/browser-common.ini index 3de527ce012f..6ebc6f5bcbe9 100644 --- a/browser/components/extensions/test/browser/browser-common.ini +++ b/browser/components/extensions/test/browser/browser-common.ini @@ -19,6 +19,9 @@ support-files = 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 @@ -120,6 +123,7 @@ support-files = [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] diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js new file mode 100644 index 000000000000..3861b1401d42 --- /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(); +}); diff --git a/browser/components/extensions/test/browser/webNav_createdTarget.html b/browser/components/extensions/test/browser/webNav_createdTarget.html new file mode 100644 index 000000000000..e8a985ef280a --- /dev/null +++ b/browser/components/extensions/test/browser/webNav_createdTarget.html @@ -0,0 +1,10 @@ + + +
+