Bug 1845311 - [Part 2] Use ChromeUtils.defineLazyGetter in more places r=arai,webcomp...
[gecko.git] / browser / components / extensions / parent / ext-browser.js
blob4906a42a008475c77cfaaf2a1671d720e37b0e1f
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5  * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 "use strict";
9 // This file provides some useful code for the |tabs| and |windows|
10 // modules. All of the code is installed on |global|, which is a scope
11 // shared among the different ext-*.js scripts.
13 ChromeUtils.defineESModuleGetters(this, {
14   AboutReaderParent: "resource:///actors/AboutReaderParent.sys.mjs",
15   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
16   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
17   PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
18 });
20 var { ExtensionError } = ExtensionUtils;
22 var { defineLazyGetter } = ExtensionCommon;
24 const READER_MODE_PREFIX = "about:reader";
26 let tabTracker;
27 let windowTracker;
29 function isPrivateTab(nativeTab) {
30   return PrivateBrowsingUtils.isBrowserPrivate(nativeTab.linkedBrowser);
33 /* eslint-disable mozilla/balanced-listeners */
34 extensions.on("uninstalling", (msg, extension) => {
35   if (extension.uninstallURL) {
36     let browser = windowTracker.topWindow.gBrowser;
37     browser.addTab(extension.uninstallURL, {
38       relatedToCurrent: true,
39       triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
40         {}
41       ),
42     });
43   }
44 });
46 extensions.on("page-shutdown", (type, context) => {
47   if (context.viewType == "tab") {
48     if (context.extension.id !== context.xulBrowser.contentPrincipal.addonId) {
49       // Only close extension tabs.
50       // This check prevents about:addons from closing when it contains a
51       // WebExtension as an embedded inline options page.
52       return;
53     }
54     let { gBrowser } = context.xulBrowser.ownerGlobal;
55     if (gBrowser && gBrowser.getTabForBrowser) {
56       let nativeTab = gBrowser.getTabForBrowser(context.xulBrowser);
57       if (nativeTab) {
58         gBrowser.removeTab(nativeTab);
59       }
60     }
61   }
62 });
63 /* eslint-enable mozilla/balanced-listeners */
65 global.openOptionsPage = extension => {
66   let window = windowTracker.topWindow;
67   if (!window) {
68     return Promise.reject({ message: "No browser window available" });
69   }
71   if (extension.manifest.options_ui.open_in_tab) {
72     window.switchToTabHavingURI(extension.manifest.options_ui.page, true, {
73       triggeringPrincipal: extension.principal,
74     });
75     return Promise.resolve();
76   }
78   let viewId = `addons://detail/${encodeURIComponent(
79     extension.id
80   )}/preferences`;
82   return window.BrowserOpenAddonsMgr(viewId);
85 global.makeWidgetId = id => {
86   id = id.toLowerCase();
87   // FIXME: This allows for collisions.
88   return id.replace(/[^a-z0-9_-]/g, "_");
91 global.clickModifiersFromEvent = event => {
92   const map = {
93     shiftKey: "Shift",
94     altKey: "Alt",
95     metaKey: "Command",
96     ctrlKey: "Ctrl",
97   };
98   let modifiers = Object.keys(map)
99     .filter(key => event[key])
100     .map(key => map[key]);
102   if (event.ctrlKey && AppConstants.platform === "macosx") {
103     modifiers.push("MacCtrl");
104   }
106   return modifiers;
109 global.waitForTabLoaded = (tab, url) => {
110   return new Promise(resolve => {
111     windowTracker.addListener("progress", {
112       onLocationChange(browser, webProgress, request, locationURI, flags) {
113         if (
114           webProgress.isTopLevel &&
115           browser.ownerGlobal.gBrowser.getTabForBrowser(browser) == tab &&
116           (!url || locationURI.spec == url)
117         ) {
118           windowTracker.removeListener("progress", this);
119           resolve();
120         }
121       },
122     });
123   });
126 global.replaceUrlInTab = (gBrowser, tab, uri) => {
127   let loaded = waitForTabLoaded(tab, uri.spec);
128   gBrowser.loadURI(uri, {
129     flags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
130     triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), // This is safe from this functions usage however it would be preferred not to dot his.
131   });
132   return loaded;
136  * Manages tab-specific and window-specific context data, and dispatches
137  * tab select events across all windows.
138  */
139 global.TabContext = class extends EventEmitter {
140   /**
141    * @param {Function} getDefaultPrototype
142    *        Provides the prototype of the context value for a tab or window when there is none.
143    *        Called with a XULElement or ChromeWindow argument.
144    *        Should return an object or null.
145    */
146   constructor(getDefaultPrototype) {
147     super();
149     this.getDefaultPrototype = getDefaultPrototype;
151     this.tabData = new WeakMap();
153     windowTracker.addListener("progress", this);
154     windowTracker.addListener("TabSelect", this);
156     this.tabAdopted = this.tabAdopted.bind(this);
157     tabTracker.on("tab-adopted", this.tabAdopted);
158   }
160   /**
161    * Returns the context data associated with `keyObject`.
162    *
163    * @param {XULElement|ChromeWindow} keyObject
164    *        Browser tab or browser chrome window.
165    * @returns {object}
166    */
167   get(keyObject) {
168     if (!this.tabData.has(keyObject)) {
169       let data = Object.create(this.getDefaultPrototype(keyObject));
170       this.tabData.set(keyObject, data);
171     }
173     return this.tabData.get(keyObject);
174   }
176   /**
177    * Clears the context data associated with `keyObject`.
178    *
179    * @param {XULElement|ChromeWindow} keyObject
180    *        Browser tab or browser chrome window.
181    */
182   clear(keyObject) {
183     this.tabData.delete(keyObject);
184   }
186   handleEvent(event) {
187     if (event.type == "TabSelect") {
188       let nativeTab = event.target;
189       this.emit("tab-select", nativeTab);
190       this.emit("location-change", nativeTab);
191     }
192   }
194   onLocationChange(browser, webProgress, request, locationURI, flags) {
195     if (!webProgress.isTopLevel) {
196       // Only pageAction and browserAction are consuming the "location-change" event
197       // to update their per-tab status, and they should only do so in response of
198       // location changes related to the top level frame (See Bug 1493470 for a rationale).
199       return;
200     }
201     let gBrowser = browser.ownerGlobal.gBrowser;
202     let tab = gBrowser.getTabForBrowser(browser);
203     // fromBrowse will be false in case of e.g. a hash change or history.pushState
204     let fromBrowse = !(
205       flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
206     );
207     this.emit("location-change", tab, fromBrowse);
208   }
210   /**
211    * Persists context data when a tab is moved between windows.
212    *
213    * @param {string} eventType
214    *        Event type, should be "tab-adopted".
215    * @param {NativeTab} adoptingTab
216    *        The tab which is being opened and adopting `adoptedTab`.
217    * @param {NativeTab} adoptedTab
218    *        The tab which is being closed and adopted by `adoptingTab`.
219    */
220   tabAdopted(eventType, adoptingTab, adoptedTab) {
221     if (!this.tabData.has(adoptedTab)) {
222       return;
223     }
224     // Create a new object (possibly with different inheritance) when a tab is moved
225     // into a new window. But then reassign own properties from the old object.
226     let newData = this.get(adoptingTab);
227     let oldData = this.tabData.get(adoptedTab);
228     this.tabData.delete(adoptedTab);
229     Object.assign(newData, oldData);
230   }
232   /**
233    * Makes the TabContext instance stop emitting events.
234    */
235   shutdown() {
236     windowTracker.removeListener("progress", this);
237     windowTracker.removeListener("TabSelect", this);
238     tabTracker.off("tab-adopted", this.tabAdopted);
239   }
242 // This promise is used to wait for the search service to be initialized.
243 // None of the code in the WebExtension modules requests that initialization.
244 // It is assumed that it is started at some point. That might never happen,
245 // e.g. if the application shuts down before the search service initializes.
246 ChromeUtils.defineLazyGetter(global, "searchInitialized", () => {
247   if (Services.search.isInitialized) {
248     return Promise.resolve();
249   }
250   return ExtensionUtils.promiseObserved(
251     "browser-search-service",
252     (_, data) => data == "init-complete"
253   );
256 class WindowTracker extends WindowTrackerBase {
257   addProgressListener(window, listener) {
258     window.gBrowser.addTabsProgressListener(listener);
259   }
261   removeProgressListener(window, listener) {
262     window.gBrowser.removeTabsProgressListener(listener);
263   }
265   /**
266    * @param {BaseContext} context
267    *        The extension context
268    * @returns {DOMWindow|null} topNormalWindow
269    *        The currently active, or topmost, browser window, or null if no
270    *        browser window is currently open.
271    *        Will return the topmost "normal" (i.e., not popup) window.
272    */
273   getTopNormalWindow(context) {
274     let options = { allowPopups: false };
275     if (!context.privateBrowsingAllowed) {
276       options.private = false;
277     }
278     return BrowserWindowTracker.getTopWindow(options);
279   }
282 class TabTracker extends TabTrackerBase {
283   constructor() {
284     super();
286     this._tabs = new WeakMap();
287     this._browsers = new WeakMap();
288     this._tabIds = new Map();
289     this._nextId = 1;
290     this._deferredTabOpenEvents = new WeakMap();
292     this._handleTabDestroyed = this._handleTabDestroyed.bind(this);
293   }
295   init() {
296     if (this.initialized) {
297       return;
298     }
299     this.initialized = true;
301     this.adoptedTabs = new WeakSet();
303     this._handleWindowOpen = this._handleWindowOpen.bind(this);
304     this._handleWindowClose = this._handleWindowClose.bind(this);
306     windowTracker.addListener("TabClose", this);
307     windowTracker.addListener("TabOpen", this);
308     windowTracker.addListener("TabSelect", this);
309     windowTracker.addListener("TabMultiSelect", this);
310     windowTracker.addOpenListener(this._handleWindowOpen);
311     windowTracker.addCloseListener(this._handleWindowClose);
313     AboutReaderParent.addMessageListener("Reader:UpdateReaderButton", this);
315     /* eslint-disable mozilla/balanced-listeners */
316     this.on("tab-detached", this._handleTabDestroyed);
317     this.on("tab-removed", this._handleTabDestroyed);
318     /* eslint-enable mozilla/balanced-listeners */
319   }
321   getId(nativeTab) {
322     let id = this._tabs.get(nativeTab);
323     if (id) {
324       return id;
325     }
327     this.init();
329     id = this._nextId++;
330     this.setId(nativeTab, id);
331     return id;
332   }
334   getBrowserTabId(browser) {
335     let id = this._browsers.get(browser);
336     if (id) {
337       return id;
338     }
340     let tab = browser.ownerGlobal.gBrowser.getTabForBrowser(browser);
341     if (tab) {
342       id = this.getId(tab);
343       this._browsers.set(browser, id);
344       return id;
345     }
346     return -1;
347   }
349   setId(nativeTab, id) {
350     if (!nativeTab.parentNode) {
351       throw new Error("Cannot attach ID to a destroyed tab.");
352     }
353     if (nativeTab.ownerGlobal.closed) {
354       throw new Error("Cannot attach ID to a tab in a closed window.");
355     }
357     this._tabs.set(nativeTab, id);
358     if (nativeTab.linkedBrowser) {
359       this._browsers.set(nativeTab.linkedBrowser, id);
360     }
361     this._tabIds.set(id, nativeTab);
362   }
364   /**
365    * Handles tab adoption when a tab is moved between windows.
366    * Ensures the new tab will have the same ID as the old one, and
367    * emits "tab-adopted", "tab-detached" and "tab-attached" events.
368    *
369    * @param {NativeTab} adoptingTab
370    *        The tab which is being opened and adopting `adoptedTab`.
371    * @param {NativeTab} adoptedTab
372    *        The tab which is being closed and adopted by `adoptingTab`.
373    */
374   adopt(adoptingTab, adoptedTab) {
375     if (this.adoptedTabs.has(adoptedTab)) {
376       // The adoption has already been handled.
377       return;
378     }
379     this.adoptedTabs.add(adoptedTab);
380     let tabId = this.getId(adoptedTab);
381     this.setId(adoptingTab, tabId);
382     this.emit("tab-adopted", adoptingTab, adoptedTab);
383     if (this.has("tab-detached")) {
384       let nativeTab = adoptedTab;
385       let adoptedBy = adoptingTab;
386       let oldWindowId = windowTracker.getId(nativeTab.ownerGlobal);
387       let oldPosition = nativeTab._tPos;
388       this.emit("tab-detached", {
389         nativeTab,
390         adoptedBy,
391         tabId,
392         oldWindowId,
393         oldPosition,
394       });
395     }
396     if (this.has("tab-attached")) {
397       let nativeTab = adoptingTab;
398       let newWindowId = windowTracker.getId(nativeTab.ownerGlobal);
399       let newPosition = nativeTab._tPos;
400       this.emit("tab-attached", {
401         nativeTab,
402         tabId,
403         newWindowId,
404         newPosition,
405       });
406     }
407   }
409   _handleTabDestroyed(event, { nativeTab }) {
410     let id = this._tabs.get(nativeTab);
411     if (id) {
412       this._tabs.delete(nativeTab);
413       if (this._tabIds.get(id) === nativeTab) {
414         this._tabIds.delete(id);
415       }
416     }
417   }
419   /**
420    * Returns the XUL <tab> element associated with the given tab ID. If no tab
421    * with the given ID exists, and no default value is provided, an error is
422    * raised, belonging to the scope of the given context.
423    *
424    * @param {integer} tabId
425    *        The ID of the tab to retrieve.
426    * @param {*} default_
427    *        The value to return if no tab exists with the given ID.
428    * @returns {Element<tab>}
429    *        A XUL <tab> element.
430    */
431   getTab(tabId, default_ = undefined) {
432     let nativeTab = this._tabIds.get(tabId);
433     if (nativeTab) {
434       return nativeTab;
435     }
436     if (default_ !== undefined) {
437       return default_;
438     }
439     throw new ExtensionError(`Invalid tab ID: ${tabId}`);
440   }
442   /**
443    * Sets the opener of `tab` to the ID `openerTab`. Both tabs must be in the
444    * same window, or this function will throw a type error.
445    *
446    * @param {Element} tab The tab for which to set the owner.
447    * @param {Element} openerTab The opener of <tab>.
448    */
449   setOpener(tab, openerTab) {
450     if (tab.ownerDocument !== openerTab.ownerDocument) {
451       throw new Error("Tab must be in the same window as its opener");
452     }
453     tab.openerTab = openerTab;
454   }
456   deferredForTabOpen(nativeTab) {
457     let deferred = this._deferredTabOpenEvents.get(nativeTab);
458     if (!deferred) {
459       deferred = PromiseUtils.defer();
460       this._deferredTabOpenEvents.set(nativeTab, deferred);
461       deferred.promise.then(() => {
462         this._deferredTabOpenEvents.delete(nativeTab);
463       });
464     }
465     return deferred;
466   }
468   async maybeWaitForTabOpen(nativeTab) {
469     let deferred = this._deferredTabOpenEvents.get(nativeTab);
470     return deferred && deferred.promise;
471   }
473   /**
474    * @param {Event} event
475    *        The DOM Event to handle.
476    * @private
477    */
478   handleEvent(event) {
479     let nativeTab = event.target;
481     switch (event.type) {
482       case "TabOpen":
483         let { adoptedTab } = event.detail;
484         if (adoptedTab) {
485           // This tab is being created to adopt a tab from a different window.
486           // Handle the adoption.
487           this.adopt(nativeTab, adoptedTab);
488         } else {
489           // Save the size of the current tab, since the newly-created tab will
490           // likely be active by the time the promise below resolves and the
491           // event is dispatched.
492           const currentTab = nativeTab.ownerGlobal.gBrowser.selectedTab;
493           const { frameLoader } = currentTab.linkedBrowser;
494           const currentTabSize = {
495             width: frameLoader.lazyWidth,
496             height: frameLoader.lazyHeight,
497           };
499           // We need to delay sending this event until the next tick, since the
500           // tab can become selected immediately after "TabOpen", then onCreated
501           // should be fired with `active: true`.
502           let deferred = this.deferredForTabOpen(event.originalTarget);
503           Promise.resolve().then(() => {
504             deferred.resolve();
505             if (!event.originalTarget.parentNode) {
506               // If the tab is already be destroyed, do nothing.
507               return;
508             }
509             this.emitCreated(event.originalTarget, currentTabSize);
510           });
511         }
512         break;
514       case "TabClose":
515         let { adoptedBy } = event.detail;
516         if (adoptedBy) {
517           // This tab is being closed because it was adopted by a new window.
518           // Handle the adoption in case it was created as the first tab of a
519           // new window, and did not have an `adoptedTab` detail when it was
520           // opened.
521           this.adopt(adoptedBy, nativeTab);
522         } else {
523           this.emitRemoved(nativeTab, false);
524         }
525         break;
527       case "TabSelect":
528         // Because we are delaying calling emitCreated above, we also need to
529         // delay sending this event because it shouldn't fire before onCreated.
530         this.maybeWaitForTabOpen(nativeTab).then(() => {
531           if (!nativeTab.parentNode) {
532             // If the tab is already be destroyed, do nothing.
533             return;
534           }
535           this.emitActivated(nativeTab, event.detail.previousTab);
536         });
537         break;
539       case "TabMultiSelect":
540         if (this.has("tabs-highlighted")) {
541           // Because we are delaying calling emitCreated above, we also need to
542           // delay sending this event because it shouldn't fire before onCreated.
543           // event.target is gBrowser, so we don't use maybeWaitForTabOpen.
544           Promise.resolve().then(() => {
545             this.emitHighlighted(event.target.ownerGlobal);
546           });
547         }
548         break;
549     }
550   }
552   /**
553    * @param {object} message
554    *        The message to handle.
555    * @private
556    */
557   receiveMessage(message) {
558     switch (message.name) {
559       case "Reader:UpdateReaderButton":
560         if (message.data && message.data.isArticle !== undefined) {
561           this.emit("tab-isarticle", message);
562         }
563         break;
564     }
565   }
567   /**
568    * A private method which is called whenever a new browser window is opened,
569    * and dispatches the necessary events for it.
570    *
571    * @param {DOMWindow} window
572    *        The window being opened.
573    * @private
574    */
575   _handleWindowOpen(window) {
576     const tabToAdopt = window.gBrowserInit.getTabToAdopt();
577     if (tabToAdopt) {
578       // Note that this event handler depends on running before the
579       // delayed startup code in browser.js, which is currently triggered
580       // by the first MozAfterPaint event. That code handles finally
581       // adopting the tab, and clears it from the arguments list in the
582       // process, so if we run later than it, we're too late.
583       let adoptedBy = window.gBrowser.tabs[0];
584       this.adopt(adoptedBy, tabToAdopt);
585     } else {
586       for (let nativeTab of window.gBrowser.tabs) {
587         this.emitCreated(nativeTab);
588       }
590       // emitActivated to trigger tab.onActivated/tab.onHighlighted for a newly opened window.
591       this.emitActivated(window.gBrowser.tabs[0]);
592       if (this.has("tabs-highlighted")) {
593         this.emitHighlighted(window);
594       }
595     }
596   }
598   /**
599    * A private method which is called whenever a browser window is closed,
600    * and dispatches the necessary events for it.
601    *
602    * @param {DOMWindow} window
603    *        The window being closed.
604    * @private
605    */
606   _handleWindowClose(window) {
607     for (let nativeTab of window.gBrowser.tabs) {
608       if (!this.adoptedTabs.has(nativeTab)) {
609         this.emitRemoved(nativeTab, true);
610       }
611     }
612   }
614   /**
615    * Emits a "tab-activated" event for the given tab element.
616    *
617    * @param {NativeTab} nativeTab
618    *        The tab element which has been activated.
619    * @param {NativeTab} previousTab
620    *        The tab element which was previously activated.
621    * @private
622    */
623   emitActivated(nativeTab, previousTab = undefined) {
624     let previousTabIsPrivate, previousTabId;
625     if (previousTab && !previousTab.closing) {
626       previousTabId = this.getId(previousTab);
627       previousTabIsPrivate = isPrivateTab(previousTab);
628     }
629     this.emit("tab-activated", {
630       tabId: this.getId(nativeTab),
631       previousTabId,
632       previousTabIsPrivate,
633       windowId: windowTracker.getId(nativeTab.ownerGlobal),
634       nativeTab,
635     });
636   }
638   /**
639    * Emits a "tabs-highlighted" event for the given tab element.
640    *
641    * @param {ChromeWindow} window
642    *        The window in which the active tab or the set of multiselected tabs changed.
643    * @private
644    */
645   emitHighlighted(window) {
646     let tabIds = window.gBrowser.selectedTabs.map(tab => this.getId(tab));
647     let windowId = windowTracker.getId(window);
648     this.emit("tabs-highlighted", {
649       tabIds,
650       windowId,
651     });
652   }
654   /**
655    * Emits a "tab-created" event for the given tab element.
656    *
657    * @param {NativeTab} nativeTab
658    *        The tab element which is being created.
659    * @param {object} [currentTabSize]
660    *        The size of the tab element for the currently active tab.
661    * @private
662    */
663   emitCreated(nativeTab, currentTabSize) {
664     this.emit("tab-created", {
665       nativeTab,
666       currentTabSize,
667     });
668   }
670   /**
671    * Emits a "tab-removed" event for the given tab element.
672    *
673    * @param {NativeTab} nativeTab
674    *        The tab element which is being removed.
675    * @param {boolean} isWindowClosing
676    *        True if the tab is being removed because the browser window is
677    *        closing.
678    * @private
679    */
680   emitRemoved(nativeTab, isWindowClosing) {
681     let windowId = windowTracker.getId(nativeTab.ownerGlobal);
682     let tabId = this.getId(nativeTab);
684     this.emit("tab-removed", {
685       nativeTab,
686       tabId,
687       windowId,
688       isWindowClosing,
689     });
690   }
692   getBrowserData(browser) {
693     let window = browser.ownerGlobal;
694     if (!window) {
695       return {
696         tabId: -1,
697         windowId: -1,
698       };
699     }
700     let { gBrowser } = window;
701     // Some non-browser windows have gBrowser but not getTabForBrowser!
702     if (!gBrowser || !gBrowser.getTabForBrowser) {
703       if (window.top.document.documentURI === "about:addons") {
704         // When we're loaded into a <browser> inside about:addons, we need to go up
705         // one more level.
706         browser = window.docShell.chromeEventHandler;
708         ({ gBrowser } = browser.ownerGlobal);
709       } else {
710         return {
711           tabId: -1,
712           windowId: -1,
713         };
714       }
715     }
717     return {
718       tabId: this.getBrowserTabId(browser),
719       windowId: windowTracker.getId(browser.ownerGlobal),
720     };
721   }
723   get activeTab() {
724     let window = windowTracker.topWindow;
725     if (window && window.gBrowser) {
726       return window.gBrowser.selectedTab;
727     }
728     return null;
729   }
732 windowTracker = new WindowTracker();
733 tabTracker = new TabTracker();
735 Object.assign(global, { tabTracker, windowTracker });
737 class Tab extends TabBase {
738   get _favIconUrl() {
739     return this.window.gBrowser.getIcon(this.nativeTab);
740   }
742   get attention() {
743     return this.nativeTab.hasAttribute("attention");
744   }
746   get audible() {
747     return this.nativeTab.soundPlaying;
748   }
750   get autoDiscardable() {
751     return !this.nativeTab.undiscardable;
752   }
754   get browser() {
755     return this.nativeTab.linkedBrowser;
756   }
758   get discarded() {
759     return !this.nativeTab.linkedPanel;
760   }
762   get frameLoader() {
763     // If we don't have a frameLoader yet, just return a dummy with no width and
764     // height.
765     return super.frameLoader || { lazyWidth: 0, lazyHeight: 0 };
766   }
768   get hidden() {
769     return this.nativeTab.hidden;
770   }
772   get sharingState() {
773     return this.window.gBrowser.getTabSharingState(this.nativeTab);
774   }
776   get cookieStoreId() {
777     return getCookieStoreIdForTab(this, this.nativeTab);
778   }
780   get openerTabId() {
781     let opener = this.nativeTab.openerTab;
782     if (
783       opener &&
784       opener.parentNode &&
785       opener.ownerDocument == this.nativeTab.ownerDocument
786     ) {
787       return tabTracker.getId(opener);
788     }
789     return null;
790   }
792   get height() {
793     return this.frameLoader.lazyHeight;
794   }
796   get index() {
797     return this.nativeTab._tPos;
798   }
800   get mutedInfo() {
801     let { nativeTab } = this;
803     let mutedInfo = { muted: nativeTab.muted };
804     if (nativeTab.muteReason === null) {
805       mutedInfo.reason = "user";
806     } else if (nativeTab.muteReason) {
807       mutedInfo.reason = "extension";
808       mutedInfo.extensionId = nativeTab.muteReason;
809     }
811     return mutedInfo;
812   }
814   get lastAccessed() {
815     return this.nativeTab.lastAccessed;
816   }
818   get pinned() {
819     return this.nativeTab.pinned;
820   }
822   get active() {
823     return this.nativeTab.selected;
824   }
826   get highlighted() {
827     let { selected, multiselected } = this.nativeTab;
828     return selected || multiselected;
829   }
831   get status() {
832     if (this.nativeTab.getAttribute("busy") === "true") {
833       return "loading";
834     }
835     return "complete";
836   }
838   get width() {
839     return this.frameLoader.lazyWidth;
840   }
842   get window() {
843     return this.nativeTab.ownerGlobal;
844   }
846   get windowId() {
847     return windowTracker.getId(this.window);
848   }
850   get isArticle() {
851     return this.nativeTab.linkedBrowser.isArticle;
852   }
854   get isInReaderMode() {
855     return this.url && this.url.startsWith(READER_MODE_PREFIX);
856   }
858   get successorTabId() {
859     const { successor } = this.nativeTab;
860     return successor ? tabTracker.getId(successor) : -1;
861   }
863   /**
864    * Converts session store data to an object compatible with the return value
865    * of the convert() method, representing that data.
866    *
867    * @param {Extension} extension
868    *        The extension for which to convert the data.
869    * @param {object} tabData
870    *        Session store data for a closed tab, as returned by
871    *        `SessionStore.getClosedTabData()`.
872    * @param {DOMWindow} [window = null]
873    *        The browser window which the tab belonged to before it was closed.
874    *        May be null if the window the tab belonged to no longer exists.
875    *
876    * @returns {object}
877    * @static
878    */
879   static convertFromSessionStoreClosedData(extension, tabData, window = null) {
880     let result = {
881       sessionId: String(tabData.closedId),
882       index: tabData.pos ? tabData.pos : 0,
883       windowId: window && windowTracker.getId(window),
884       highlighted: false,
885       active: false,
886       pinned: false,
887       hidden: tabData.state ? tabData.state.hidden : tabData.hidden,
888       incognito: Boolean(tabData.state && tabData.state.isPrivate),
889       lastAccessed: tabData.state
890         ? tabData.state.lastAccessed
891         : tabData.lastAccessed,
892     };
894     let entries = tabData.state ? tabData.state.entries : tabData.entries;
895     let lastTabIndex = tabData.state ? tabData.state.index : tabData.index;
897     // Tab may have empty history.
898     if (entries.length) {
899       // We need to take lastTabIndex - 1 because the index in the tab data is
900       // 1-based rather than 0-based.
901       let entry = entries[lastTabIndex - 1];
903       // tabData is a representation of a tab, as stored in the session data,
904       // and given that is not a real nativeTab, we only need to check if the extension
905       // has the "tabs" or host permission (because tabData represents a closed tab,
906       // and so we already know that it can't be the activeTab).
907       if (
908         extension.hasPermission("tabs") ||
909         extension.allowedOrigins.matches(entry.url)
910       ) {
911         result.url = entry.url;
912         result.title = entry.title;
913         if (tabData.image) {
914           result.favIconUrl = tabData.image;
915         }
916       }
917     }
919     return result;
920   }
923 class Window extends WindowBase {
924   /**
925    * Update the geometry of the browser window.
926    *
927    * @param {object} options
928    *        An object containing new values for the window's geometry.
929    * @param {integer} [options.left]
930    *        The new pixel distance of the left side of the browser window from
931    *        the left of the screen.
932    * @param {integer} [options.top]
933    *        The new pixel distance of the top side of the browser window from
934    *        the top of the screen.
935    * @param {integer} [options.width]
936    *        The new pixel width of the window.
937    * @param {integer} [options.height]
938    *        The new pixel height of the window.
939    */
940   updateGeometry(options) {
941     let { window } = this;
943     if (options.left !== null || options.top !== null) {
944       let left = options.left !== null ? options.left : window.screenX;
945       let top = options.top !== null ? options.top : window.screenY;
946       window.moveTo(left, top);
947     }
949     if (options.width !== null || options.height !== null) {
950       let width = options.width !== null ? options.width : window.outerWidth;
951       let height =
952         options.height !== null ? options.height : window.outerHeight;
953       window.resizeTo(width, height);
954     }
955   }
957   get _title() {
958     return this.window.document.title;
959   }
961   setTitlePreface(titlePreface) {
962     this.window.document.documentElement.setAttribute(
963       "titlepreface",
964       titlePreface
965     );
966   }
968   get focused() {
969     return this.window.document.hasFocus();
970   }
972   get top() {
973     return this.window.screenY;
974   }
976   get left() {
977     return this.window.screenX;
978   }
980   get width() {
981     return this.window.outerWidth;
982   }
984   get height() {
985     return this.window.outerHeight;
986   }
988   get incognito() {
989     return PrivateBrowsingUtils.isWindowPrivate(this.window);
990   }
992   get alwaysOnTop() {
993     return this.appWindow.zLevel >= Ci.nsIAppWindow.raisedZ;
994   }
996   get isLastFocused() {
997     return this.window === windowTracker.topWindow;
998   }
1000   static getState(window) {
1001     const STATES = {
1002       [window.STATE_MAXIMIZED]: "maximized",
1003       [window.STATE_MINIMIZED]: "minimized",
1004       [window.STATE_FULLSCREEN]: "fullscreen",
1005       [window.STATE_NORMAL]: "normal",
1006     };
1007     return STATES[window.windowState];
1008   }
1010   get state() {
1011     return Window.getState(this.window);
1012   }
1014   async setState(state) {
1015     let { window } = this;
1017     const expectedState = (function () {
1018       switch (state) {
1019         case "maximized":
1020           return window.STATE_MAXIMIZED;
1021         case "minimized":
1022         case "docked":
1023           return window.STATE_MINIMIZED;
1024         case "normal":
1025           return window.STATE_NORMAL;
1026         case "fullscreen":
1027           return window.STATE_FULLSCREEN;
1028       }
1029       throw new Error(`Unexpected window state: ${state}`);
1030     })();
1032     const initialState = window.windowState;
1033     if (expectedState == initialState) {
1034       return;
1035     }
1037     // We check for window.fullScreen here to make sure to exit fullscreen even
1038     // if DOM and widget disagree on what the state is. This is a speculative
1039     // fix for bug 1780876, ideally it should not be needed.
1040     if (initialState == window.STATE_FULLSCREEN || window.fullScreen) {
1041       window.fullScreen = false;
1042     }
1044     switch (expectedState) {
1045       case window.STATE_MAXIMIZED:
1046         window.maximize();
1047         break;
1048       case window.STATE_MINIMIZED:
1049         window.minimize();
1050         break;
1052       case window.STATE_NORMAL:
1053         // Restore sometimes returns the window to its previous state, rather
1054         // than to the "normal" state, so it may need to be called anywhere from
1055         // zero to two times.
1056         window.restore();
1057         if (window.windowState !== window.STATE_NORMAL) {
1058           window.restore();
1059         }
1060         if (window.windowState !== window.STATE_NORMAL) {
1061           // And on OS-X, where normal vs. maximized is basically a heuristic,
1062           // we need to cheat.
1063           window.sizeToContent();
1064         }
1065         break;
1067       case window.STATE_FULLSCREEN:
1068         window.fullScreen = true;
1069         break;
1071       default:
1072         throw new Error(`Unexpected window state: ${state}`);
1073     }
1075     if (window.windowState != expectedState) {
1076       // On Linux, sizemode changes are asynchronous. Some of them might not
1077       // even happen if the window manager doesn't want to, so wait for a bit
1078       // instead of forever for a sizemode change that might not ever happen.
1079       const noWindowManagerTimeout = 2000;
1081       let onSizeModeChange;
1082       const promiseExpectedSizeMode = new Promise(resolve => {
1083         onSizeModeChange = function () {
1084           if (window.windowState == expectedState) {
1085             resolve();
1086           }
1087         };
1088         window.addEventListener("sizemodechange", onSizeModeChange);
1089       });
1091       await Promise.any([
1092         promiseExpectedSizeMode,
1093         new Promise(resolve => setTimeout(resolve, noWindowManagerTimeout)),
1094       ]);
1096       window.removeEventListener("sizemodechange", onSizeModeChange);
1097     }
1098   }
1100   *getTabs() {
1101     // A new window is being opened and it is adopting an existing tab, we return
1102     // an empty iterator here because there should be no other tabs to return during
1103     // that duration (See Bug 1458918 for a rationale).
1104     if (this.window.gBrowserInit.isAdoptingTab()) {
1105       return;
1106     }
1108     let { tabManager } = this.extension;
1110     for (let nativeTab of this.window.gBrowser.tabs) {
1111       let tab = tabManager.getWrapper(nativeTab);
1112       if (tab) {
1113         yield tab;
1114       }
1115     }
1116   }
1118   *getHighlightedTabs() {
1119     let { tabManager } = this.extension;
1120     for (let nativeTab of this.window.gBrowser.selectedTabs) {
1121       let tab = tabManager.getWrapper(nativeTab);
1122       if (tab) {
1123         yield tab;
1124       }
1125     }
1126   }
1128   get activeTab() {
1129     let { tabManager } = this.extension;
1131     // A new window is being opened and it is adopting a tab, and we do not create
1132     // a TabWrapper for the tab being adopted because it will go away once the tab
1133     // adoption has been completed (See Bug 1458918 for rationale).
1134     if (this.window.gBrowserInit.isAdoptingTab()) {
1135       return null;
1136     }
1138     return tabManager.getWrapper(this.window.gBrowser.selectedTab);
1139   }
1141   getTabAtIndex(index) {
1142     let nativeTab = this.window.gBrowser.tabs[index];
1143     if (nativeTab) {
1144       return this.extension.tabManager.getWrapper(nativeTab);
1145     }
1146   }
1148   /**
1149    * Converts session store data to an object compatible with the return value
1150    * of the convert() method, representing that data.
1151    *
1152    * @param {Extension} extension
1153    *        The extension for which to convert the data.
1154    * @param {object} windowData
1155    *        Session store data for a closed window, as returned by
1156    *        `SessionStore.getClosedWindowData()`.
1157    *
1158    * @returns {object}
1159    * @static
1160    */
1161   static convertFromSessionStoreClosedData(extension, windowData) {
1162     let result = {
1163       sessionId: String(windowData.closedId),
1164       focused: false,
1165       incognito: false,
1166       type: "normal", // this is always "normal" for a closed window
1167       // Bug 1781226: we assert "state" is "normal" in tests, but we could use
1168       // the "sizemode" property if we wanted.
1169       state: "normal",
1170       alwaysOnTop: false,
1171     };
1173     if (windowData.tabs.length) {
1174       result.tabs = windowData.tabs.map(tabData => {
1175         return Tab.convertFromSessionStoreClosedData(extension, tabData);
1176       });
1177     }
1179     return result;
1180   }
1183 Object.assign(global, { Tab, Window });
1185 class TabManager extends TabManagerBase {
1186   get(tabId, default_ = undefined) {
1187     let nativeTab = tabTracker.getTab(tabId, default_);
1189     if (nativeTab) {
1190       if (!this.canAccessTab(nativeTab)) {
1191         throw new ExtensionError(`Invalid tab ID: ${tabId}`);
1192       }
1193       return this.getWrapper(nativeTab);
1194     }
1195     return default_;
1196   }
1198   addActiveTabPermission(nativeTab = tabTracker.activeTab) {
1199     return super.addActiveTabPermission(nativeTab);
1200   }
1202   revokeActiveTabPermission(nativeTab = tabTracker.activeTab) {
1203     return super.revokeActiveTabPermission(nativeTab);
1204   }
1206   canAccessTab(nativeTab) {
1207     // Check private browsing access at browser window level.
1208     if (!this.extension.canAccessWindow(nativeTab.ownerGlobal)) {
1209       return false;
1210     }
1211     if (
1212       this.extension.userContextIsolation &&
1213       !this.extension.canAccessContainer(nativeTab.userContextId)
1214     ) {
1215       return false;
1216     }
1217     return true;
1218   }
1220   wrapTab(nativeTab) {
1221     return new Tab(this.extension, nativeTab, tabTracker.getId(nativeTab));
1222   }
1224   getWrapper(nativeTab) {
1225     if (!nativeTab.ownerGlobal.gBrowserInit.isAdoptingTab()) {
1226       return super.getWrapper(nativeTab);
1227     }
1228   }
1231 class WindowManager extends WindowManagerBase {
1232   get(windowId, context) {
1233     let window = windowTracker.getWindow(windowId, context);
1235     return this.getWrapper(window);
1236   }
1238   *getAll(context) {
1239     for (let window of windowTracker.browserWindows()) {
1240       if (!this.canAccessWindow(window, context)) {
1241         continue;
1242       }
1243       let wrapped = this.getWrapper(window);
1244       if (wrapped) {
1245         yield wrapped;
1246       }
1247     }
1248   }
1250   wrapWindow(window) {
1251     return new Window(this.extension, window, windowTracker.getId(window));
1252   }
1255 // eslint-disable-next-line mozilla/balanced-listeners
1256 extensions.on("startup", (type, extension) => {
1257   defineLazyGetter(extension, "tabManager", () => new TabManager(extension));
1258   defineLazyGetter(
1259     extension,
1260     "windowManager",
1261     () => new WindowManager(extension)
1262   );