tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ext-browser.js (39030B)


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