From a913eed2014ea2e5f1a8a0d1405dcc7aa54836a4 Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Tue, 1 Jan 2019 12:59:09 -0500 Subject: [PATCH 1/4] Port 'Bug 1357589: Part 1 - Use the correct default remote type for content-created tabs/windows. r=mconley r=mystor When creating new windows/tabs from the content process, we currently choose the default remote type based on the referrer URI, rather than the actual remote type of the opener. While this generally works, it fails when opening windows from URLs (particularly in iframes) which are opened in process types other than their default process type, since the initial about:blank load gets redirected to a new process before the child process has a chance to load its actual target URI. Using the actual remote type of the child opening the window fixes this problem for URLs which are capable of being loaded into the child's process type.' --- browser/base/content/browser.js | 11 ++++++----- browser/base/content/tabbrowser.xml | 20 ++++++++++++++++--- dom/base/nsOpenURIInFrameParams.cpp | 22 +++++++++++++++++++-- dom/base/nsOpenURIInFrameParams.h | 9 +++++++-- dom/interfaces/base/nsIBrowserDOMWindow.idl | 4 ++++ dom/ipc/ContentParent.cpp | 16 ++++++++------- 6 files changed, 63 insertions(+), 19 deletions(-) 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..cdcffb122766e 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1623,6 +1623,7 @@ var aSameProcessAsFrameLoader; var aOriginPrincipal; var aOpener; + var aOpenerBrowser; var aIsPrerendered; var aCreateLazyBrowser; var aNextTabParentId; @@ -1650,6 +1651,7 @@ aSameProcessAsFrameLoader = params.sameProcessAsFrameLoader; aOriginPrincipal = params.originPrincipal; aOpener = params.opener; + aOpenerBrowser = params.openerBrowser; aIsPrerendered = params.isPrerendered; aCreateLazyBrowser = params.createLazyBrowser; aNextTabParentId = params.nextTabParentId; @@ -1681,6 +1683,7 @@ originPrincipal: aOriginPrincipal, sameProcessAsFrameLoader: aSameProcessAsFrameLoader, opener: aOpener, + openerBrowser: aOpenerBrowser, isPrerendered: aIsPrerendered, nextTabParentId: aNextTabParentId, focusUrlBar: aFocusUrlBar, @@ -2117,11 +2120,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 +2141,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 +2430,7 @@ var aOriginPrincipal; var aDisallowInheritPrincipal; var aOpener; + var aOpenerBrowser; var aIsPrerendered; var aCreateLazyBrowser; var aSkipBackgroundNotify; @@ -2455,6 +2462,7 @@ aOriginPrincipal = params.originPrincipal; aDisallowInheritPrincipal = params.disallowInheritPrincipal; aOpener = params.opener; + aOpenerBrowser = params.openerBrowser; aIsPrerendered = params.isPrerendered; aCreateLazyBrowser = params.createLazyBrowser; aSkipBackgroundNotify = params.skipBackgroundNotify; @@ -2550,6 +2558,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 +2600,7 @@ uriIsAboutBlank, userContextId: aUserContextId, sameProcessAsFrameLoader: aSameProcessAsFrameLoader, - opener: aOpener, + openerWindow: aOpener, isPrerendered: aIsPrerendered, nextTabParentId: aNextTabParentId, name: aName }); 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); From 618728572a8d502488ba43ae5865792def7c0105 Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Tue, 1 Jan 2019 13:05:05 -0500 Subject: [PATCH 2/4] Port 'Bug 1357589: Part 2 - Test that opening web URLs from extension iframes works correctly.' --- .../test/mochitest/mochitest-common.ini | 1 + .../test_ext_new_tab_processType.html | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_new_tab_processType.html 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 + + + + + + + + + + + + From fcc9a024aa1745f08d3673e85102fb7593aa551c Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Tue, 1 Jan 2019 13:06:39 -0500 Subject: [PATCH 3/4] Port 'Bug 1238314: Part 1 - Track opener tabs separately from owner and selected tab.' --- browser/base/content/tabbrowser.xml | 69 +++++++++++++++++++---------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index cdcffb122766e..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; @@ -2476,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 { @@ -2507,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) { @@ -2682,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) { @@ -3072,7 +3090,7 @@ aNewTab = false; } - this._lastRelatedTab = null; + this._lastRelatedTabMap = new WeakMap(); // update the UI early for responsiveness aTab.collapsed = true; @@ -3722,6 +3740,7 @@ + Date: Tue, 1 Jan 2019 13:11:10 -0500 Subject: [PATCH 4/4] Port 'Bug 1238314: Part 2 - Implement browser.tabs openerTabId functionality.' --- browser/components/extensions/ext-tabs.js | 17 +++- browser/components/extensions/ext-utils.js | 22 ++++++ .../components/extensions/schemas/tabs.json | 10 ++- .../test/browser/browser-common.ini | 1 + .../test/browser/browser_ext_tabs_opener.js | 78 +++++++++++++++++++ .../components/extensions/ExtensionTabs.jsm | 18 ++++- 6 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 browser/components/extensions/test/browser/browser_ext_tabs_opener.js diff --git a/browser/components/extensions/ext-tabs.js b/browser/components/extensions/ext-tabs.js index 82d50f5620a67..012eb505dff67 100644 --- a/browser/components/extensions/ext-tabs.js +++ b/browser/components/extensions/ext-tabs.js @@ -367,6 +367,15 @@ this.tabs = class extends ExtensionAPI { tabListener.initTabReady(); let currentTab = window.gBrowser.selectedTab; + + if (createProperties.openerTabId !== null) { + options.ownerTab = tabTracker.getTab(createProperties.openerTabId); + options.openerBrowser = options.ownerTab.linkedBrowser; + if (options.ownerTab.ownerGlobal !== window) { + return Promise.reject({message: "Opener tab must be in the same window as the tab being created"}); + } + } + let nativeTab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options); let active = true; @@ -450,7 +459,13 @@ this.tabs = class extends ExtensionAPI { tabbrowser.unpinTab(nativeTab); } } - // FIXME: highlighted/selected, openerTabId + if (updateProperties.openerTabId !== null) { + let opener = tabTracker.getTab(updateProperties.openerTabId); + if (opener.ownerDocument !== nativeTab.ownerDocument) { + return Promise.reject({message: "Opener tab must be in the same window as the tab being updated"}); + } + tabTracker.setOpener(nativeTab, opener); + } return tabManager.convert(nativeTab); }, diff --git a/browser/components/extensions/ext-utils.js b/browser/components/extensions/ext-utils.js index 5a6345ffc66d1..b61215901e7ca 100644 --- a/browser/components/extensions/ext-utils.js +++ b/browser/components/extensions/ext-utils.js @@ -219,6 +219,20 @@ class TabTracker extends TabTrackerBase { throw new ExtensionError(`Invalid tab ID: ${tabId}`); } + /** + * Sets the opener of `tab` to the ID `openerTab`. Both tabs must be in the + * same window, or this function will throw a type error. + * + * @param {Element} tab The tab for which to set the owner. + * @param {Element} openerTab The opener of . + */ + 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/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; }