1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
9 BrowsingContextListener:
10 "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
11 EventPromise: "chrome://remote/content/shared/Sync.sys.mjs",
12 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
13 MobileTabBrowser: "chrome://remote/content/shared/MobileTabBrowser.sys.mjs",
15 "chrome://remote/content/shared/UserContextManager.sys.mjs",
18 class TabManagerClass {
24 // Maps browser's permanentKey to uuid: WeakMap.<Object, string>
25 this.#browserUniqueIds = new WeakMap();
27 // Maps browsing contexts to uuid: WeakMap.<BrowsingContext, string>.
28 // It's required as a fallback, since in the case when a context was discarded
29 // embedderElement is gone, and we cannot retrieve
30 // the context id from this.#browserUniqueIds.
31 this.#navigableIds = new WeakMap();
33 this.#contextListener = new lazy.BrowsingContextListener();
34 this.#contextListener.on("attached", this.#onContextAttached);
35 this.#contextListener.startListening();
37 this.browsers.forEach(browser => {
38 if (this.isValidCanonicalBrowsingContext(browser.browsingContext)) {
39 this.#navigableIds.set(
40 browser.browsingContext,
41 this.getIdForBrowsingContext(browser.browsingContext)
48 * Retrieve all the browser elements from tabs as contained in open windows.
50 * @returns {Array<XULBrowser>}
51 * All the found <xul:browser>s. Will return an empty array if
52 * no windows and tabs can be found.
57 for (const win of this.windows) {
58 for (const tab of this.getTabsForWindow(win)) {
59 const contentBrowser = this.getBrowserForTab(tab);
60 if (contentBrowser !== null) {
61 browsers.push(contentBrowser);
70 return Services.wm.getEnumerator(null);
74 * Array of unique browser ids (UUIDs) for all content browsers of all
77 * TODO: Similarly to getBrowserById, we should improve the performance of
78 * this getter in Bug 1750065.
80 * @returns {Array<string>}
81 * Array of UUIDs for all content browsers.
83 get allBrowserUniqueIds() {
84 const browserIds = [];
86 for (const win of this.windows) {
87 // Only return handles for browser windows
88 for (const tab of this.getTabsForWindow(win)) {
89 const contentBrowser = this.getBrowserForTab(tab);
90 const winId = this.getIdForBrowser(contentBrowser);
92 browserIds.push(winId);
101 * Get the <code><xul:browser></code> for the specified tab.
104 * The tab whose browser needs to be returned.
106 * @returns {XULBrowser}
107 * The linked browser for the tab or null if no browser can be found.
109 getBrowserForTab(tab) {
110 if (tab && "linkedBrowser" in tab) {
111 return tab.linkedBrowser;
118 * Return the tab browser for the specified chrome window.
120 * @param {ChromeWindow} win
121 * Window whose <code>tabbrowser</code> needs to be accessed.
124 * Tab browser or null if it's not a browser window.
127 if (lazy.AppInfo.isAndroid) {
128 return new lazy.MobileTabBrowser(win);
129 } else if (lazy.AppInfo.isFirefox) {
139 * @param {object} options
140 * @param {boolean=} options.focus
141 * Set to true if the new tab should be focused (selected). Defaults to
142 * false. `false` value is not properly supported on Android, additional
143 * focus of previously selected tab is required after initial navigation.
144 * @param {Tab=} options.referenceTab
145 * The reference tab after which the new tab will be added. If no
146 * reference tab is provided, the new tab will be added after all the
148 * @param {string=} options.userContextId
149 * A user context id from UserContextManager.
150 * @param {window=} options.window
151 * The window where the new tab will open. Defaults to Services.wm.getMostRecentWindow
152 * if no window is provided. Will be ignored if referenceTab is provided.
154 async addTab(options = {}) {
158 userContextId = null,
159 window = Services.wm.getMostRecentWindow(null),
163 if (referenceTab != null) {
164 // If a reference tab was specified, the window should be the window
165 // owning the reference tab.
166 window = this.getWindowForTab(referenceTab);
169 if (referenceTab != null) {
170 index = this.getTabsForWindow(window).indexOf(referenceTab) + 1;
173 const tabBrowser = this.getTabBrowser(window);
175 const tab = await tabBrowser.addTab("about:blank", {
177 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
178 userContextId: lazy.UserContextManager.getInternalIdById(userContextId),
182 await this.selectTab(tab);
189 * Retrieve the browser element corresponding to the provided unique id,
190 * previously generated via getIdForBrowser.
192 * TODO: To avoid creating strong references on browser elements and
193 * potentially leaking those elements, this method loops over all windows and
194 * all tabs. It should be replaced by a faster implementation in Bug 1750065.
197 * A browser unique id created by getIdForBrowser.
198 * @returns {XULBrowser}
199 * The <xul:browser> corresponding to the provided id. Will return null if
200 * no matching browser element is found.
203 for (const win of this.windows) {
204 for (const tab of this.getTabsForWindow(win)) {
205 const contentBrowser = this.getBrowserForTab(tab);
206 if (this.getIdForBrowser(contentBrowser) == id) {
207 return contentBrowser;
215 * Retrieve the browsing context corresponding to the provided unique id.
218 * A browsing context unique id (created by getIdForBrowsingContext).
219 * @returns {BrowsingContext=}
220 * The browsing context found for this id, null if none was found.
222 getBrowsingContextById(id) {
223 const browser = this.getBrowserById(id);
225 return browser.browsingContext;
228 return BrowsingContext.get(id);
232 * Retrieve the unique id for the given xul browser element. The id is a
233 * dynamically generated uuid associated with the permanentKey property of the
234 * given browser element. This method is preferable over getIdForBrowsingContext
235 * in case of working with browser element of a tab, since we can not guarantee
236 * that browsing context is attached to it.
238 * @param {XULBrowser} browserElement
239 * The <xul:browser> for which we want to retrieve the id.
240 * @returns {string} The unique id for this browser.
242 getIdForBrowser(browserElement) {
243 if (browserElement === null) {
247 const key = browserElement.permanentKey;
248 if (key === undefined) {
252 if (!this.#browserUniqueIds.has(key)) {
253 this.#browserUniqueIds.set(key, lazy.generateUUID());
255 return this.#browserUniqueIds.get(key);
259 * Retrieve the id of a Browsing Context.
261 * For a top-level browsing context a custom unique id will be returned.
263 * @param {BrowsingContext=} browsingContext
264 * The browsing context to get the id from.
267 * The id of the browsing context.
269 getIdForBrowsingContext(browsingContext) {
270 if (!browsingContext) {
274 if (!browsingContext.parent) {
275 // Top-level browsing contexts have their own custom unique id.
276 // If a context was discarded, embedderElement is already gone,
277 // so use navigable id instead.
278 return browsingContext.embedderElement
279 ? this.getIdForBrowser(browsingContext.embedderElement)
280 : this.#navigableIds.get(browsingContext);
283 return browsingContext.id.toString();
287 * Get the navigable for the given browsing context.
289 * Because Gecko doesn't support the Navigable concept in content
290 * scope the content browser could be used to uniquely identify
291 * top-level browsing contexts.
293 * @param {BrowsingContext} browsingContext
295 * @returns {BrowsingContext|XULBrowser} The navigable
297 * @throws {TypeError}
298 * If `browsingContext` is not a CanonicalBrowsingContext instance.
300 getNavigableForBrowsingContext(browsingContext) {
301 if (!this.isValidCanonicalBrowsingContext(browsingContext)) {
303 `Expected browsingContext to be a CanonicalBrowsingContext, got ${browsingContext}`
307 if (browsingContext.isContent && browsingContext.parent === null) {
308 return browsingContext.embedderElement;
311 return browsingContext;
316 for (const win of this.windows) {
317 // For browser windows count the tabs. Otherwise take the window itself.
318 const tabsLength = this.getTabsForWindow(win).length;
319 count += tabsLength ? tabsLength : 1;
325 * Retrieve the tab owning a Browsing Context.
327 * @param {BrowsingContext=} browsingContext
328 * The browsing context to get the tab from.
330 * @returns {Tab|null}
331 * The tab owning the Browsing Context.
333 getTabForBrowsingContext(browsingContext) {
334 const browser = browsingContext?.top.embedderElement;
339 const tabBrowser = this.getTabBrowser(browser.ownerGlobal);
340 return tabBrowser.getTabForBrowser(browser);
344 * Retrieve the list of tabs for a given window.
346 * @param {ChromeWindow} win
347 * Window whose <code>tabs</code> need to be returned.
349 * @returns {Array<Tab>}
350 * The list of tabs. Will return an empty list if tab browser is not available
351 * or tabs are undefined.
353 getTabsForWindow(win) {
354 const tabBrowser = this.getTabBrowser(win);
355 // For web-platform reftests a faked tabbrowser is used,
356 // which does not actually have tabs.
357 if (tabBrowser && tabBrowser.tabs) {
358 return tabBrowser.tabs;
363 getWindowForTab(tab) {
364 // `.linkedBrowser.ownerGlobal` works both with Firefox Desktop and Mobile.
365 // Other accessors (eg `.ownerGlobal` or `.browser.ownerGlobal`) fail on one
367 return tab.linkedBrowser.ownerGlobal;
371 * Check if the given argument is a valid canonical browsing context and was not
374 * @param {BrowsingContext} browsingContext
375 * The browsing context to check.
378 * True if the browsing context is valid, false otherwise.
380 isValidCanonicalBrowsingContext(browsingContext) {
382 CanonicalBrowsingContext.isInstance(browsingContext) &&
383 !browsingContext.isDiscarded
388 * Remove the given tab.
393 async removeTab(tab) {
398 const ownerWindow = this.getWindowForTab(tab);
399 const tabBrowser = this.getTabBrowser(ownerWindow);
400 await tabBrowser.removeTab(tab);
404 * Select the given tab.
410 * Promise that resolves when the given tab has been selected.
412 async selectTab(tab) {
414 return Promise.resolve();
417 const ownerWindow = this.getWindowForTab(tab);
418 const tabBrowser = this.getTabBrowser(ownerWindow);
420 if (tab === tabBrowser.selectedTab) {
421 return Promise.resolve();
424 const selected = new lazy.EventPromise(ownerWindow, "TabSelect");
425 tabBrowser.selectedTab = tab;
429 // Sometimes at that point window is not focused.
430 if (Services.focus.activeWindow != ownerWindow) {
431 const activated = new lazy.EventPromise(ownerWindow, "activate");
436 return Promise.resolve();
440 return lazy.AppInfo.isAndroid || lazy.AppInfo.isFirefox;
443 #onContextAttached = (eventName, data = {}) => {
444 const { browsingContext } = data;
445 if (this.isValidCanonicalBrowsingContext(browsingContext)) {
446 this.#navigableIds.set(
448 this.getIdForBrowsingContext(browsingContext)
454 // Expose a shared singleton.
455 export const TabManager = new TabManagerClass();