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 @@ + + + + WebNavigatio onCreatedNavigationTarget target + + + + Go back to the source page + + diff --git a/browser/components/extensions/test/browser/webNav_createdTargetSource.html b/browser/components/extensions/test/browser/webNav_createdTargetSource.html new file mode 100644 index 000000000000..3cfd4afa032a --- /dev/null +++ b/browser/components/extensions/test/browser/webNav_createdTargetSource.html @@ -0,0 +1,38 @@ + + + + WebNavigatio onCreatedNavigationTarget source + + + + + + + + diff --git a/browser/components/extensions/test/browser/webNav_createdTargetSource_subframe.html b/browser/components/extensions/test/browser/webNav_createdTargetSource_subframe.html new file mode 100644 index 000000000000..1ef3ab1318bc --- /dev/null +++ b/browser/components/extensions/test/browser/webNav_createdTargetSource_subframe.html @@ -0,0 +1,35 @@ + + + + WebNavigatio onCreatedNavigationTarget source subframe + + + + + + diff --git a/browser/modules/ContentClick.jsm b/browser/modules/ContentClick.jsm index 451d31d6a12a..6f35627bdb43 100644 --- a/browser/modules/ContentClick.jsm +++ b/browser/modules/ContentClick.jsm @@ -85,6 +85,7 @@ var ContentClick = { allowMixedContent: json.allowMixedContent, isContentWindowPrivate: json.isContentWindowPrivate, originPrincipal: json.originPrincipal, + frameOuterWindowID: json.frameOuterWindowID, }; // The new tab/window must use the same userContextId. diff --git a/toolkit/components/extensions/ext-webNavigation.js b/toolkit/components/extensions/ext-webNavigation.js index b5cea4035e6b..08e3974172fb 100644 --- a/toolkit/components/extensions/ext-webNavigation.js +++ b/toolkit/components/extensions/ext-webNavigation.js @@ -113,20 +113,31 @@ function WebNavigationEventManager(context, eventName) { 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); @@ -166,7 +177,7 @@ extensions.registerSchemaAPI("webNavigation", "addon_parent", context => { 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); diff --git a/toolkit/components/extensions/schemas/web_navigation.json b/toolkit/components/extensions/schemas/web_navigation.json index 1e13b181ace6..3fda550e9125 100644 --- a/toolkit/components/extensions/schemas/web_navigation.json +++ b/toolkit/components/extensions/schemas/web_navigation.json @@ -284,7 +284,6 @@ }, { "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": [ diff --git a/toolkit/modules/addons/WebNavigation.jsm b/toolkit/modules/addons/WebNavigation.jsm index 6aff80a62926..939831417885 100644 --- a/toolkit/modules/addons/WebNavigation.jsm +++ b/toolkit/modules/addons/WebNavigation.jsm @@ -21,9 +21,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", // 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(), @@ -34,6 +31,8 @@ var Manager = { 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); @@ -48,6 +47,8 @@ var Manager = { 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); @@ -92,16 +93,33 @@ var Manager = { 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, + }); } }, @@ -357,7 +375,7 @@ const EVENTS = [ "onErrorOccurred", "onReferenceFragmentUpdated", "onHistoryStateUpdated", - // "onCreatedNavigationTarget", + "onCreatedNavigationTarget", ]; var WebNavigation = {};