diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 64bab110d0..8c97910941 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -5466,6 +5466,9 @@ function handleLinkClick(event, href, linkNode) { urlSecurityCheck(href, doc.nodePrincipal); let params = { charset: doc.characterSet, + currentBrowser: gBrowser.getBrowserForDocument(doc), + frameOuterWindowID: doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID, allowMixedContent: persistAllowMixedContentInChildTab, referrerURI: referrerURI, referrerPolicy: referrerPolicy, diff --git a/browser/base/content/content.js b/browser/base/content/content.js index 4b5bb9ac4c..ca7458616a 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -442,7 +442,9 @@ var ClickEventHandler = { } } - let json = { button: event.button, shiftKey: event.shiftKey, + let json = { frameOuterWindowID: ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID, + button: event.button, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, href: null, title: null, bookmark: false, referrerPolicy: referrerPolicy, diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js index 61740a1599..41e1133da9 100644 --- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -938,6 +938,8 @@ nsContextMenu.prototype = { _openLinkInParameters : function (extra) { let params = { charset: gContextMenuContentData.charSet, + currentBrowser: this.browser, + frameOuterWindowID: this.frameOuterWindowID, originPrincipal: this.principal, triggeringPrincipal: this.principal, referrerURI: gContextMenuContentData.documentURIObject, diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js index fa2ebeb0a4..5022116510 100644 --- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -273,6 +273,18 @@ function openLinkIn(url, where, params) { // Note that if |w| is null we might have no current browser (we'll open a new window). var aCurrentBrowser = params.currentBrowser || (w && w.gBrowser.selectedBrowser); + // Capture the source before opening a foreground tab changes the selection. + function notifyNavigationTarget(createdTabBrowser) { + if (params.frameOuterWindowID && aCurrentBrowser) { + Services.obs.notifyObservers({wrappedJSObject: { + url, + createdTabBrowser, + sourceTabBrowser: aCurrentBrowser, + sourceFrameOuterWindowID: params.frameOuterWindowID, + }}, "webNavigation-createdNavigationTarget", null); + } + } + if (where == "save") { // TODO(1073187): propagate referrerPolicy. @@ -365,7 +377,21 @@ function openLinkIn(url, where, params) { features += ",private"; } - Services.ww.openWindow(w || window, getBrowserURL(), null, features, sa); + let newWindow = Services.ww.openWindow(w || window, getBrowserURL(), null, features, sa); + if (params.frameOuterWindowID && aCurrentBrowser) { + let cleanup = () => { + Services.obs.removeObserver(observer, "browser-delayed-startup-finished"); + newWindow.removeEventListener("unload", cleanup); + }; + let observer = subject => { + if (subject == newWindow) { + cleanup(); + notifyNavigationTarget(newWindow.gBrowser.selectedBrowser); + } + }; + Services.obs.addObserver(observer, "browser-delayed-startup-finished", false); + newWindow.addEventListener("unload", cleanup); + } return; } @@ -474,6 +500,7 @@ function openLinkIn(url, where, params) { triggeringPrincipal: aTriggeringPrincipal, }); browserUsedForLoad = tabUsedForLoad.linkedBrowser; + notifyNavigationTarget(browserUsedForLoad); break; } diff --git a/browser/modules/ContentClick.jsm b/browser/modules/ContentClick.jsm index 40101d5d35..89a744f940 100644 --- a/browser/modules/ContentClick.jsm +++ b/browser/modules/ContentClick.jsm @@ -79,6 +79,8 @@ var ContentClick = { let params = { charset: browser.characterSet, + currentBrowser: browser, + frameOuterWindowID: json.frameOuterWindowID, referrerURI: browser.documentURI, referrerPolicy: json.referrerPolicy, noReferrer: json.noReferrer, diff --git a/embedding/components/windowwatcher/nsWindowWatcher.cpp b/embedding/components/windowwatcher/nsWindowWatcher.cpp index aa4d5e3914..b554c08d98 100644 --- a/embedding/components/windowwatcher/nsWindowWatcher.cpp +++ b/embedding/components/windowwatcher/nsWindowWatcher.cpp @@ -6,6 +6,7 @@ //#define USEWEAKREFS // (haven't quite figured that out yet) #include "nsWindowWatcher.h" +#include "nsHashPropertyBag.h" #include "nsAutoWindowStateHelper.h" #include "nsCRT.h" @@ -1212,6 +1213,29 @@ nsWindowWatcher::OpenWindowInternal(mozIDOMWindowProxy* aParent, // userContextId. MOZ_ASSERT(CheckUserContextCompatibility(newDocShell)); + // If this tab or window has been opened by a window.open call, we have to provide + // all the data needed to send a webNavigation.onCreatedNavigationTarget event. + if (windowIsNew && parentDocShell && newDocShellItem) { + nsCOMPtr obsSvc = + mozilla::services::GetObserverService(); + + if (obsSvc) { + RefPtr props = new nsHashPropertyBag(); + + if (uriToLoad) { + // The url notified in the webNavigation.onCreatedNavigationTarget event. + props->SetPropertyAsACString(NS_LITERAL_STRING("url"), + uriToLoad->GetSpecOrDefault()); + } + + props->SetPropertyAsInterface(NS_LITERAL_STRING("sourceTabDocShell"), parentDocShell); + props->SetPropertyAsInterface(NS_LITERAL_STRING("createdTabDocShell"), newDocShellItem); + + obsSvc->NotifyObservers(static_cast(props), + "webNavigation-createdNavigationTarget-from-js", nullptr); + } + } + if (uriToLoad && aNavigate) { newDocShell->LoadURI( uriToLoad, diff --git a/toolkit/components/webextensions/ext-webNavigation.js b/toolkit/components/webextensions/ext-webNavigation.js index 904f3a4a78..fd318e58a8 100644 --- a/toolkit/components/webextensions/ext-webNavigation.js +++ b/toolkit/components/webextensions/ext-webNavigation.js @@ -115,6 +115,20 @@ function WebNavigationEventManager(context, eventName) { parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId), }; + if (eventName == "onCreatedNavigationTarget") { + let source = {}; + extensions.emit("fill-browser-data", data.sourceTabBrowser, source); + if (!(source.tabId >= 0)) { + return; + } + delete data2.frameId; + delete data2.parentFrameId; + data2.sourceTabId = source.tabId; + data2.sourceFrameId = ExtensionManagement.getFrameId(data.sourceWindowId); + // Firefox does not expose renderer process IDs through this API. + data2.sourceProcessId = -1; + } + if (eventName == "onErrorOccurred") { data2.error = data.error; } @@ -162,7 +176,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.getTab(details.tabId, context); diff --git a/toolkit/components/webextensions/schemas/web_navigation.json b/toolkit/components/webextensions/schemas/web_navigation.json index 1e13b181ac..3fda550e91 100644 --- a/toolkit/components/webextensions/schemas/web_navigation.json +++ b/toolkit/components/webextensions/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 6302a9d790..3754e13692 100644 --- a/toolkit/modules/addons/WebNavigation.jsm +++ b/toolkit/modules/addons/WebNavigation.jsm @@ -12,6 +12,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource:///modules/RecentWindow.jsm"); @@ -21,8 +22,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]] @@ -32,7 +31,10 @@ var Manager = { // Collect recent tab transition data in a WeakMap: // browser -> tabTransitionData this.recentTabTransitionData = new WeakMap(); + this.createdNavigationTargetByOuterWindowId = new Map(); Services.obs.addObserver(this, "autocomplete-did-enter-text", true); + Services.obs.addObserver(this, "webNavigation-createdNavigationTarget", false); + Services.mm.addMessageListener("Extension:CreatedNavigationTarget", this); Services.mm.addMessageListener("Content:Click", this); Services.mm.addMessageListener("Extension:DOMContentLoaded", this); @@ -45,7 +47,13 @@ var Manager = { uninit() { // Stop collecting recent tab transition data and reset the WeakMap. - Services.obs.removeObserver(this, "autocomplete-did-enter-text", true); + Services.obs.removeObserver(this, "autocomplete-did-enter-text"); + Services.obs.removeObserver(this, "webNavigation-createdNavigationTarget"); + Services.mm.removeMessageListener("Extension:CreatedNavigationTarget", this); + for (let pending of this.createdNavigationTargetByOuterWindowId.values()) { + clearTimeout(pending.timer); + } + this.createdNavigationTargetByOuterWindowId.clear(); this.recentTabTransitionData = new WeakMap(); Services.mm.removeMessageListener("Content:Click", this); @@ -102,6 +110,22 @@ var Manager = { 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, + }); } }, @@ -241,6 +265,9 @@ var Manager = { */ receiveMessage({name, data, target}) { switch (name) { + case "Extension:CreatedNavigationTarget": + this.onCreatedNavigationTarget(target, data); + break; case "Extension:StateChange": this.onStateChange(target, data); break; @@ -274,6 +301,44 @@ var Manager = { } }, + onCreatedNavigationTarget(browser, data) { + const {isSourceTab, createdWindowId, sourceWindowId, url} = data; + + // Source and target frame scripts identify their browsers independently. + // Pair their messages by the new window's outer ID, in either arrival order. + const pairedMessage = this.createdNavigationTargetByOuterWindowId.get(createdWindowId); + + if (!pairedMessage) { + // A tab can close before its frame script reports. Do not retain it forever. + let timer = setTimeout(() => { + this.createdNavigationTargetByOuterWindowId.delete(createdWindowId); + }, 30000); + this.createdNavigationTargetByOuterWindowId.set(createdWindowId, {browser, data, timer}); + return; + } + + if (pairedMessage.data.isSourceTab == isSourceTab) { + return; + } + clearTimeout(pairedMessage.timer); + this.createdNavigationTargetByOuterWindowId.delete(createdWindowId); + + let sourceTabBrowser; + let createdTabBrowser; + + if (isSourceTab) { + sourceTabBrowser = browser; + createdTabBrowser = pairedMessage.browser; + } else { + sourceTabBrowser = pairedMessage.browser; + createdTabBrowser = browser; + } + + this.fire("onCreatedNavigationTarget", createdTabBrowser, {}, { + sourceTabBrowser, sourceWindowId, url, + }); + }, + onStateChange(browser, data) { let stateFlags = data.stateFlags; if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { @@ -357,7 +422,7 @@ const EVENTS = [ "onErrorOccurred", "onReferenceFragmentUpdated", "onHistoryStateUpdated", - // "onCreatedNavigationTarget", + "onCreatedNavigationTarget", ]; var WebNavigation = {}; diff --git a/toolkit/modules/addons/WebNavigationContent.js b/toolkit/modules/addons/WebNavigationContent.js index cea4a97b38..436ce26ba3 100644 --- a/toolkit/modules/addons/WebNavigationContent.js +++ b/toolkit/modules/addons/WebNavigationContent.js @@ -23,6 +23,55 @@ addMessageListener("Extension:DisableWebNavigation", () => { removeEventListener("DOMContentLoaded", loadListener); }); +var CreatedNavigationTargetListener = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + + init() { + Services.obs.addObserver(this, "webNavigation-createdNavigationTarget-from-js", false); + }, + uninit() { + Services.obs.removeObserver(this, "webNavigation-createdNavigationTarget-from-js"); + }, + + observe(subject, topic, data) { + if (!(subject instanceof Ci.nsIPropertyBag2)) { + return; + } + + let props = subject.QueryInterface(Ci.nsIPropertyBag2); + + const createdDocShell = props.getPropertyAsInterface("createdTabDocShell", Ci.nsIDocShell); + const sourceDocShell = props.getPropertyAsInterface("sourceTabDocShell", Ci.nsIDocShell); + + const isSourceTabDescendant = sourceDocShell.sameTypeRootTreeItem === docShell; + + if (docShell !== createdDocShell && docShell !== sourceDocShell && + !isSourceTabDescendant) { + // if the createdNavigationTarget is not related to this docShell + // (this docShell is not the newly created docShell, it is not the source docShell, + // and the source docShell is not a descendant of it) + // there is nothing to do here and return early. + return; + } + + const isSourceTab = docShell === sourceDocShell || isSourceTabDescendant; + const sourceWindowId = WebNavigationFrames.getWindowId(sourceDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow)); + const createdWindowId = WebNavigationFrames.getWindowId(createdDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow)); + + let url = "about:blank"; + if (props.hasKey("url")) { + url = props.getPropertyAsACString("url"); + } + + sendAsyncMessage("Extension:CreatedNavigationTarget", { + url, + sourceWindowId, + createdWindowId, + isSourceTab, + }); + }, +}; + var FormSubmitListener = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsIFormSubmitObserver, @@ -256,11 +305,13 @@ var WebProgressListener = { var disabled = false; WebProgressListener.init(); FormSubmitListener.init(); +CreatedNavigationTargetListener.init(); addEventListener("unload", () => { if (!disabled) { disabled = true; WebProgressListener.uninit(); FormSubmitListener.uninit(); + CreatedNavigationTargetListener.uninit(); } }); addMessageListener("Extension:DisableWebNavigation", () => { @@ -268,5 +319,6 @@ addMessageListener("Extension:DisableWebNavigation", () => { disabled = true; WebProgressListener.uninit(); FormSubmitListener.uninit(); + CreatedNavigationTargetListener.uninit(); } });