Bug 1190687 - [webext] webNavigation.onCreatedNavigationTarget on new windows/tabs from window.open.
MozReview-Commit-ID: KFtRP1eSI05
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -122,16 +122,17 @@ support-files =
[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_onCreatedNavigationTarget_window_open.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_window_open.js
@@ -0,0 +1,169 @@
+/* -*- 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.onMessage.addListener(({type, code}) => {
+ if (type === "execute-contentscript") {
+ browser.tabs.executeScript(sourceTabId, {code: code});
+ }
+ });
+
+ 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");
+}
+
+add_task(function* test_on_created_navigation_target_from_window_open() {
+ const tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, SOURCE_PAGE);
+
+ gBrowser.selectedTab = tab1;
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation", "tabs", "<all_urls>"],
+ },
+ });
+
+ yield extension.startup();
+
+ const expectedSourceTab = yield extension.awaitMessage("expectedSourceTab");
+
+ info("open an url in a new tab from a window.open call");
+
+ yield runTestCase({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-tab-from-window-open"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-window-open`,
+ },
+ });
+
+ info("open an url in a new window from a window.open call");
+
+ yield runTestCase({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-win-from-window-open", "_blank", "toolbar=0"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-win-from-window-open`,
+ },
+ });
+
+ yield BrowserTestUtils.removeTab(tab1);
+
+ yield extension.unload();
+});
+
+add_task(function* test_on_created_navigation_target_from_window_open_subframe() {
+ const tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, SOURCE_PAGE);
+
+ gBrowser.selectedTab = tab1;
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation", "tabs", "<all_urls>"],
+ },
+ });
+
+ yield extension.startup();
+
+ const expectedSourceTab = yield extension.awaitMessage("expectedSourceTab");
+
+ info("open an url in a new tab from subframe window.open call");
+
+ yield runTestCase({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `document.querySelector('iframe').contentWindow.open("${OPENED_PAGE}#new-tab-from-window-open-subframe"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-tab-from-window-open-subframe`,
+ },
+ });
+
+ info("open an url in a new window from subframe window.open call");
+
+ yield runTestCase({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `document.querySelector('iframe').contentWindow.open("${OPENED_PAGE}#new-win-from-window-open-subframe", "_blank", "toolbar=0"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-win-from-window-open-subframe`,
+ },
+ });
+
+ yield BrowserTestUtils.removeTab(tab1);
+
+ yield extension.unload();
+});
--- a/toolkit/components/windowwatcher/nsWindowWatcher.cpp
+++ b/toolkit/components/windowwatcher/nsWindowWatcher.cpp
@@ -15,16 +15,17 @@
#include "nsIAuthPrompt2.h"
#include "nsISimpleEnumerator.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsJSUtils.h"
#include "plstr.h"
#include "nsDocShell.h"
#include "nsGlobalWindow.h"
+#include "nsHashPropertyBag.h"
#include "nsIBaseWindow.h"
#include "nsIBrowserDOMWindow.h"
#include "nsIDocShell.h"
#include "nsIDocShellLoadInfo.h"
#include "nsIDocShellTreeItem.h"
#include "nsIDocShellTreeOwner.h"
#include "nsIDocumentLoader.h"
#include "nsIDocument.h"
@@ -1204,16 +1205,39 @@ nsWindowWatcher::OpenWindowInternal(mozI
obsSvc->NotifyObservers(*aResult, "toplevel-window-ready", nullptr);
}
}
// Before loading the URI we want to be 100% sure that we use the correct
// 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 (aCalledFromJS && parentDocShell && newDocShellItem) {
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+
+ if (obsSvc) {
+ RefPtr<nsHashPropertyBag> 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<nsIPropertyBag2*>(props),
+ "webNavigation-createdNavigationTarget-from-js", nullptr);
+ }
+ }
+
if (uriToLoad && aNavigate) {
newDocShell->LoadURI(
uriToLoad,
loadInfo,
windowIsNew ?
static_cast<uint32_t>(nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD) :
static_cast<uint32_t>(nsIWebNavigation::LOAD_FLAGS_NONE),
true);
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -24,44 +24,53 @@ const RECENT_DATA_THRESHOLD = 5 * 100000
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();
+
+ // Collect the pending created navigation target events that still have to
+ // pair the message received from the source tab to the one received from
+ // the new tab.
+ this.createdNavigationTargetByOuterWindowId = new Map();
+
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.addMessageListener("Extension:CreatedNavigationTarget", 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.removeMessageListener("Extension:CreatedNavigationTarget", this);
Services.mm.removeDelayedFrameScript("resource://gre/modules/WebNavigationContent.js");
Services.mm.broadcastAsyncMessage("Extension:DisableWebNavigation");
+
+ this.recentTabTransitionData = new WeakMap();
+ this.createdNavigationTargetByOuterWindowId.clear();
},
addListener(type, listener, filters) {
if (this.listeners.size == 0) {
this.init();
}
if (!this.listeners.has(type)) {
@@ -273,30 +282,65 @@ var Manager = {
case "Extension:DOMContentLoaded":
this.onLoad(target, data);
break;
case "Content:Click":
this.onContentClick(target, data);
break;
+
+ case "Extension:CreatedNavigationTarget":
+ this.onCreatedNavigationTarget(target, data);
+ break;
}
},
onContentClick(target, data) {
// We are interested only on clicks to links which are not "add to bookmark" commands
if (data.href && !data.bookmark) {
let ownerWin = target.ownerGlobal;
let where = ownerWin.whereToOpenLink(data);
if (where == "current") {
this.setRecentTabTransitionData({link: true});
}
}
},
+ onCreatedNavigationTarget(browser, data) {
+ const {isSourceTab, createdWindowId, sourceWindowId, url} = data;
+
+ // We are going to potentially received two message manager messages for a single
+ // onCreatedNavigationTarget event that is happening in the child process,
+ // we are going to use the generate uuid to pair them together.
+ const pairedMessage = this.createdNavigationTargetByOuterWindowId.get(createdWindowId);
+
+ if (!pairedMessage) {
+ this.createdNavigationTargetByOuterWindowId.set(createdWindowId, {browser, data});
+ return;
+ }
+
+ 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) {
let url = data.requestURL;
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
this.fire("onBeforeNavigate", browser, data, {url});
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
if (Components.isSuccessCode(data.status)) {
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -18,16 +18,65 @@ function loadListener(event) {
sendAsyncMessage("Extension:DOMContentLoaded", {windowId, parentWindowId, url});
}
addEventListener("DOMContentLoaded", loadListener);
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 = WebNavigationFrames.isDescendantDocShell(sourceDocShell, 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.getDocShellWindowId(sourceDocShell);
+ const createdWindowId = WebNavigationFrames.getDocShellWindowId(createdDocShell);
+
+ let url;
+ if (props.hasKey("url")) {
+ url = props.getPropertyAsACString("url");
+ }
+
+ sendAsyncMessage("Extension:CreatedNavigationTarget", {
+ url,
+ sourceWindowId,
+ createdWindowId,
+ isSourceTab,
+ });
+ },
+};
+
var FormSubmitListener = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsIFormSubmitObserver,
Ci.nsISupportsWeakReference]),
init() {
this.formSubmitWindows = new WeakSet();
Services.obs.addObserver(FormSubmitListener, "earlyformsubmit", false);
},
@@ -251,22 +300,25 @@ var WebProgressListener = {
Ci.nsIWebProgressListener2,
Ci.nsISupportsWeakReference,
]),
};
var disabled = false;
WebProgressListener.init();
FormSubmitListener.init();
+CreatedNavigationTargetListener.init();
addEventListener("unload", () => {
if (!disabled) {
disabled = true;
WebProgressListener.uninit();
FormSubmitListener.uninit();
+ CreatedNavigationTargetListener.uninit();
}
});
addMessageListener("Extension:DisableWebNavigation", () => {
if (!disabled) {
disabled = true;
WebProgressListener.uninit();
FormSubmitListener.uninit();
+ CreatedNavigationTargetListener.uninit();
}
});
--- a/toolkit/modules/addons/WebNavigationFrames.jsm
+++ b/toolkit/modules/addons/WebNavigationFrames.jsm
@@ -15,16 +15,27 @@ function getWindowId(window) {
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
}
function getParentWindowId(window) {
return getWindowId(window.parent);
}
+function getDocShellWindowId(docShell) {
+ if (!docShell) {
+ return undefined;
+ }
+
+ return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+}
+
/**
* Retrieve the DOMWindow associated to the docShell passed as parameter.
*
* @param {nsIDocShell} docShell - the docShell that we want to get the DOMWindow from.
* @returns {nsIDOMWindow} - the DOMWindow associated to the docShell.
*/
function docShellToWindow(docShell) {
return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -113,18 +124,24 @@ function findDocShell(frameId, rootDocSh
if (frameId == getFrameId(docShellToWindow(docShell))) {
return docShell;
}
}
return null;
}
+function isDescendantDocShell(targetDocShell, rootDocShell) {
+ return (rootDocShell === targetDocShell.sameTypeRootTreeItem
+ .QueryInterface(Ci.nsIDocShell));
+}
+
var WebNavigationFrames = {
iterateDocShellTree,
+ isDescendantDocShell,
findDocShell,
getFrame(docShell, frameId) {
let result = findDocShell(frameId, docShell);
if (result) {
return convertDocShellToFrameDetail(result);
}
@@ -134,9 +151,10 @@ var WebNavigationFrames = {
getFrameId,
getAllFrames(docShell) {
return Array.from(iterateDocShellTree(docShell), convertDocShellToFrameDetail);
},
getWindowId,
getParentWindowId,
+ getDocShellWindowId,
};