Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / remote / shared / TabManager.sys.mjs
blob89377e833944a60d28202455ceb23b37fb9a9a35
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/. */
5 const lazy = {};
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",
14   UserContextManager:
15     "chrome://remote/content/shared/UserContextManager.sys.mjs",
16   windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
17 });
19 class TabManagerClass {
20   #browserUniqueIds;
21   #contextListener;
22   #navigableIds;
24   constructor() {
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)
43         );
44       }
45     });
46   }
48   /**
49    * Retrieve all the browser elements from tabs as contained in open windows.
50    *
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.
54    */
55   get browsers() {
56     const browsers = [];
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);
63         }
64       }
65     }
67     return browsers;
68   }
70   /**
71    * Retrieve all the browser tabs in open windows.
72    *
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.
76    */
77   get tabs() {
78     const tabs = [];
80     for (const win of lazy.windowManager.windows) {
81       tabs.push(...this.getTabsForWindow(win));
82     }
84     return tabs;
85   }
87   /**
88    * Array of unique browser ids (UUIDs) for all content browsers of all
89    * windows.
90    *
91    * TODO: Similarly to getBrowserById, we should improve the performance of
92    * this getter in Bug 1750065.
93    *
94    * @returns {Array<string>}
95    *     Array of UUIDs for all content browsers.
96    */
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);
107         }
108       }
109     }
111     return browserIds;
112   }
114   /**
115    * Get the <code>&lt;xul:browser&gt;</code> for the specified tab.
116    *
117    * @param {Tab} tab
118    *     The tab whose browser needs to be returned.
119    *
120    * @returns {XULBrowser}
121    *     The linked browser for the tab or null if no browser can be found.
122    */
123   getBrowserForTab(tab) {
124     if (tab && "linkedBrowser" in tab) {
125       return tab.linkedBrowser;
126     }
128     return null;
129   }
131   /**
132    * Return the tab browser for the specified chrome window.
133    *
134    * @param {ChromeWindow} win
135    *     Window whose <code>tabbrowser</code> needs to be accessed.
136    *
137    * @returns {Tab}
138    *     Tab browser or null if it's not a browser window.
139    */
140   getTabBrowser(win) {
141     if (lazy.AppInfo.isAndroid) {
142       return new lazy.MobileTabBrowser(win);
143     } else if (lazy.AppInfo.isFirefox) {
144       return win.gBrowser;
145     }
147     return null;
148   }
150   /**
151    * Create a new tab.
152    *
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
161    *     other tabs.
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.
167    */
168   async addTab(options = {}) {
169     let {
170       focus = false,
171       referenceTab = null,
172       userContextId = null,
173       window = Services.wm.getMostRecentWindow(null),
174     } = options;
176     let index;
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);
181     }
183     if (referenceTab != null) {
184       index = this.getTabsForWindow(window).indexOf(referenceTab) + 1;
185     }
187     const tabBrowser = this.getTabBrowser(window);
189     const tab = await tabBrowser.addTab("about:blank", {
190       index,
191       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
192       userContextId: lazy.UserContextManager.getInternalIdById(userContextId),
193     });
195     if (focus) {
196       await this.selectTab(tab);
197     }
199     return tab;
200   }
202   /**
203    * Retrieve the browser element corresponding to the provided unique id,
204    * previously generated via getIdForBrowser.
205    *
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.
209    *
210    * @param {string} id
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.
215    */
216   getBrowserById(id) {
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;
222         }
223       }
224     }
225     return null;
226   }
228   /**
229    * Retrieve the browsing context corresponding to the provided unique id.
230    *
231    * @param {string} 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.
235    */
236   getBrowsingContextById(id) {
237     const browser = this.getBrowserById(id);
238     if (browser) {
239       return browser.browsingContext;
240     }
242     return BrowsingContext.get(id);
243   }
245   /**
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.
251    *
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.
255    */
256   getIdForBrowser(browserElement) {
257     if (browserElement === null) {
258       return null;
259     }
261     const key = browserElement.permanentKey;
262     if (key === undefined) {
263       return null;
264     }
266     if (!this.#browserUniqueIds.has(key)) {
267       this.#browserUniqueIds.set(key, lazy.generateUUID());
268     }
269     return this.#browserUniqueIds.get(key);
270   }
272   /**
273    * Retrieve the id of a Browsing Context.
274    *
275    * For a top-level browsing context a custom unique id will be returned.
276    *
277    * @param {BrowsingContext=} browsingContext
278    *     The browsing context to get the id from.
279    *
280    * @returns {string}
281    *     The id of the browsing context.
282    */
283   getIdForBrowsingContext(browsingContext) {
284     if (!browsingContext) {
285       return null;
286     }
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);
295     }
297     return browsingContext.id.toString();
298   }
300   /**
301    * Get the navigable for the given browsing context.
302    *
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.
306    *
307    * @param {BrowsingContext} browsingContext
308    *
309    * @returns {BrowsingContext|XULBrowser} The navigable
310    *
311    * @throws {TypeError}
312    *     If `browsingContext` is not a CanonicalBrowsingContext instance.
313    */
314   getNavigableForBrowsingContext(browsingContext) {
315     if (!this.isValidCanonicalBrowsingContext(browsingContext)) {
316       throw new TypeError(
317         `Expected browsingContext to be a CanonicalBrowsingContext, got ${browsingContext}`
318       );
319     }
321     if (browsingContext.isContent && browsingContext.parent === null) {
322       return browsingContext.embedderElement;
323     }
325     return browsingContext;
326   }
328   getTabCount() {
329     let count = 0;
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;
334     }
335     return count;
336   }
338   /**
339    * Retrieve the tab owning a Browsing Context.
340    *
341    * @param {BrowsingContext=} browsingContext
342    *     The browsing context to get the tab from.
343    *
344    * @returns {Tab|null}
345    *     The tab owning the Browsing Context.
346    */
347   getTabForBrowsingContext(browsingContext) {
348     const browser = browsingContext?.top.embedderElement;
349     if (!browser) {
350       return null;
351     }
353     const tabBrowser = this.getTabBrowser(browser.ownerGlobal);
354     return tabBrowser.getTabForBrowser(browser);
355   }
357   /**
358    * Retrieve the list of tabs for a given window.
359    *
360    * @param {ChromeWindow} win
361    *     Window whose <code>tabs</code> need to be returned.
362    *
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.
366    */
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;
373     }
374     return [];
375   }
377   getWindowForTab(tab) {
378     // `.linkedBrowser.ownerGlobal` works both with Firefox Desktop and Mobile.
379     // Other accessors (eg `.ownerGlobal` or `.browser.ownerGlobal`) fail on one
380     // of the platforms.
381     return tab.linkedBrowser.ownerGlobal;
382   }
384   /**
385    * Check if the given argument is a valid canonical browsing context and was not
386    * discarded.
387    *
388    * @param {BrowsingContext} browsingContext
389    *     The browsing context to check.
390    *
391    * @returns {boolean}
392    *     True if the browsing context is valid, false otherwise.
393    */
394   isValidCanonicalBrowsingContext(browsingContext) {
395     return (
396       CanonicalBrowsingContext.isInstance(browsingContext) &&
397       !browsingContext.isDiscarded
398     );
399   }
401   /**
402    * Remove the given tab.
403    *
404    * @param {Tab} tab
405    *     Tab to remove.
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.
410    */
411   async removeTab(tab, options = {}) {
412     const { skipPermitUnload = false } = options;
414     if (!tab) {
415       return;
416     }
418     const ownerWindow = this.getWindowForTab(tab);
419     const tabBrowser = this.getTabBrowser(ownerWindow);
420     await tabBrowser.removeTab(tab, {
421       skipPermitUnload,
422     });
423   }
425   /**
426    * Select the given tab.
427    *
428    * @param {Tab} tab
429    *     Tab to select.
430    *
431    * @returns {Promise}
432    *     Promise that resolves when the given tab has been selected.
433    */
434   async selectTab(tab) {
435     if (!tab) {
436       return Promise.resolve();
437     }
439     const ownerWindow = this.getWindowForTab(tab);
440     const tabBrowser = this.getTabBrowser(ownerWindow);
442     if (tab === tabBrowser.selectedTab) {
443       return Promise.resolve();
444     }
446     const selected = new lazy.EventPromise(ownerWindow, "TabSelect");
447     tabBrowser.selectedTab = tab;
449     await selected;
451     // Sometimes at that point window is not focused.
452     if (Services.focus.activeWindow != ownerWindow) {
453       const activated = new lazy.EventPromise(ownerWindow, "activate");
454       ownerWindow.focus();
455       return activated;
456     }
458     return Promise.resolve();
459   }
461   supportsTabs() {
462     return lazy.AppInfo.isAndroid || lazy.AppInfo.isFirefox;
463   }
465   #onContextAttached = (eventName, data = {}) => {
466     const { browsingContext } = data;
467     if (this.isValidCanonicalBrowsingContext(browsingContext)) {
468       this.#navigableIds.set(
469         browsingContext,
470         this.getIdForBrowsingContext(browsingContext)
471       );
472     }
473   };
476 // Expose a shared singleton.
477 export const TabManager = new TabManagerClass();