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",
16 windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
19 class TabManagerClass {
25 // Maps browser's permanentKey to uuid: WeakMap.<Object, string>
26 this.#browserUniqueIds = new WeakMap();
28 // Maps browsing contexts to uuid: WeakMap.<BrowsingContext, string>.
29 // It's required as a fallback, since in the case when a context was discarded
30 // embedderElement is gone, and we cannot retrieve
31 // the context id from this.#browserUniqueIds.
32 this.#navigableIds = new WeakMap();
34 this.#contextListener = new lazy.BrowsingContextListener();
35 this.#contextListener.on("attached", this.#onContextAttached);
36 this.#contextListener.startListening();
38 this.browsers.forEach(browser => {
39 if (this.isValidCanonicalBrowsingContext(browser.browsingContext)) {
40 this.#navigableIds.set(
41 browser.browsingContext,
42 this.getIdForBrowsingContext(browser.browsingContext)
49 * Retrieve all the browser elements from tabs as contained in open windows.
51 * @returns {Array<XULBrowser>}
52 * All the found <xul:browser>s. Will return an empty array if
53 * no windows and tabs can be found.
58 for (const win of lazy.windowManager.windows) {
59 for (const tab of this.getTabsForWindow(win)) {
60 const contentBrowser = this.getBrowserForTab(tab);
61 if (contentBrowser !== null) {
62 browsers.push(contentBrowser);
71 * Retrieve all the browser tabs in open windows.
73 * @returns {Array<Tab>}
74 * All the open browser tabs. Will return an empty list if tab browser
75 * is not available or tabs are undefined.
80 for (const win of lazy.windowManager.windows) {
81 tabs.push(...this.getTabsForWindow(win));
88 * Array of unique browser ids (UUIDs) for all content browsers of all
91 * TODO: Similarly to getBrowserById, we should improve the performance of
92 * this getter in Bug 1750065.
94 * @returns {Array<string>}
95 * Array of UUIDs for all content browsers.
97 get allBrowserUniqueIds() {
98 const browserIds = [];
100 for (const win of lazy.windowManager.windows) {
101 // Only return handles for browser windows
102 for (const tab of this.getTabsForWindow(win)) {
103 const contentBrowser = this.getBrowserForTab(tab);
104 const winId = this.getIdForBrowser(contentBrowser);
105 if (winId !== null) {
106 browserIds.push(winId);
115 * Get the <code><xul:browser></code> for the specified tab.
118 * The tab whose browser needs to be returned.
120 * @returns {XULBrowser}
121 * The linked browser for the tab or null if no browser can be found.
123 getBrowserForTab(tab) {
124 if (tab && "linkedBrowser" in tab) {
125 return tab.linkedBrowser;
132 * Return the tab browser for the specified chrome window.
134 * @param {ChromeWindow} win
135 * Window whose <code>tabbrowser</code> needs to be accessed.
138 * Tab browser or null if it's not a browser window.
141 if (lazy.AppInfo.isAndroid) {
142 return new lazy.MobileTabBrowser(win);
143 } else if (lazy.AppInfo.isFirefox) {
153 * @param {object} options
154 * @param {boolean=} options.focus
155 * Set to true if the new tab should be focused (selected). Defaults to
156 * false. `false` value is not properly supported on Android, additional
157 * focus of previously selected tab is required after initial navigation.
158 * @param {Tab=} options.referenceTab
159 * The reference tab after which the new tab will be added. If no
160 * reference tab is provided, the new tab will be added after all the
162 * @param {string=} options.userContextId
163 * A user context id from UserContextManager.
164 * @param {window=} options.window
165 * The window where the new tab will open. Defaults to Services.wm.getMostRecentWindow
166 * if no window is provided. Will be ignored if referenceTab is provided.
168 async addTab(options = {}) {
172 userContextId = null,
173 window = Services.wm.getMostRecentWindow(null),
177 if (referenceTab != null) {
178 // If a reference tab was specified, the window should be the window
179 // owning the reference tab.
180 window = this.getWindowForTab(referenceTab);
183 if (referenceTab != null) {
184 index = this.getTabsForWindow(window).indexOf(referenceTab) + 1;
187 const tabBrowser = this.getTabBrowser(window);
189 const tab = await tabBrowser.addTab("about:blank", {
191 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
192 userContextId: lazy.UserContextManager.getInternalIdById(userContextId),
196 await this.selectTab(tab);
203 * Retrieve the browser element corresponding to the provided unique id,
204 * previously generated via getIdForBrowser.
206 * TODO: To avoid creating strong references on browser elements and
207 * potentially leaking those elements, this method loops over all windows and
208 * all tabs. It should be replaced by a faster implementation in Bug 1750065.
211 * A browser unique id created by getIdForBrowser.
212 * @returns {XULBrowser}
213 * The <xul:browser> corresponding to the provided id. Will return null if
214 * no matching browser element is found.
217 for (const win of lazy.windowManager.windows) {
218 for (const tab of this.getTabsForWindow(win)) {
219 const contentBrowser = this.getBrowserForTab(tab);
220 if (this.getIdForBrowser(contentBrowser) == id) {
221 return contentBrowser;
229 * Retrieve the browsing context corresponding to the provided unique id.
232 * A browsing context unique id (created by getIdForBrowsingContext).
233 * @returns {BrowsingContext=}
234 * The browsing context found for this id, null if none was found.
236 getBrowsingContextById(id) {
237 const browser = this.getBrowserById(id);
239 return browser.browsingContext;
242 return BrowsingContext.get(id);
246 * Retrieve the unique id for the given xul browser element. The id is a
247 * dynamically generated uuid associated with the permanentKey property of the
248 * given browser element. This method is preferable over getIdForBrowsingContext
249 * in case of working with browser element of a tab, since we can not guarantee
250 * that browsing context is attached to it.
252 * @param {XULBrowser} browserElement
253 * The <xul:browser> for which we want to retrieve the id.
254 * @returns {string} The unique id for this browser.
256 getIdForBrowser(browserElement) {
257 if (browserElement === null) {
261 const key = browserElement.permanentKey;
262 if (key === undefined) {
266 if (!this.#browserUniqueIds.has(key)) {
267 this.#browserUniqueIds.set(key, lazy.generateUUID());
269 return this.#browserUniqueIds.get(key);
273 * Retrieve the id of a Browsing Context.
275 * For a top-level browsing context a custom unique id will be returned.
277 * @param {BrowsingContext=} browsingContext
278 * The browsing context to get the id from.
281 * The id of the browsing context.
283 getIdForBrowsingContext(browsingContext) {
284 if (!browsingContext) {
288 if (!browsingContext.parent) {
289 // Top-level browsing contexts have their own custom unique id.
290 // If a context was discarded, embedderElement is already gone,
291 // so use navigable id instead.
292 return browsingContext.embedderElement
293 ? this.getIdForBrowser(browsingContext.embedderElement)
294 : this.#navigableIds.get(browsingContext);
297 return browsingContext.id.toString();
301 * Get the navigable for the given browsing context.
303 * Because Gecko doesn't support the Navigable concept in content
304 * scope the content browser could be used to uniquely identify
305 * top-level browsing contexts.
307 * @param {BrowsingContext} browsingContext
309 * @returns {BrowsingContext|XULBrowser} The navigable
311 * @throws {TypeError}
312 * If `browsingContext` is not a CanonicalBrowsingContext instance.
314 getNavigableForBrowsingContext(browsingContext) {
315 if (!this.isValidCanonicalBrowsingContext(browsingContext)) {
317 `Expected browsingContext to be a CanonicalBrowsingContext, got ${browsingContext}`
321 if (browsingContext.isContent && browsingContext.parent === null) {
322 return browsingContext.embedderElement;
325 return browsingContext;
330 for (const win of lazy.windowManager.windows) {
331 // For browser windows count the tabs. Otherwise take the window itself.
332 const tabsLength = this.getTabsForWindow(win).length;
333 count += tabsLength ? tabsLength : 1;
339 * Retrieve the tab owning a Browsing Context.
341 * @param {BrowsingContext=} browsingContext
342 * The browsing context to get the tab from.
344 * @returns {Tab|null}
345 * The tab owning the Browsing Context.
347 getTabForBrowsingContext(browsingContext) {
348 const browser = browsingContext?.top.embedderElement;
353 const tabBrowser = this.getTabBrowser(browser.ownerGlobal);
354 return tabBrowser.getTabForBrowser(browser);
358 * Retrieve the list of tabs for a given window.
360 * @param {ChromeWindow} win
361 * Window whose <code>tabs</code> need to be returned.
363 * @returns {Array<Tab>}
364 * The list of tabs. Will return an empty list if tab browser is not available
365 * or tabs are undefined.
367 getTabsForWindow(win) {
368 const tabBrowser = this.getTabBrowser(win);
369 // For web-platform reftests a faked tabbrowser is used,
370 // which does not actually have tabs.
371 if (tabBrowser && tabBrowser.tabs) {
372 return tabBrowser.tabs;
377 getWindowForTab(tab) {
378 // `.linkedBrowser.ownerGlobal` works both with Firefox Desktop and Mobile.
379 // Other accessors (eg `.ownerGlobal` or `.browser.ownerGlobal`) fail on one
381 return tab.linkedBrowser.ownerGlobal;
385 * Check if the given argument is a valid canonical browsing context and was not
388 * @param {BrowsingContext} browsingContext
389 * The browsing context to check.
392 * True if the browsing context is valid, false otherwise.
394 isValidCanonicalBrowsingContext(browsingContext) {
396 CanonicalBrowsingContext.isInstance(browsingContext) &&
397 !browsingContext.isDiscarded
402 * Remove the given tab.
406 * @param {object=} options
407 * @param {boolean=} options.skipPermitUnload
408 * Flag to indicate if a potential beforeunload prompt should be skipped
409 * when closing the tab. Defaults to false.
411 async removeTab(tab, options = {}) {
412 const { skipPermitUnload = false } = options;
418 const ownerWindow = this.getWindowForTab(tab);
419 const tabBrowser = this.getTabBrowser(ownerWindow);
420 await tabBrowser.removeTab(tab, {
426 * Select the given tab.
432 * Promise that resolves when the given tab has been selected.
434 async selectTab(tab) {
436 return Promise.resolve();
439 const ownerWindow = this.getWindowForTab(tab);
440 const tabBrowser = this.getTabBrowser(ownerWindow);
442 if (tab === tabBrowser.selectedTab) {
443 return Promise.resolve();
446 const selected = new lazy.EventPromise(ownerWindow, "TabSelect");
447 tabBrowser.selectedTab = tab;
451 // Sometimes at that point window is not focused.
452 if (Services.focus.activeWindow != ownerWindow) {
453 const activated = new lazy.EventPromise(ownerWindow, "activate");
458 return Promise.resolve();
462 return lazy.AppInfo.isAndroid || lazy.AppInfo.isFirefox;
465 #onContextAttached = (eventName, data = {}) => {
466 const { browsingContext } = data;
467 if (this.isValidCanonicalBrowsingContext(browsingContext)) {
468 this.#navigableIds.set(
470 this.getIdForBrowsingContext(browsingContext)
476 // Expose a shared singleton.
477 export const TabManager = new TabManagerClass();