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 * Retrieve all the browser tabs in open windows.
72 * @returns {Array<Tab>}
73 * All the open browser tabs. Will return an empty list if tab browser
74 * is not available or tabs are undefined.
79 for (const win of this.windows) {
80 tabs.push(...this.getTabsForWindow(win));
87 * Retrieve all the open windows.
89 * @returns {Array<Window>}
90 * All the open windows. Will return an empty list if no window is open.
95 for (const win of Services.wm.getEnumerator(null)) {
106 * Array of unique browser ids (UUIDs) for all content browsers of all
109 * TODO: Similarly to getBrowserById, we should improve the performance of
110 * this getter in Bug 1750065.
112 * @returns {Array<string>}
113 * Array of UUIDs for all content browsers.
115 get allBrowserUniqueIds() {
116 const browserIds = [];
118 for (const win of this.windows) {
119 // Only return handles for browser windows
120 for (const tab of this.getTabsForWindow(win)) {
121 const contentBrowser = this.getBrowserForTab(tab);
122 const winId = this.getIdForBrowser(contentBrowser);
123 if (winId !== null) {
124 browserIds.push(winId);
133 * Get the <code><xul:browser></code> for the specified tab.
136 * The tab whose browser needs to be returned.
138 * @returns {XULBrowser}
139 * The linked browser for the tab or null if no browser can be found.
141 getBrowserForTab(tab) {
142 if (tab && "linkedBrowser" in tab) {
143 return tab.linkedBrowser;
150 * Return the tab browser for the specified chrome window.
152 * @param {ChromeWindow} win
153 * Window whose <code>tabbrowser</code> needs to be accessed.
156 * Tab browser or null if it's not a browser window.
159 if (lazy.AppInfo.isAndroid) {
160 return new lazy.MobileTabBrowser(win);
161 } else if (lazy.AppInfo.isFirefox) {
171 * @param {object} options
172 * @param {boolean=} options.focus
173 * Set to true if the new tab should be focused (selected). Defaults to
174 * false. `false` value is not properly supported on Android, additional
175 * focus of previously selected tab is required after initial navigation.
176 * @param {Tab=} options.referenceTab
177 * The reference tab after which the new tab will be added. If no
178 * reference tab is provided, the new tab will be added after all the
180 * @param {string=} options.userContextId
181 * A user context id from UserContextManager.
182 * @param {window=} options.window
183 * The window where the new tab will open. Defaults to Services.wm.getMostRecentWindow
184 * if no window is provided. Will be ignored if referenceTab is provided.
186 async addTab(options = {}) {
190 userContextId = null,
191 window = Services.wm.getMostRecentWindow(null),
195 if (referenceTab != null) {
196 // If a reference tab was specified, the window should be the window
197 // owning the reference tab.
198 window = this.getWindowForTab(referenceTab);
201 if (referenceTab != null) {
202 index = this.getTabsForWindow(window).indexOf(referenceTab) + 1;
205 const tabBrowser = this.getTabBrowser(window);
207 const tab = await tabBrowser.addTab("about:blank", {
209 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
210 userContextId: lazy.UserContextManager.getInternalIdById(userContextId),
214 await this.selectTab(tab);
221 * Retrieve the browser element corresponding to the provided unique id,
222 * previously generated via getIdForBrowser.
224 * TODO: To avoid creating strong references on browser elements and
225 * potentially leaking those elements, this method loops over all windows and
226 * all tabs. It should be replaced by a faster implementation in Bug 1750065.
229 * A browser unique id created by getIdForBrowser.
230 * @returns {XULBrowser}
231 * The <xul:browser> corresponding to the provided id. Will return null if
232 * no matching browser element is found.
235 for (const win of this.windows) {
236 for (const tab of this.getTabsForWindow(win)) {
237 const contentBrowser = this.getBrowserForTab(tab);
238 if (this.getIdForBrowser(contentBrowser) == id) {
239 return contentBrowser;
247 * Retrieve the browsing context corresponding to the provided unique id.
250 * A browsing context unique id (created by getIdForBrowsingContext).
251 * @returns {BrowsingContext=}
252 * The browsing context found for this id, null if none was found.
254 getBrowsingContextById(id) {
255 const browser = this.getBrowserById(id);
257 return browser.browsingContext;
260 return BrowsingContext.get(id);
264 * Retrieve the unique id for the given xul browser element. The id is a
265 * dynamically generated uuid associated with the permanentKey property of the
266 * given browser element. This method is preferable over getIdForBrowsingContext
267 * in case of working with browser element of a tab, since we can not guarantee
268 * that browsing context is attached to it.
270 * @param {XULBrowser} browserElement
271 * The <xul:browser> for which we want to retrieve the id.
272 * @returns {string} The unique id for this browser.
274 getIdForBrowser(browserElement) {
275 if (browserElement === null) {
279 const key = browserElement.permanentKey;
280 if (key === undefined) {
284 if (!this.#browserUniqueIds.has(key)) {
285 this.#browserUniqueIds.set(key, lazy.generateUUID());
287 return this.#browserUniqueIds.get(key);
291 * Retrieve the id of a Browsing Context.
293 * For a top-level browsing context a custom unique id will be returned.
295 * @param {BrowsingContext=} browsingContext
296 * The browsing context to get the id from.
299 * The id of the browsing context.
301 getIdForBrowsingContext(browsingContext) {
302 if (!browsingContext) {
306 if (!browsingContext.parent) {
307 // Top-level browsing contexts have their own custom unique id.
308 // If a context was discarded, embedderElement is already gone,
309 // so use navigable id instead.
310 return browsingContext.embedderElement
311 ? this.getIdForBrowser(browsingContext.embedderElement)
312 : this.#navigableIds.get(browsingContext);
315 return browsingContext.id.toString();
319 * Get the navigable for the given browsing context.
321 * Because Gecko doesn't support the Navigable concept in content
322 * scope the content browser could be used to uniquely identify
323 * top-level browsing contexts.
325 * @param {BrowsingContext} browsingContext
327 * @returns {BrowsingContext|XULBrowser} The navigable
329 * @throws {TypeError}
330 * If `browsingContext` is not a CanonicalBrowsingContext instance.
332 getNavigableForBrowsingContext(browsingContext) {
333 if (!this.isValidCanonicalBrowsingContext(browsingContext)) {
335 `Expected browsingContext to be a CanonicalBrowsingContext, got ${browsingContext}`
339 if (browsingContext.isContent && browsingContext.parent === null) {
340 return browsingContext.embedderElement;
343 return browsingContext;
348 for (const win of this.windows) {
349 // For browser windows count the tabs. Otherwise take the window itself.
350 const tabsLength = this.getTabsForWindow(win).length;
351 count += tabsLength ? tabsLength : 1;
357 * Retrieve the tab owning a Browsing Context.
359 * @param {BrowsingContext=} browsingContext
360 * The browsing context to get the tab from.
362 * @returns {Tab|null}
363 * The tab owning the Browsing Context.
365 getTabForBrowsingContext(browsingContext) {
366 const browser = browsingContext?.top.embedderElement;
371 const tabBrowser = this.getTabBrowser(browser.ownerGlobal);
372 return tabBrowser.getTabForBrowser(browser);
376 * Retrieve the list of tabs for a given window.
378 * @param {ChromeWindow} win
379 * Window whose <code>tabs</code> need to be returned.
381 * @returns {Array<Tab>}
382 * The list of tabs. Will return an empty list if tab browser is not available
383 * or tabs are undefined.
385 getTabsForWindow(win) {
386 const tabBrowser = this.getTabBrowser(win);
387 // For web-platform reftests a faked tabbrowser is used,
388 // which does not actually have tabs.
389 if (tabBrowser && tabBrowser.tabs) {
390 return tabBrowser.tabs;
395 getWindowForTab(tab) {
396 // `.linkedBrowser.ownerGlobal` works both with Firefox Desktop and Mobile.
397 // Other accessors (eg `.ownerGlobal` or `.browser.ownerGlobal`) fail on one
399 return tab.linkedBrowser.ownerGlobal;
403 * Check if the given argument is a valid canonical browsing context and was not
406 * @param {BrowsingContext} browsingContext
407 * The browsing context to check.
410 * True if the browsing context is valid, false otherwise.
412 isValidCanonicalBrowsingContext(browsingContext) {
414 CanonicalBrowsingContext.isInstance(browsingContext) &&
415 !browsingContext.isDiscarded
420 * Remove the given tab.
424 * @param {object=} options
425 * @param {boolean=} options.skipPermitUnload
426 * Flag to indicate if a potential beforeunload prompt should be skipped
427 * when closing the tab. Defaults to false.
429 async removeTab(tab, options = {}) {
430 const { skipPermitUnload = false } = options;
436 const ownerWindow = this.getWindowForTab(tab);
437 const tabBrowser = this.getTabBrowser(ownerWindow);
438 await tabBrowser.removeTab(tab, {
444 * Select the given tab.
450 * Promise that resolves when the given tab has been selected.
452 async selectTab(tab) {
454 return Promise.resolve();
457 const ownerWindow = this.getWindowForTab(tab);
458 const tabBrowser = this.getTabBrowser(ownerWindow);
460 if (tab === tabBrowser.selectedTab) {
461 return Promise.resolve();
464 const selected = new lazy.EventPromise(ownerWindow, "TabSelect");
465 tabBrowser.selectedTab = tab;
469 // Sometimes at that point window is not focused.
470 if (Services.focus.activeWindow != ownerWindow) {
471 const activated = new lazy.EventPromise(ownerWindow, "activate");
476 return Promise.resolve();
480 return lazy.AppInfo.isAndroid || lazy.AppInfo.isFirefox;
483 #onContextAttached = (eventName, data = {}) => {
484 const { browsingContext } = data;
485 if (this.isValidCanonicalBrowsingContext(browsingContext)) {
486 this.#navigableIds.set(
488 this.getIdForBrowsingContext(browsingContext)
494 // Expose a shared singleton.
495 export const TabManager = new TabManagerClass();