diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index d55882ba50286..9f924c1464bbf 100755 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -5180,8 +5180,8 @@ nsBrowserAccess.prototype = { _openURIInNewTab(aURI, aReferrer, aReferrerPolicy, aIsPrivate, aIsExternal, aForceNotRemote = false, aUserContextId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID, - aOpener = null, aTriggeringPrincipal = null, - aNextTabParentId = 0, aName = "") { + aOpenerWindow = null, aOpenerBrowser = null, + aTriggeringPrincipal = null, aNextTabParentId = 0, aName = "") { let win, needToFocusWin; // try the current window. if we're in a popup, fall back on the most recent browser window @@ -5213,7 +5213,8 @@ nsBrowserAccess.prototype = { fromExternal: aIsExternal, inBackground: loadInBackground, forceNotRemote: aForceNotRemote, - opener: aOpener, + opener: aOpenerWindow, + openerBrowser: aOpenerBrowser, nextTabParentId: aNextTabParentId, name: aName, }); @@ -5293,7 +5294,7 @@ nsBrowserAccess.prototype = { let browser = this._openURIInNewTab(aURI, referrer, referrerPolicy, isPrivate, isExternal, forceNotRemote, userContextId, - openerWindow, aTriggeringPrincipal); + openerWindow, null, aTriggeringPrincipal); if (browser) newWindow = browser.contentWindow; break; @@ -5335,7 +5336,7 @@ nsBrowserAccess.prototype = { aParams.referrerPolicy, aParams.isPrivate, isExternal, false, - userContextId, null, + userContextId, null, aParams.openerBrowser, aParams.triggeringPrincipal, aNextTabParentId, aName); if (browser) diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 358781c2d4347..83ff34fcad7d4 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -82,8 +82,8 @@ null - - null + + new WeakMap(); null @@ -1133,11 +1133,12 @@ if (!this._previewMode && !oldTab.selected) oldTab.owner = null; - if (this._lastRelatedTab) { - if (!this._lastRelatedTab.selected) - this._lastRelatedTab.owner = null; - this._lastRelatedTab = null; + let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); + if (lastRelatedTab) { + if (!lastRelatedTab.selected) + lastRelatedTab.owner = null; } + this._lastRelatedTabMap = new WeakMap(); var oldBrowser = this.mCurrentBrowser; @@ -1623,6 +1624,7 @@ var aSameProcessAsFrameLoader; var aOriginPrincipal; var aOpener; + var aOpenerBrowser; var aIsPrerendered; var aCreateLazyBrowser; var aNextTabParentId; @@ -1650,6 +1652,7 @@ aSameProcessAsFrameLoader = params.sameProcessAsFrameLoader; aOriginPrincipal = params.originPrincipal; aOpener = params.opener; + aOpenerBrowser = params.openerBrowser; aIsPrerendered = params.isPrerendered; aCreateLazyBrowser = params.createLazyBrowser; aNextTabParentId = params.nextTabParentId; @@ -1681,6 +1684,7 @@ originPrincipal: aOriginPrincipal, sameProcessAsFrameLoader: aSameProcessAsFrameLoader, opener: aOpener, + openerBrowser: aOpenerBrowser, isPrerendered: aIsPrerendered, nextTabParentId: aNextTabParentId, focusUrlBar: aFocusUrlBar, @@ -2117,11 +2121,11 @@ b.setAttribute("remote", "true"); } - if (aParams.opener) { + if (aParams.openerWindow) { if (aParams.remoteType) { throw new Error("Cannot set opener window on a remote browser!"); } - b.QueryInterface(Ci.nsIFrameLoaderOwner).presetOpenerWindow(aParams.opener); + b.QueryInterface(Ci.nsIFrameLoaderOwner).presetOpenerWindow(aParams.openerWindow); } if (!aParams.isPreloadBrowser && this.hasAttribute("autocompletepopup")) { @@ -2138,6 +2142,9 @@ b.setAttribute("autoscrollpopup", this._autoScrollPopup.id); if (aParams.nextTabParentId) { + if (!aParams.remoteType) { + throw new Error("Cannot have nextTabParentId without a remoteType"); + } // Gecko is going to read this attribute and use it. b.setAttribute("nextTabParentId", aParams.nextTabParentId.toString()); } @@ -2424,6 +2431,7 @@ var aOriginPrincipal; var aDisallowInheritPrincipal; var aOpener; + var aOpenerBrowser; var aIsPrerendered; var aCreateLazyBrowser; var aSkipBackgroundNotify; @@ -2455,6 +2463,7 @@ aOriginPrincipal = params.originPrincipal; aDisallowInheritPrincipal = params.disallowInheritPrincipal; aOpener = params.opener; + aOpenerBrowser = params.openerBrowser; aIsPrerendered = params.isPrerendered; aCreateLazyBrowser = params.createLazyBrowser; aSkipBackgroundNotify = params.skipBackgroundNotify; @@ -2468,8 +2477,28 @@ if (this.mCurrentTab.owner) this.mCurrentTab.owner = null; + // Find the tab that opened this one, if any. This is used for + // determining positioning, and inherited attributes such as the + // user context ID. + // + // If we have a browser opener (which is usually the browser + // element from a remote window.open() call), use that. + // + // Otherwise, if the tab is related to the current tab (e.g., + // because it was opened by a link click), use the selected tab as + // the owner. If aReferrerURI is set, and we don't have an + // explicit relatedToCurrent arg, we assume that the tab is + // related to the current tab, since aReferrerURI is null or + // undefined if the tab is opened from an external application or + // bookmark (i.e. somewhere other than an existing tab). + let relatedToCurrent = aRelatedToCurrent == null ? !!aReferrerURI : aRelatedToCurrent; + let openerTab = ((aOpenerBrowser && this.getTabForBrowser(aOpenerBrowser)) || + (relatedToCurrent && this.selectedTab)); + var t = document.createElementNS(NS_XUL, "tab"); + t.openerTab = openerTab; + aURI = aURI || "about:blank"; let aURIObject = null; try { @@ -2499,9 +2528,8 @@ // Related tab inherits current tab's user context unless a different // usercontextid is specified - if (aUserContextId == null && - (aRelatedToCurrent == null ? aReferrerURI : aRelatedToCurrent)) { - aUserContextId = this.mCurrentTab.getAttribute("usercontextid") || 0; + if (aUserContextId == null && openerTab) { + aUserContextId = openerTab.getAttribute("usercontextid") || 0; } if (aUserContextId) { @@ -2550,6 +2578,12 @@ this.tabContainer.updateVisibility(); + // If we don't have a preferred remote type, and we have a remote + // opener, use the opener's remote type. + if (!aPreferredRemoteType && aOpenerBrowser) { + aPreferredRemoteType = aOpenerBrowser.remoteType; + } + // If URI is about:blank and we don't have a preferred remote type, // then we need to use the referrer, if we have one, to get the // correct remote type for the new tab. @@ -2586,7 +2620,7 @@ uriIsAboutBlank, userContextId: aUserContextId, sameProcessAsFrameLoader: aSameProcessAsFrameLoader, - opener: aOpener, + openerWindow: aOpener, isPrerendered: aIsPrerendered, nextTabParentId: aNextTabParentId, name: aName }); @@ -2668,21 +2702,19 @@ } } - // Check if we're opening a tab related to the current tab and - // move it to after the current tab. - // aReferrerURI is null or undefined if the tab is opened from - // an external application or bookmark, i.e. somewhere other - // than the current tab. - if ((aRelatedToCurrent == null ? aReferrerURI : aRelatedToCurrent) && + // If we're opening a tab related to the an existing tab, move it + // to a position after that tab. + if (openerTab && Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) { - let newTabPos = (this._lastRelatedTab || - this.selectedTab)._tPos + 1; - if (this._lastRelatedTab) - this._lastRelatedTab.owner = null; + + let lastRelatedTab = this._lastRelatedTabMap.get(openerTab); + let newTabPos = (lastRelatedTab || openerTab)._tPos + 1; + if (lastRelatedTab) + lastRelatedTab.owner = null; else - t.owner = this.selectedTab; - this.moveTabTo(t, newTabPos); - this._lastRelatedTab = t; + t.owner = openerTab; + this.moveTabTo(t, newTabPos, true); + this._lastRelatedTabMap.set(openerTab, t); } if (animate) { @@ -3058,7 +3090,7 @@ aNewTab = false; } - this._lastRelatedTab = null; + this._lastRelatedTabMap = new WeakMap(); // update the UI early for responsiveness aTab.collapsed = true; @@ -3708,6 +3740,7 @@ + . + */ + setOpener(tab, openerTab) { + if (tab.ownerDocument !== openerTab.ownerDocument) { + throw new Error("Tab must be in the same window as its opener"); + } + tab.openerTab = openerTab; + } + /** * @param {Event} event * The DOM Event to handle. @@ -500,6 +514,14 @@ class Tab extends TabBase { return getCookieStoreIdForTab(this, this.nativeTab); } + get openerTabId() { + let opener = this.nativeTab.openerTab; + if (opener && opener.parentNode && opener.ownerDocument == this.nativeTab.ownerDocument) { + return tabTracker.getId(opener); + } + return null; + } + get height() { return this.frameLoader.lazyHeight; } diff --git a/browser/components/extensions/schemas/tabs.json b/browser/components/extensions/schemas/tabs.json index f57cb07a74a8b..bf33481066706 100644 --- a/browser/components/extensions/schemas/tabs.json +++ b/browser/components/extensions/schemas/tabs.json @@ -59,7 +59,7 @@ "id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."}, "index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."}, "windowId": {"type": "integer", "optional": true, "minimum": 0, "description": "The ID of the window the tab is contained within."}, - "openerTabId": {"unsupported": true, "type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."}, + "openerTabId": {"type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."}, "selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true}, "highlighted": {"type": "boolean", "description": "Whether the tab is highlighted. Works as an alias of active"}, "active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"}, @@ -485,7 +485,6 @@ "description": "Whether the tab should be pinned. Defaults to false" }, "openerTabId": { - "unsupported": true, "type": "integer", "minimum": 0, "optional": true, @@ -624,6 +623,12 @@ "type": "string", "optional": true, "description": "The CookieStoreId used for the tab." + }, + "openerTabId": { + "type": "integer", + "minimum": 0, + "optional": true, + "description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as this tab." } } }, @@ -733,7 +738,6 @@ "description": "Whether the tab should be muted." }, "openerTabId": { - "unsupported": true, "type": "integer", "minimum": 0, "optional": true, diff --git a/browser/components/extensions/test/browser/browser-common.ini b/browser/components/extensions/test/browser/browser-common.ini index 18030c0cbd3f2..50075b7dc933d 100644 --- a/browser/components/extensions/test/browser/browser-common.ini +++ b/browser/components/extensions/test/browser/browser-common.ini @@ -136,6 +136,7 @@ skip-if = debug || asan # Bug 1354681 [browser_ext_tabs_move_window_pinned.js] [browser_ext_tabs_onHighlighted.js] [browser_ext_tabs_onUpdated.js] +[browser_ext_tabs_opener.js] [browser_ext_tabs_printPreview.js] [browser_ext_tabs_query.js] [browser_ext_tabs_reload.js] diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_opener.js b/browser/components/extensions/test/browser/browser_ext_tabs_opener.js new file mode 100644 index 0000000000000..e1049358873ee --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_tabs_opener.js @@ -0,0 +1,78 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank?1"); + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank?2"); + + gBrowser.selectedTab = tab1; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background() { + let activeTab; + let tabId; + let tabIds; + browser.tabs.query({lastFocusedWindow: true}).then(tabs => { + browser.test.assertEq(3, tabs.length, "We have three tabs"); + + browser.test.assertTrue(tabs[1].active, "Tab 1 is active"); + activeTab = tabs[1]; + + tabIds = tabs.map(tab => tab.id); + + return browser.tabs.create({openerTabId: activeTab.id, active: false}); + }).then(tab => { + browser.test.assertEq(activeTab.id, tab.openerTabId, "Tab opener ID is correct"); + browser.test.assertEq(activeTab.index + 1, tab.index, "Tab was inserted after the related current tab"); + + tabId = tab.id; + return browser.tabs.get(tabId); + }).then(tab => { + browser.test.assertEq(activeTab.id, tab.openerTabId, "Tab opener ID is still correct"); + + return browser.tabs.update(tabId, {openerTabId: tabIds[0]}); + }).then(tab => { + browser.test.assertEq(tabIds[0], tab.openerTabId, "Updated tab opener ID is correct"); + + return browser.tabs.get(tabId); + }).then(tab => { + browser.test.assertEq(tabIds[0], tab.openerTabId, "Updated tab opener ID is still correct"); + + return browser.tabs.create({openerTabId: tabId, active: false}); + }).then(tab => { + browser.test.assertEq(tabId, tab.openerTabId, "New tab opener ID is correct"); + browser.test.assertEq(tabIds.length, tab.index, "New tab was not inserted after the unrelated current tab"); + + let promise = browser.tabs.remove(tabId); + + tabId = tab.id; + return promise; + }).then(() => { + return browser.tabs.get(tabId); + }).then(tab => { + browser.test.assertEq(undefined, tab.openerTabId, "Tab opener ID was cleared after opener tab closed"); + + return browser.tabs.remove(tabId); + }).then(() => { + browser.test.notifyPass("tab-opener"); + }).catch(e => { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("tab-opener"); + }); + }, + }); + + await extension.startup(); + + await extension.awaitFinish("tab-opener"); + + await extension.unload(); + + await BrowserTestUtils.removeTab(tab1); + await BrowserTestUtils.removeTab(tab2); +}); diff --git a/dom/base/nsOpenURIInFrameParams.cpp b/dom/base/nsOpenURIInFrameParams.cpp index 0297caa4f1fa0..5d6696880a99f 100644 --- a/dom/base/nsOpenURIInFrameParams.cpp +++ b/dom/base/nsOpenURIInFrameParams.cpp @@ -8,10 +8,20 @@ #include "mozilla/BasePrincipal.h" #include "mozilla/dom/ToJSValue.h" -NS_IMPL_ISUPPORTS(nsOpenURIInFrameParams, nsIOpenURIInFrameParams) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsOpenURIInFrameParams) + NS_INTERFACE_MAP_ENTRY(nsIOpenURIInFrameParams) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END -nsOpenURIInFrameParams::nsOpenURIInFrameParams(const mozilla::OriginAttributes& aOriginAttributes) +NS_IMPL_CYCLE_COLLECTION(nsOpenURIInFrameParams, mOpenerBrowser) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsOpenURIInFrameParams) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsOpenURIInFrameParams) + +nsOpenURIInFrameParams::nsOpenURIInFrameParams(const mozilla::OriginAttributes& aOriginAttributes, + nsIFrameLoaderOwner* aOpener) : mOpenerOriginAttributes(aOriginAttributes) + , mOpenerBrowser(aOpener) { } @@ -55,6 +65,14 @@ nsOpenURIInFrameParams::SetTriggeringPrincipal(nsIPrincipal* aTriggeringPrincipa return NS_OK; } +NS_IMETHODIMP +nsOpenURIInFrameParams::GetOpenerBrowser(nsIFrameLoaderOwner** aOpenerBrowser) +{ + nsCOMPtr owner = mOpenerBrowser; + owner.forget(aOpenerBrowser); + return NS_OK; +} + NS_IMETHODIMP nsOpenURIInFrameParams::GetOpenerOriginAttributes(JSContext* aCx, JS::MutableHandle aValue) diff --git a/dom/base/nsOpenURIInFrameParams.h b/dom/base/nsOpenURIInFrameParams.h index f00ee3d47aa11..4eefec623c805 100644 --- a/dom/base/nsOpenURIInFrameParams.h +++ b/dom/base/nsOpenURIInFrameParams.h @@ -5,7 +5,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/BasePrincipal.h" +#include "nsCycleCollectionParticipant.h" #include "nsIBrowserDOMWindow.h" +#include "nsIFrameLoader.h" #include "nsString.h" namespace mozilla { @@ -15,15 +17,18 @@ class OriginAttributes; class nsOpenURIInFrameParams final : public nsIOpenURIInFrameParams { public: - NS_DECL_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(nsOpenURIInFrameParams) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_NSIOPENURIINFRAMEPARAMS - explicit nsOpenURIInFrameParams(const mozilla::OriginAttributes& aOriginAttributes); + explicit nsOpenURIInFrameParams(const mozilla::OriginAttributes& aOriginAttributes, + nsIFrameLoaderOwner* aOpener); private: ~nsOpenURIInFrameParams(); mozilla::OriginAttributes mOpenerOriginAttributes; + nsCOMPtr mOpenerBrowser; nsString mReferrer; nsCOMPtr mTriggeringPrincipal; }; diff --git a/dom/interfaces/base/nsIBrowserDOMWindow.idl b/dom/interfaces/base/nsIBrowserDOMWindow.idl index 29142f8a17d61..1dc15586e70c4 100644 --- a/dom/interfaces/base/nsIBrowserDOMWindow.idl +++ b/dom/interfaces/base/nsIBrowserDOMWindow.idl @@ -18,6 +18,10 @@ interface nsIOpenURIInFrameParams : nsISupports readonly attribute boolean isPrivate; attribute nsIPrincipal triggeringPrincipal; + // The browser or frame element in the parent process which holds the + // opener window in the content process. May be null. + readonly attribute nsIFrameLoaderOwner openerBrowser; + [implicit_jscontext] readonly attribute jsval openerOriginAttributes; }; diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index 51676ed3f0bbe..88466d9483030 100644 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -1146,6 +1146,12 @@ ContentParent::CreateBrowser(const TabContext& aContext, return nullptr; } + nsAutoString remoteType; + if (!aFrameElement->GetAttr(kNameSpaceID_None, nsGkAtoms::RemoteType, + remoteType)) { + remoteType.AssignLiteral(DEFAULT_REMOTE_TYPE); + } + if (aNextTabParentId) { if (TabParent* parent = sNextTabParents.GetAndRemove(aNextTabParentId).valueOr(nullptr)) { @@ -1166,12 +1172,6 @@ ContentParent::CreateBrowser(const TabContext& aContext, openerTabId = TabParent::GetTabIdFrom(docShell); } - nsAutoString remoteType; - if (!aFrameElement->GetAttr(kNameSpaceID_None, nsGkAtoms::RemoteType, - remoteType)) { - remoteType.AssignLiteral(DEFAULT_REMOTE_TYPE); - } - RefPtr constructorSender; if (isInContentProcess) { MOZ_ASSERT(aContext.IsMozBrowserElement() || aContext.IsJSPlugin()); @@ -4531,8 +4531,10 @@ ContentParent::CommonCreateWindow(PBrowserParent* aThisTab, return IPC_OK(); } + nsCOMPtr opener = do_QueryInterface(frame); + nsCOMPtr params = - new nsOpenURIInFrameParams(openerOriginAttributes); + new nsOpenURIInFrameParams(openerOriginAttributes, opener); params->SetReferrer(NS_ConvertUTF8toUTF16(aBaseURI)); MOZ_ASSERT(aTriggeringPrincipal, "need a valid triggeringPrincipal"); params->SetTriggeringPrincipal(aTriggeringPrincipal); diff --git a/toolkit/components/extensions/ExtensionTabs.jsm b/toolkit/components/extensions/ExtensionTabs.jsm index f2d55266a52a1..4f139991840fa 100644 --- a/toolkit/components/extensions/ExtensionTabs.jsm +++ b/toolkit/components/extensions/ExtensionTabs.jsm @@ -315,6 +315,15 @@ class TabBase { throw new Error("Not implemented"); } + /** + * @property {integer} openerTabId + * Returns the ID of the tab which opened this one. + * @readonly + */ + get openerTabId() { + return null; + } + /** * @property {integer} height * Returns the pixel height of the visible area of the tab. @@ -451,9 +460,9 @@ class TabBase { * True if the tab matches the query. */ matches(queryInfo) { - const PROPS = ["active", "audible", "cookieStoreId", "highlighted", "index", "pinned", "status", "title"]; + const PROPS = ["active", "audible", "cookieStoreId", "highlighted", "index", "openerTabId", "pinned", "status", "title"]; - if (PROPS.some(prop => queryInfo[prop] !== null && queryInfo[prop] !== this[prop])) { + if (PROPS.some(prop => queryInfo[prop] != null && queryInfo[prop] !== this[prop])) { return false; } @@ -504,6 +513,11 @@ class TabBase { result.height = fallbackTab.height; } + let opener = this.openerTabId; + if (opener) { + result.openerTabId = opener; + } + if (this.extension.hasPermission("cookies")) { result.cookieStoreId = this.cookieStoreId; } diff --git a/toolkit/components/extensions/test/mochitest/mochitest-common.ini b/toolkit/components/extensions/test/mochitest/mochitest-common.ini index 7779a9ed15d74..ba2adbb71b40e 100644 --- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini +++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini @@ -78,6 +78,7 @@ skip-if = os == 'android' # bug 1369440 [test_ext_generate.html] [test_ext_geolocation.html] skip-if = os == 'android' # Android support Bug 1336194 +[test_ext_new_tab_processType.html] [test_ext_notifications.html] [test_ext_permission_xhr.html] [test_ext_proxy.html] diff --git a/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html b/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html new file mode 100644 index 0000000000000..2e2da2cbf8af5 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html @@ -0,0 +1,89 @@ + + + + Test for opening links in new tabs from extension frames + + + + + + + + + + + +