tor-browser

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

ext-tabs.js (63502B)


      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 ChromeUtils.defineESModuleGetters(this, {
     10  BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
     11  CustomizableUI:
     12    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     13  DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
     14  ExtensionControlledPopup:
     15    "resource:///modules/ExtensionControlledPopup.sys.mjs",
     16  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     17  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
     18 });
     19 
     20 ChromeUtils.defineLazyGetter(this, "strBundle", function () {
     21  return Services.strings.createBundle(
     22    "chrome://global/locale/extensions.properties"
     23  );
     24 });
     25 
     26 var { DefaultMap, ExtensionError } = ExtensionUtils;
     27 
     28 const TAB_HIDE_CONFIRMED_TYPE = "tabHideNotification";
     29 
     30 const TAB_ID_NONE = -1;
     31 
     32 ChromeUtils.defineLazyGetter(this, "tabHidePopup", () => {
     33  return new ExtensionControlledPopup({
     34    confirmedType: TAB_HIDE_CONFIRMED_TYPE,
     35    popupnotificationId: "extension-tab-hide-notification",
     36    descriptionId: "extension-tab-hide-notification-description",
     37    descriptionMessageId: "tabHideControlled.message",
     38    getLocalizedDescription: (doc, message, addonDetails) => {
     39      let image = doc.createXULElement("image");
     40      image.classList.add("extension-controlled-icon", "alltabs-icon");
     41      if (!doc.getElementById("alltabs-button")?.closest("#TabsToolbar")) {
     42        image.classList.add("alltabs-icon-generic");
     43      }
     44      return BrowserUIUtils.getLocalizedFragment(
     45        doc,
     46        message,
     47        addonDetails,
     48        image
     49      );
     50    },
     51    learnMoreLink: "extension-hiding-tabs",
     52  });
     53 });
     54 
     55 function showHiddenTabs(id) {
     56  for (let win of Services.wm.getEnumerator("navigator:browser")) {
     57    if (win.closed || !win.gBrowser) {
     58      continue;
     59    }
     60 
     61    for (let tab of win.gBrowser.tabs) {
     62      if (
     63        tab.hidden &&
     64        tab.ownerGlobal &&
     65        SessionStore.getCustomTabValue(tab, "hiddenBy") === id
     66      ) {
     67        win.gBrowser.showTab(tab);
     68      }
     69    }
     70  }
     71 }
     72 
     73 let tabListener = {
     74  tabReadyInitialized: false,
     75  // Map[tab -> Promise]
     76  tabBlockedPromises: new WeakMap(),
     77  // Map[tab -> Deferred]
     78  tabReadyPromises: new WeakMap(),
     79  initializingTabs: new WeakSet(),
     80 
     81  initTabReady() {
     82    if (!this.tabReadyInitialized) {
     83      windowTracker.addListener("progress", this);
     84 
     85      this.tabReadyInitialized = true;
     86    }
     87  },
     88 
     89  onLocationChange(browser, webProgress) {
     90    if (webProgress.isTopLevel) {
     91      let { gBrowser } = browser.ownerGlobal;
     92      let nativeTab = gBrowser.getTabForBrowser(browser);
     93 
     94      // Now we are certain that the first page in the tab was loaded.
     95      this.initializingTabs.delete(nativeTab);
     96 
     97      // browser.innerWindowID is now set, resolve the promises if any.
     98      let deferred = this.tabReadyPromises.get(nativeTab);
     99      if (deferred) {
    100        deferred.resolve(nativeTab);
    101        this.tabReadyPromises.delete(nativeTab);
    102      }
    103    }
    104  },
    105 
    106  blockTabUntilRestored(nativeTab) {
    107    let promise = ExtensionUtils.promiseEvent(nativeTab, "SSTabRestored").then(
    108      ({ target }) => {
    109        this.tabBlockedPromises.delete(target);
    110        return target;
    111      }
    112    );
    113 
    114    this.tabBlockedPromises.set(nativeTab, promise);
    115  },
    116 
    117  /**
    118   * Returns a promise that resolves when the tab is ready.
    119   * Tabs created via the `tabs.create` method are "ready" once the location
    120   * changes to the requested URL. Other tabs are assumed to be ready once their
    121   * inner window ID is known.
    122   *
    123   * @param {XULElement} nativeTab The <tab> element.
    124   * @returns {Promise} Resolves with the given tab once ready.
    125   */
    126  awaitTabReady(nativeTab) {
    127    let deferred = this.tabReadyPromises.get(nativeTab);
    128    if (!deferred) {
    129      let promise = this.tabBlockedPromises.get(nativeTab);
    130      if (promise) {
    131        return promise;
    132      }
    133      deferred = Promise.withResolvers();
    134      if (
    135        !this.initializingTabs.has(nativeTab) &&
    136        (nativeTab.linkedBrowser.innerWindowID ||
    137          nativeTab.linkedBrowser.currentURI.spec === "about:blank")
    138      ) {
    139        deferred.resolve(nativeTab);
    140      } else {
    141        this.initTabReady();
    142        this.tabReadyPromises.set(nativeTab, deferred);
    143      }
    144    }
    145    return deferred.promise;
    146  },
    147 };
    148 
    149 const allAttrs = new Set([
    150  "attention",
    151  "audible",
    152  "favIconUrl",
    153  "mutedInfo",
    154  "sharingState",
    155  "title",
    156  "autoDiscardable",
    157 ]);
    158 const allProperties = new Set([
    159  "attention",
    160  "audible",
    161  "autoDiscardable",
    162  "discarded",
    163  "favIconUrl",
    164  "groupId",
    165  "hidden",
    166  "isArticle",
    167  "mutedInfo",
    168  "openerTabId",
    169  "pinned",
    170  "sharingState",
    171  "status",
    172  "title",
    173  "url",
    174 ]);
    175 const restricted = new Set(["url", "favIconUrl", "title"]);
    176 
    177 this.tabs = class extends ExtensionAPIPersistent {
    178  static onUpdate(id, manifest) {
    179    if (!manifest.permissions || !manifest.permissions.includes("tabHide")) {
    180      showHiddenTabs(id);
    181    }
    182  }
    183 
    184  static onDisable(id) {
    185    showHiddenTabs(id);
    186    tabHidePopup.clearConfirmation(id);
    187  }
    188 
    189  static onUninstall(id) {
    190    tabHidePopup.clearConfirmation(id);
    191  }
    192 
    193  tabEventRegistrar({ event, listener }) {
    194    let { extension } = this;
    195    let { tabManager } = extension;
    196    return ({ fire }) => {
    197      let listener2 = (eventName, eventData, ...args) => {
    198        if (!tabManager.canAccessTab(eventData.nativeTab)) {
    199          return;
    200        }
    201 
    202        listener(fire, eventData, ...args);
    203      };
    204 
    205      tabTracker.on(event, listener2);
    206      return {
    207        unregister() {
    208          tabTracker.off(event, listener2);
    209        },
    210        convert(_fire) {
    211          fire = _fire;
    212        },
    213      };
    214    };
    215  }
    216 
    217  PERSISTENT_EVENTS = {
    218    onActivated: this.tabEventRegistrar({
    219      event: "tab-activated",
    220      listener: (fire, event) => {
    221        let { extension } = this;
    222        let { tabId, windowId, previousTabId, previousTabIsPrivate } = event;
    223        if (previousTabIsPrivate && !extension.privateBrowsingAllowed) {
    224          previousTabId = undefined;
    225        }
    226        fire.async({ tabId, previousTabId, windowId });
    227      },
    228    }),
    229    onAttached: this.tabEventRegistrar({
    230      event: "tab-attached",
    231      listener: (fire, event) => {
    232        fire.async(event.tabId, {
    233          newWindowId: event.newWindowId,
    234          newPosition: event.newPosition,
    235        });
    236      },
    237    }),
    238    onCreated: this.tabEventRegistrar({
    239      event: "tab-created",
    240      listener: (fire, event) => {
    241        let { tabManager } = this.extension;
    242        fire.async(tabManager.convert(event.nativeTab, event.currentTabSize));
    243      },
    244    }),
    245    onDetached: this.tabEventRegistrar({
    246      event: "tab-detached",
    247      listener: (fire, event) => {
    248        fire.async(event.tabId, {
    249          oldWindowId: event.oldWindowId,
    250          oldPosition: event.oldPosition,
    251        });
    252      },
    253    }),
    254    onRemoved: this.tabEventRegistrar({
    255      event: "tab-removed",
    256      listener: (fire, event) => {
    257        fire.async(event.tabId, {
    258          windowId: event.windowId,
    259          isWindowClosing: event.isWindowClosing,
    260        });
    261      },
    262    }),
    263    onMoved({ fire }) {
    264      let { tabManager } = this.extension;
    265      /**
    266       * @param {CustomEvent} event
    267       */
    268      let moveListener = event => {
    269        let nativeTab = event.originalTarget;
    270        let { previousTabState, currentTabState } = event.detail;
    271        let fromIndex = previousTabState.tabIndex;
    272        let toIndex = currentTabState.tabIndex;
    273        // TabMove also fires if its tab group changes; we should only fire
    274        // event if the position actually moved.
    275        if (fromIndex !== toIndex && tabManager.canAccessTab(nativeTab)) {
    276          fire.async(tabTracker.getId(nativeTab), {
    277            windowId: windowTracker.getId(nativeTab.ownerGlobal),
    278            fromIndex,
    279            toIndex,
    280          });
    281        }
    282      };
    283 
    284      windowTracker.addListener("TabMove", moveListener);
    285      return {
    286        unregister() {
    287          windowTracker.removeListener("TabMove", moveListener);
    288        },
    289        convert(_fire) {
    290          fire = _fire;
    291        },
    292      };
    293    },
    294 
    295    onHighlighted({ fire, context }) {
    296      let { windowManager } = this.extension;
    297      let highlightListener = (eventName, event) => {
    298        // TODO see if we can avoid "context" here
    299        let window = windowTracker.getWindow(event.windowId, context, false);
    300        if (!window) {
    301          return;
    302        }
    303        let windowWrapper = windowManager.getWrapper(window);
    304        if (!windowWrapper) {
    305          return;
    306        }
    307        let tabIds = Array.from(
    308          windowWrapper.getHighlightedTabs(),
    309          tab => tab.id
    310        );
    311        fire.async({ tabIds: tabIds, windowId: event.windowId });
    312      };
    313 
    314      tabTracker.on("tabs-highlighted", highlightListener);
    315      return {
    316        unregister() {
    317          tabTracker.off("tabs-highlighted", highlightListener);
    318        },
    319        convert(_fire, _context) {
    320          fire = _fire;
    321          context = _context;
    322        },
    323      };
    324    },
    325 
    326    onUpdated({ fire, context }, params) {
    327      let { extension } = this;
    328      let { tabManager } = extension;
    329      let [filterProps] = params;
    330      let filter = { ...filterProps };
    331      if (filter.urls) {
    332        filter.urls = new MatchPatternSet(filter.urls, {
    333          restrictSchemes: false,
    334        });
    335      }
    336      let needsModified = true;
    337      if (filter.properties) {
    338        // Default is to listen for all events.
    339        needsModified = filter.properties.some(p => allAttrs.has(p));
    340        filter.properties = new Set(filter.properties);
    341      } else {
    342        filter.properties = allProperties;
    343      }
    344 
    345      function sanitize(tab, changeInfo) {
    346        let result = {};
    347        let nonempty = false;
    348        for (let prop in changeInfo) {
    349          // In practice, changeInfo contains at most one property from
    350          // restricted. Therefore it is not necessary to cache the value
    351          // of tab.hasTabPermission outside the loop.
    352          // Unnecessarily accessing tab.hasTabPermission can cause bugs, see
    353          // https://bugzilla.mozilla.org/show_bug.cgi?id=1694699#c21
    354          if (!restricted.has(prop) || tab.hasTabPermission) {
    355            nonempty = true;
    356            result[prop] = changeInfo[prop];
    357          }
    358        }
    359        return nonempty && result;
    360      }
    361 
    362      function getWindowID(windowId) {
    363        if (windowId === Window.WINDOW_ID_CURRENT) {
    364          let window = windowTracker.getTopWindow(context);
    365          if (!window) {
    366            return undefined;
    367          }
    368          return windowTracker.getId(window);
    369        }
    370        return windowId;
    371      }
    372 
    373      function matchFilters(tab) {
    374        if (!filterProps) {
    375          return true;
    376        }
    377        if (filter.tabId != null && tab.id != filter.tabId) {
    378          return false;
    379        }
    380        if (
    381          filter.windowId != null &&
    382          tab.windowId != getWindowID(filter.windowId)
    383        ) {
    384          return false;
    385        }
    386        if (
    387          filter.cookieStoreId != null &&
    388          filter.cookieStoreId !== tab.cookieStoreId
    389        ) {
    390          return false;
    391        }
    392        if (filter.urls) {
    393          return filter.urls.matches(tab._uri) && tab.hasTabPermission;
    394        }
    395        return true;
    396      }
    397 
    398      let fireForTab = (tab, changed, nativeTab) => {
    399        // Tab may be null if private and not_allowed.
    400        if (!tab || !matchFilters(tab, changed)) {
    401          return;
    402        }
    403 
    404        let changeInfo = sanitize(tab, changed);
    405        if (changeInfo) {
    406          tabTracker.maybeWaitForTabOpen(nativeTab).then(() => {
    407            if (!nativeTab.parentNode) {
    408              // If the tab is already be destroyed, do nothing.
    409              return;
    410            }
    411            fire.async(tab.id, changeInfo, tab.convert());
    412          });
    413        }
    414      };
    415 
    416      let listener = event => {
    417        // tab grouping events are fired on the group,
    418        // not the tab itself.
    419        let updatedTab = event.originalTarget;
    420        if (event.type == "TabGrouped" || event.type == "TabUngrouped") {
    421          updatedTab = event.detail;
    422        }
    423 
    424        // Ignore any events prior to TabOpen
    425        // and events that are triggered while tabs are swapped between windows.
    426        if (
    427          updatedTab.initializingTab ||
    428          updatedTab.ownerGlobal.gBrowserInit?.isAdoptingTab()
    429        ) {
    430          return;
    431        }
    432        if (!extension.canAccessWindow(event.originalTarget.ownerGlobal)) {
    433          return;
    434        }
    435        let needed = [];
    436 
    437        if (event.type == "TabAttrModified") {
    438          let changed = event.detail.changed;
    439          if (
    440            changed.includes("image") &&
    441            filter.properties.has("favIconUrl")
    442          ) {
    443            needed.push("favIconUrl");
    444          }
    445          if (changed.includes("muted") && filter.properties.has("mutedInfo")) {
    446            needed.push("mutedInfo");
    447          }
    448          if (
    449            changed.includes("soundplaying") &&
    450            filter.properties.has("audible")
    451          ) {
    452            needed.push("audible");
    453          }
    454          if (
    455            changed.includes("undiscardable") &&
    456            filter.properties.has("autoDiscardable")
    457          ) {
    458            needed.push("autoDiscardable");
    459          }
    460          if (changed.includes("label") && filter.properties.has("title")) {
    461            needed.push("title");
    462          }
    463          if (
    464            changed.includes("sharing") &&
    465            filter.properties.has("sharingState")
    466          ) {
    467            needed.push("sharingState");
    468          }
    469          if (
    470            changed.includes("attention") &&
    471            filter.properties.has("attention")
    472          ) {
    473            needed.push("attention");
    474          }
    475        } else if (event.type == "TabPinned") {
    476          needed.push("pinned");
    477        } else if (event.type == "TabUnpinned") {
    478          needed.push("pinned");
    479        } else if (event.type == "TabBrowserInserted") {
    480          // This may be an adopted tab. Bail early to avoid asking tabManager
    481          // about the tab before we run the adoption logic in ext-browser.js.
    482          if (event.detail.insertedOnTabCreation) {
    483            return;
    484          }
    485          needed.push("discarded");
    486        } else if (event.type == "TabBrowserDiscarded") {
    487          needed.push("discarded");
    488        } else if (event.type === "TabGrouped") {
    489          needed.push("groupId");
    490        } else if (event.type === "TabUngrouped") {
    491          if (updatedTab.group) {
    492            // If there is still a group, that means that the group changed,
    493            // so TabGrouped will also fire. Ignore to avoid duplicate events.
    494            return;
    495          }
    496          needed.push("groupId");
    497        } else if (event.type == "TabShow") {
    498          needed.push("hidden");
    499        } else if (event.type == "TabHide") {
    500          needed.push("hidden");
    501        }
    502 
    503        let tab = tabManager.getWrapper(updatedTab);
    504 
    505        let changeInfo = {};
    506        for (let prop of needed) {
    507          changeInfo[prop] = tab[prop];
    508        }
    509 
    510        fireForTab(tab, changeInfo, updatedTab);
    511      };
    512 
    513      let statusListener = ({ browser, status, url }) => {
    514        let { gBrowser } = browser.ownerGlobal;
    515        let tabElem = gBrowser.getTabForBrowser(browser);
    516        if (tabElem) {
    517          if (!extension.canAccessWindow(tabElem.ownerGlobal)) {
    518            return;
    519          }
    520 
    521          let changed = {};
    522          if (filter.properties.has("status")) {
    523            changed.status = status;
    524          }
    525          if (url && filter.properties.has("url")) {
    526            changed.url = url;
    527          }
    528 
    529          fireForTab(tabManager.wrapTab(tabElem), changed, tabElem);
    530        }
    531      };
    532 
    533      let isArticleChangeListener = (messageName, message) => {
    534        let { gBrowser } = message.target.ownerGlobal;
    535        let nativeTab = gBrowser.getTabForBrowser(message.target);
    536 
    537        if (nativeTab && extension.canAccessWindow(nativeTab.ownerGlobal)) {
    538          let tab = tabManager.getWrapper(nativeTab);
    539          fireForTab(tab, { isArticle: message.data.isArticle }, nativeTab);
    540        }
    541      };
    542 
    543      let openerTabIdChangeListener = (_, { nativeTab, openerTabId }) => {
    544        let tab = tabManager.getWrapper(nativeTab);
    545        fireForTab(tab, { openerTabId }, nativeTab);
    546      };
    547 
    548      let listeners = new Map();
    549      if (filter.properties.has("status") || filter.properties.has("url")) {
    550        listeners.set("status", statusListener);
    551      }
    552      if (needsModified) {
    553        listeners.set("TabAttrModified", listener);
    554      }
    555      if (filter.properties.has("pinned")) {
    556        listeners.set("TabPinned", listener);
    557        listeners.set("TabUnpinned", listener);
    558      }
    559      if (filter.properties.has("discarded")) {
    560        listeners.set("TabBrowserInserted", listener);
    561        listeners.set("TabBrowserDiscarded", listener);
    562      }
    563      if (filter.properties.has("groupId")) {
    564        listeners.set("TabGrouped", listener);
    565        listeners.set("TabUngrouped", listener);
    566      }
    567      if (filter.properties.has("hidden")) {
    568        listeners.set("TabShow", listener);
    569        listeners.set("TabHide", listener);
    570      }
    571 
    572      for (let [name, listener] of listeners) {
    573        windowTracker.addListener(name, listener);
    574      }
    575 
    576      if (filter.properties.has("isArticle")) {
    577        tabTracker.on("tab-isarticle", isArticleChangeListener);
    578      }
    579 
    580      if (filter.properties.has("openerTabId")) {
    581        tabTracker.on("tab-openerTabId", openerTabIdChangeListener);
    582      }
    583 
    584      return {
    585        unregister() {
    586          for (let [name, listener] of listeners) {
    587            windowTracker.removeListener(name, listener);
    588          }
    589 
    590          if (filter.properties.has("isArticle")) {
    591            tabTracker.off("tab-isarticle", isArticleChangeListener);
    592          }
    593 
    594          if (filter.properties.has("openerTabId")) {
    595            tabTracker.off("tab-openerTabId", openerTabIdChangeListener);
    596          }
    597        },
    598        convert(_fire, _context) {
    599          fire = _fire;
    600          context = _context;
    601        },
    602      };
    603    },
    604  };
    605 
    606  getAPI(context) {
    607    let { extension } = context;
    608    let { tabManager, windowManager } = extension;
    609    let extensionApi = this;
    610    let module = "tabs";
    611 
    612    function getTabOrActive(tabId) {
    613      let tab =
    614        tabId !== null ? tabTracker.getTab(tabId) : tabTracker.activeTab;
    615      if (!tabManager.canAccessTab(tab)) {
    616        throw new ExtensionError(
    617          tabId === null
    618            ? "Cannot access activeTab"
    619            : `Invalid tab ID: ${tabId}`
    620        );
    621      }
    622      return tab;
    623    }
    624 
    625    function getNativeTabsFromIDArray(tabIds) {
    626      if (!Array.isArray(tabIds)) {
    627        tabIds = [tabIds];
    628      }
    629      return tabIds.map(tabId => {
    630        let tab = tabTracker.getTab(tabId);
    631        if (!tabManager.canAccessTab(tab)) {
    632          throw new ExtensionError(`Invalid tab ID: ${tabId}`);
    633        }
    634        return tab;
    635      });
    636    }
    637 
    638    async function promiseTabWhenReady(tabId) {
    639      let tab;
    640      if (tabId !== null) {
    641        tab = tabManager.get(tabId);
    642      } else {
    643        tab = tabManager.getWrapper(tabTracker.activeTab);
    644      }
    645      if (!tab) {
    646        throw new ExtensionError(
    647          tabId == null ? "Cannot access activeTab" : `Invalid tab ID: ${tabId}`
    648        );
    649      }
    650 
    651      await tabListener.awaitTabReady(tab.nativeTab);
    652 
    653      return tab;
    654    }
    655 
    656    function setContentTriggeringPrincipal(url, browser, options) {
    657      // For urls that we want to allow an extension to open in a tab, but
    658      // that it may not otherwise have access to, we set the triggering
    659      // principal to the url that is being opened.  This is used for newtab,
    660      // about: and moz-extension: protocols.
    661      options.triggeringPrincipal =
    662        Services.scriptSecurityManager.createContentPrincipal(
    663          Services.io.newURI(url),
    664          {
    665            userContextId: options.userContextId,
    666            privateBrowsingId: PrivateBrowsingUtils.isBrowserPrivate(browser)
    667              ? 1
    668              : 0,
    669          }
    670        );
    671    }
    672 
    673    let tabsApi = {
    674      tabs: {
    675        onActivated: new EventManager({
    676          context,
    677          module,
    678          event: "onActivated",
    679          extensionApi,
    680        }).api(),
    681 
    682        onCreated: new EventManager({
    683          context,
    684          module,
    685          event: "onCreated",
    686          extensionApi,
    687        }).api(),
    688 
    689        onHighlighted: new EventManager({
    690          context,
    691          module,
    692          event: "onHighlighted",
    693          extensionApi,
    694        }).api(),
    695 
    696        onAttached: new EventManager({
    697          context,
    698          module,
    699          event: "onAttached",
    700          extensionApi,
    701        }).api(),
    702 
    703        onDetached: new EventManager({
    704          context,
    705          module,
    706          event: "onDetached",
    707          extensionApi,
    708        }).api(),
    709 
    710        onRemoved: new EventManager({
    711          context,
    712          module,
    713          event: "onRemoved",
    714          extensionApi,
    715        }).api(),
    716 
    717        onReplaced: new EventManager({
    718          context,
    719          name: "tabs.onReplaced",
    720          register: () => {
    721            return () => {};
    722          },
    723        }).api(),
    724 
    725        onMoved: new EventManager({
    726          context,
    727          module,
    728          event: "onMoved",
    729          extensionApi,
    730        }).api(),
    731 
    732        onUpdated: new EventManager({
    733          context,
    734          module,
    735          event: "onUpdated",
    736          extensionApi,
    737        }).api(),
    738 
    739        create(createProperties) {
    740          return new Promise(resolve => {
    741            let window =
    742              createProperties.windowId !== null
    743                ? windowTracker.getWindow(createProperties.windowId, context)
    744                : windowTracker.getTopNormalWindow(context);
    745            if (!window || !context.canAccessWindow(window)) {
    746              throw new Error(
    747                "Not allowed to create tabs on the target window"
    748              );
    749            }
    750            let { gBrowserInit } = window;
    751            if (!gBrowserInit || !gBrowserInit.delayedStartupFinished) {
    752              let obs = finishedWindow => {
    753                if (finishedWindow != window) {
    754                  return;
    755                }
    756                Services.obs.removeObserver(
    757                  obs,
    758                  "browser-delayed-startup-finished"
    759                );
    760                resolve(window);
    761              };
    762              Services.obs.addObserver(obs, "browser-delayed-startup-finished");
    763            } else {
    764              resolve(window);
    765            }
    766          }).then(window => {
    767            let url;
    768 
    769            let options = { triggeringPrincipal: context.principal };
    770            if (createProperties.cookieStoreId) {
    771              // May throw if validation fails.
    772              options.userContextId = getUserContextIdForCookieStoreId(
    773                extension,
    774                createProperties.cookieStoreId,
    775                PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser)
    776              );
    777            }
    778 
    779            if (createProperties.url !== null) {
    780              url = context.uri.resolve(createProperties.url);
    781 
    782              if (
    783                !ExtensionUtils.isExtensionUrl(url) &&
    784                !context.checkLoadURL(url, { dontReportErrors: true })
    785              ) {
    786                return Promise.reject({ message: `Illegal URL: ${url}` });
    787              }
    788 
    789              if (createProperties.openInReaderMode) {
    790                url = `about:reader?url=${encodeURIComponent(url)}`;
    791              }
    792            } else {
    793              url = window.BROWSER_NEW_TAB_URL;
    794            }
    795            let discardable = url && !url.startsWith("about:");
    796            // Handle moz-ext separately from the discardable flag to retain prior behavior.
    797            if (!discardable || ExtensionUtils.isExtensionUrl(url)) {
    798              setContentTriggeringPrincipal(url, window.gBrowser, options);
    799            }
    800 
    801            tabListener.initTabReady();
    802            const currentTab = window.gBrowser.selectedTab;
    803            const { frameLoader } = currentTab.linkedBrowser;
    804            const currentTabSize = {
    805              width: frameLoader.lazyWidth,
    806              height: frameLoader.lazyHeight,
    807            };
    808 
    809            if (createProperties.openerTabId !== null) {
    810              options.ownerTab = tabTracker.getTab(
    811                createProperties.openerTabId
    812              );
    813              options.openerBrowser = options.ownerTab.linkedBrowser;
    814              if (options.ownerTab.ownerGlobal !== window) {
    815                return Promise.reject({
    816                  message:
    817                    "Opener tab must be in the same window as the tab being created",
    818                });
    819              }
    820            }
    821 
    822            if (createProperties.index != null) {
    823              options.tabIndex = createProperties.index;
    824            }
    825 
    826            if (createProperties.pinned != null) {
    827              options.pinned = createProperties.pinned;
    828            }
    829 
    830            let active =
    831              createProperties.active !== null
    832                ? createProperties.active
    833                : !createProperties.discarded;
    834            if (createProperties.discarded) {
    835              if (active) {
    836                return Promise.reject({
    837                  message: `Active tabs cannot be created and discarded.`,
    838                });
    839              }
    840              if (createProperties.pinned) {
    841                return Promise.reject({
    842                  message: `Pinned tabs cannot be created and discarded.`,
    843                });
    844              }
    845              if (!discardable) {
    846                return Promise.reject({
    847                  message: `Cannot create a discarded new tab or "about" urls.`,
    848                });
    849              }
    850              options.createLazyBrowser = true;
    851              options.lazyTabTitle = createProperties.title;
    852            } else if (createProperties.title) {
    853              return Promise.reject({
    854                message: `Title may only be set for discarded tabs.`,
    855              });
    856            }
    857 
    858            let nativeTab = window.gBrowser.addTab(url, options);
    859 
    860            if (active) {
    861              window.gBrowser.selectedTab = nativeTab;
    862              if (!createProperties.url) {
    863                window.gURLBar.select();
    864              }
    865            }
    866 
    867            if (
    868              createProperties.url &&
    869              createProperties.url !== window.BROWSER_NEW_TAB_URL &&
    870              !createProperties.url.startsWith("about:blank")
    871            ) {
    872              // We can't wait for a location change event for about:newtab,
    873              // since it may be pre-rendered, in which case its initial
    874              // location change event has already fired.
    875              // The same goes for about:blank, since the initial blank document
    876              // is loaded synchronously.
    877 
    878              // Mark the tab as initializing, so that operations like
    879              // `executeScript` wait until the requested URL is loaded in
    880              // the tab before dispatching messages to the inner window
    881              // that contains the URL we're attempting to load.
    882              tabListener.initializingTabs.add(nativeTab);
    883            }
    884 
    885            if (createProperties.muted) {
    886              nativeTab.toggleMuteAudio(extension.id);
    887            }
    888 
    889            return tabManager.convert(nativeTab, currentTabSize);
    890          });
    891        },
    892 
    893        async remove(tabIds) {
    894          let nativeTabs = getNativeTabsFromIDArray(tabIds);
    895 
    896          if (nativeTabs.length === 1) {
    897            nativeTabs[0].ownerGlobal.gBrowser.removeTab(nativeTabs[0]);
    898            return;
    899          }
    900 
    901          // Or for multiple tabs, first group them by window
    902          let windowTabMap = new DefaultMap(() => []);
    903          for (let nativeTab of nativeTabs) {
    904            windowTabMap.get(nativeTab.ownerGlobal).push(nativeTab);
    905          }
    906 
    907          // Then make one call to removeTabs() for each window, to keep the
    908          // count accurate for SessionStore.getLastClosedTabCount().
    909          // Note: always pass options to disable animation and the warning
    910          // dialogue box, so that way all tabs are actually closed when the
    911          // browser.tabs.remove() promise resolves
    912          for (let [eachWindow, tabsToClose] of windowTabMap.entries()) {
    913            eachWindow.gBrowser.removeTabs(tabsToClose, {
    914              animate: false,
    915              suppressWarnAboutClosingWindow: true,
    916            });
    917          }
    918        },
    919 
    920        async discard(tabIds) {
    921          let nativeTabs = getNativeTabsFromIDArray(tabIds);
    922          await Promise.all(
    923            nativeTabs.map(nativeTab =>
    924              nativeTab.ownerGlobal.gBrowser.prepareDiscardBrowser(nativeTab)
    925            )
    926          );
    927          for (let nativeTab of nativeTabs) {
    928            nativeTab.ownerGlobal.gBrowser.discardBrowser(nativeTab);
    929          }
    930        },
    931 
    932        async update(tabId, updateProperties) {
    933          let nativeTab = getTabOrActive(tabId);
    934 
    935          let tabbrowser = nativeTab.ownerGlobal.gBrowser;
    936 
    937          if (updateProperties.url !== null) {
    938            let url = context.uri.resolve(updateProperties.url);
    939 
    940            let options = {
    941              flags: updateProperties.loadReplace
    942                ? Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY
    943                : Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
    944              triggeringPrincipal: context.principal,
    945            };
    946 
    947            if (!context.checkLoadURL(url, { dontReportErrors: true })) {
    948              // We allow loading top level tabs for "other" extensions.
    949              if (ExtensionUtils.isExtensionUrl(url)) {
    950                setContentTriggeringPrincipal(url, tabbrowser, options);
    951              } else {
    952                return Promise.reject({ message: `Illegal URL: ${url}` });
    953              }
    954            }
    955 
    956            let browser = nativeTab.linkedBrowser;
    957            if (nativeTab.linkedPanel) {
    958              browser.fixupAndLoadURIString(url, options);
    959            } else {
    960              // Shift to fully loaded browser and make
    961              // sure load handler is instantiated.
    962              nativeTab.addEventListener(
    963                "SSTabRestoring",
    964                () => browser.fixupAndLoadURIString(url, options),
    965                { once: true }
    966              );
    967              tabbrowser._insertBrowser(nativeTab);
    968            }
    969          }
    970 
    971          if (updateProperties.active) {
    972            tabbrowser.selectedTab = nativeTab;
    973          }
    974          if (updateProperties.autoDiscardable !== null) {
    975            nativeTab.undiscardable = !updateProperties.autoDiscardable;
    976          }
    977          if (updateProperties.highlighted !== null) {
    978            if (updateProperties.highlighted) {
    979              if (!nativeTab.selected && !nativeTab.multiselected) {
    980                tabbrowser.addToMultiSelectedTabs(nativeTab);
    981                // Select the highlighted tab unless active:false is provided.
    982                // Note that Chrome selects it even in that case.
    983                if (updateProperties.active !== false) {
    984                  tabbrowser.lockClearMultiSelectionOnce();
    985                  tabbrowser.selectedTab = nativeTab;
    986                }
    987              }
    988            } else {
    989              tabbrowser.removeFromMultiSelectedTabs(nativeTab);
    990            }
    991          }
    992          if (updateProperties.muted !== null) {
    993            if (nativeTab.muted != updateProperties.muted) {
    994              nativeTab.toggleMuteAudio(extension.id);
    995            }
    996          }
    997          if (updateProperties.pinned !== null) {
    998            if (updateProperties.pinned) {
    999              tabbrowser.pinTab(nativeTab);
   1000            } else {
   1001              tabbrowser.unpinTab(nativeTab);
   1002            }
   1003          }
   1004          if (updateProperties.openerTabId !== null) {
   1005            tabTracker.setOpener(nativeTab, updateProperties.openerTabId);
   1006          }
   1007          if (updateProperties.successorTabId !== null) {
   1008            let successor = null;
   1009            if (updateProperties.successorTabId !== TAB_ID_NONE) {
   1010              successor = tabTracker.getTab(
   1011                updateProperties.successorTabId,
   1012                null
   1013              );
   1014              if (!successor) {
   1015                throw new ExtensionError("Invalid successorTabId");
   1016              }
   1017              // This also ensures "privateness" matches.
   1018              if (successor.ownerDocument !== nativeTab.ownerDocument) {
   1019                throw new ExtensionError(
   1020                  "Successor tab must be in the same window as the tab being updated"
   1021                );
   1022              }
   1023            }
   1024            tabbrowser.setSuccessor(nativeTab, successor);
   1025          }
   1026 
   1027          return tabManager.convert(nativeTab);
   1028        },
   1029 
   1030        async reload(tabId, reloadProperties) {
   1031          let nativeTab = getTabOrActive(tabId);
   1032 
   1033          let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
   1034          if (reloadProperties && reloadProperties.bypassCache) {
   1035            flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
   1036          }
   1037          nativeTab.linkedBrowser.reloadWithFlags(flags);
   1038        },
   1039 
   1040        async warmup(tabId) {
   1041          let nativeTab = tabTracker.getTab(tabId);
   1042          if (!tabManager.canAccessTab(nativeTab)) {
   1043            throw new ExtensionError(`Invalid tab ID: ${tabId}`);
   1044          }
   1045          let tabbrowser = nativeTab.ownerGlobal.gBrowser;
   1046          tabbrowser.warmupTab(nativeTab);
   1047        },
   1048 
   1049        async get(tabId) {
   1050          return tabManager.get(tabId).convert();
   1051        },
   1052 
   1053        getCurrent() {
   1054          let tabData;
   1055          if (context.tabId) {
   1056            tabData = tabManager.get(context.tabId).convert();
   1057          }
   1058          return Promise.resolve(tabData);
   1059        },
   1060 
   1061        async query(queryInfo) {
   1062          return Array.from(tabManager.query(queryInfo, context), tab =>
   1063            tab.convert()
   1064          );
   1065        },
   1066 
   1067        async captureTab(tabId, options) {
   1068          let nativeTab = getTabOrActive(tabId);
   1069          await tabListener.awaitTabReady(nativeTab);
   1070 
   1071          let browser = nativeTab.linkedBrowser;
   1072          let window = browser.ownerGlobal;
   1073          let zoom = window.ZoomManager.getZoomForBrowser(browser);
   1074 
   1075          let tab = tabManager.wrapTab(nativeTab);
   1076          return tab.capture(context, zoom, options);
   1077        },
   1078 
   1079        async captureVisibleTab(windowId, options) {
   1080          let window =
   1081            windowId == null
   1082              ? windowTracker.getTopWindow(context)
   1083              : windowTracker.getWindow(windowId, context);
   1084 
   1085          let tab = tabManager.getWrapper(window.gBrowser.selectedTab);
   1086          if (
   1087            !extension.hasPermission("<all_urls>") &&
   1088            !tab.hasActiveTabPermission
   1089          ) {
   1090            throw new ExtensionError("Missing activeTab permission");
   1091          }
   1092          await tabListener.awaitTabReady(tab.nativeTab);
   1093 
   1094          let zoom = window.ZoomManager.getZoomForBrowser(
   1095            tab.nativeTab.linkedBrowser
   1096          );
   1097          return tab.capture(context, zoom, options);
   1098        },
   1099 
   1100        async detectLanguage(tabId) {
   1101          let tab = await promiseTabWhenReady(tabId);
   1102          let results = await tab.queryContent("DetectLanguage", {});
   1103          return results[0];
   1104        },
   1105 
   1106        async executeScript(tabId, details) {
   1107          let tab = await promiseTabWhenReady(tabId);
   1108          return tab.executeScript(context, details);
   1109        },
   1110 
   1111        async insertCSS(tabId, details) {
   1112          let tab = await promiseTabWhenReady(tabId);
   1113          return tab.insertCSS(context, details);
   1114        },
   1115 
   1116        async removeCSS(tabId, details) {
   1117          let tab = await promiseTabWhenReady(tabId);
   1118          return tab.removeCSS(context, details);
   1119        },
   1120 
   1121        async move(tabIds, moveProperties) {
   1122          let tabsMoved = [];
   1123          if (!Array.isArray(tabIds)) {
   1124            tabIds = [tabIds];
   1125          }
   1126 
   1127          let destinationWindow = null;
   1128          if (moveProperties.windowId !== null) {
   1129            destinationWindow = windowTracker.getWindow(
   1130              moveProperties.windowId,
   1131              context
   1132            );
   1133            // Fail on an invalid window.
   1134            if (!destinationWindow) {
   1135              return Promise.reject({
   1136                message: `Invalid window ID: ${moveProperties.windowId}`,
   1137              });
   1138            }
   1139          }
   1140 
   1141          /*
   1142            Indexes are maintained on a per window basis so that a call to
   1143              move([tabA, tabB], {index: 0})
   1144                -> tabA to 0, tabB to 1 if tabA and tabB are in the same window
   1145              move([tabA, tabB], {index: 0})
   1146                -> tabA to 0, tabB to 0 if tabA and tabB are in different windows
   1147          */
   1148          let lastInsertionMap = new Map();
   1149 
   1150          for (let nativeTab of getNativeTabsFromIDArray(tabIds)) {
   1151            // If the window is not specified, use the window from the tab.
   1152            let window = destinationWindow || nativeTab.ownerGlobal;
   1153            let isSameWindow = nativeTab.ownerGlobal == window;
   1154            let gBrowser = window.gBrowser;
   1155 
   1156            // If we are not moving the tab to a different window, and the window
   1157            // only has one tab, do nothing.
   1158            if (isSameWindow && gBrowser.tabs.length === 1) {
   1159              lastInsertionMap.set(window, 0);
   1160              continue;
   1161            }
   1162            // If moving between windows, be sure privacy matches.  While gBrowser
   1163            // prevents this, we want to silently ignore it.
   1164            if (
   1165              !isSameWindow &&
   1166              PrivateBrowsingUtils.isBrowserPrivate(gBrowser) !=
   1167                PrivateBrowsingUtils.isBrowserPrivate(
   1168                  nativeTab.ownerGlobal.gBrowser
   1169                )
   1170            ) {
   1171              continue;
   1172            }
   1173 
   1174            let insertionPoint;
   1175            let lastInsertion = lastInsertionMap.get(window);
   1176            if (lastInsertion == null) {
   1177              insertionPoint = moveProperties.index;
   1178              let maxIndex = gBrowser.tabs.length - (isSameWindow ? 1 : 0);
   1179              if (insertionPoint == -1) {
   1180                // If the index is -1 it should go to the end of the tabs.
   1181                insertionPoint = maxIndex;
   1182              } else {
   1183                insertionPoint = Math.min(insertionPoint, maxIndex);
   1184              }
   1185            } else if (isSameWindow && nativeTab._tPos <= lastInsertion) {
   1186              // lastInsertion is the current index of the last inserted tab.
   1187              // insertionPoint is the desired index of the current tab *after* moving it.
   1188              // When the tab is moved, the last inserted tab will no longer be at index
   1189              // lastInsertion, but (lastInsertion - 1). To position the tabs adjacent to
   1190              // each other, the tab should therefore be at index (lastInsertion - 1 + 1).
   1191              insertionPoint = lastInsertion;
   1192            } else {
   1193              // In this case the last inserted tab will stay at index lastInsertion,
   1194              // so we should move the current tab to index (lastInsertion + 1).
   1195              insertionPoint = lastInsertion + 1;
   1196            }
   1197 
   1198            // We can only move pinned tabs to a point within, or just after,
   1199            // the current set of pinned tabs. Unpinned tabs, likewise, can only
   1200            // be moved to a position after the current set of pinned tabs.
   1201            // Attempts to move a tab to an illegal position are ignored.
   1202            let numPinned = gBrowser.pinnedTabCount;
   1203            let ok = nativeTab.pinned
   1204              ? insertionPoint <= numPinned
   1205              : insertionPoint >= numPinned;
   1206            if (!ok) {
   1207              continue;
   1208            }
   1209 
   1210            if (isSameWindow) {
   1211              // If the window we are moving is the same, just move the tab.
   1212              gBrowser.moveTabTo(nativeTab, { tabIndex: insertionPoint });
   1213            } else {
   1214              // If the window we are moving the tab in is different, then move the tab
   1215              // to the new window.
   1216              nativeTab = gBrowser.adoptTab(nativeTab, {
   1217                tabIndex: insertionPoint,
   1218              });
   1219            }
   1220            lastInsertionMap.set(window, nativeTab._tPos);
   1221            tabsMoved.push(nativeTab);
   1222          }
   1223 
   1224          return tabsMoved.map(nativeTab => tabManager.convert(nativeTab));
   1225        },
   1226 
   1227        duplicate(tabId, duplicateProperties) {
   1228          const { active, index: tabIndex } = duplicateProperties || {};
   1229          const inBackground = active === undefined ? false : !active;
   1230 
   1231          // Schema requires tab id.
   1232          let nativeTab = getTabOrActive(tabId);
   1233 
   1234          let gBrowser = nativeTab.ownerGlobal.gBrowser;
   1235          let newTab = gBrowser.duplicateTab(nativeTab, true, {
   1236            inBackground,
   1237            tabIndex,
   1238          });
   1239 
   1240          tabListener.blockTabUntilRestored(newTab);
   1241          return new Promise(resolve => {
   1242            // Use SSTabRestoring to ensure that the tab's URL is ready before
   1243            // resolving the promise.
   1244            newTab.addEventListener(
   1245              "SSTabRestoring",
   1246              () => resolve(tabManager.convert(newTab)),
   1247              { once: true }
   1248            );
   1249          });
   1250        },
   1251 
   1252        getZoom(tabId) {
   1253          let nativeTab = getTabOrActive(tabId);
   1254 
   1255          let { ZoomManager } = nativeTab.ownerGlobal;
   1256          let zoom = ZoomManager.getZoomForBrowser(nativeTab.linkedBrowser);
   1257 
   1258          return Promise.resolve(zoom);
   1259        },
   1260 
   1261        setZoom(tabId, zoom) {
   1262          let nativeTab = getTabOrActive(tabId);
   1263 
   1264          let { FullZoom, ZoomManager } = nativeTab.ownerGlobal;
   1265 
   1266          if (zoom === 0) {
   1267            // A value of zero means use the default zoom factor.
   1268            return FullZoom.reset(nativeTab.linkedBrowser);
   1269          } else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) {
   1270            FullZoom.setZoom(zoom, nativeTab.linkedBrowser);
   1271          } else {
   1272            return Promise.reject({
   1273              message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
   1274            });
   1275          }
   1276 
   1277          return Promise.resolve();
   1278        },
   1279 
   1280        async getZoomSettings(tabId) {
   1281          let nativeTab = getTabOrActive(tabId);
   1282 
   1283          let { FullZoom, ZoomUI } = nativeTab.ownerGlobal;
   1284 
   1285          return {
   1286            mode: "automatic",
   1287            scope: FullZoom.siteSpecific ? "per-origin" : "per-tab",
   1288            defaultZoomFactor: await ZoomUI.getGlobalValue(),
   1289          };
   1290        },
   1291 
   1292        async setZoomSettings(tabId, settings) {
   1293          let nativeTab = getTabOrActive(tabId);
   1294 
   1295          let currentSettings = await this.getZoomSettings(
   1296            tabTracker.getId(nativeTab)
   1297          );
   1298 
   1299          if (
   1300            !Object.keys(settings).every(
   1301              key => settings[key] === currentSettings[key]
   1302            )
   1303          ) {
   1304            throw new ExtensionError(
   1305              `Unsupported zoom settings: ${JSON.stringify(settings)}`
   1306            );
   1307          }
   1308        },
   1309 
   1310        onZoomChange: new EventManager({
   1311          context,
   1312          name: "tabs.onZoomChange",
   1313          register: fire => {
   1314            let getZoomLevel = browser => {
   1315              let { ZoomManager } = browser.ownerGlobal;
   1316 
   1317              return ZoomManager.getZoomForBrowser(browser);
   1318            };
   1319 
   1320            // Stores the last known zoom level for each tab's browser.
   1321            // WeakMap[<browser> -> number]
   1322            let zoomLevels = new WeakMap();
   1323 
   1324            // Store the zoom level for all existing tabs.
   1325            for (let window of windowTracker.browserWindows()) {
   1326              if (!context.canAccessWindow(window)) {
   1327                continue;
   1328              }
   1329              for (let nativeTab of window.gBrowser.tabs) {
   1330                let browser = nativeTab.linkedBrowser;
   1331                zoomLevels.set(browser, getZoomLevel(browser));
   1332              }
   1333            }
   1334 
   1335            let tabCreated = (eventName, event) => {
   1336              let browser = event.nativeTab.linkedBrowser;
   1337              if (!event.isPrivate || context.privateBrowsingAllowed) {
   1338                zoomLevels.set(browser, getZoomLevel(browser));
   1339              }
   1340            };
   1341 
   1342            let zoomListener = async event => {
   1343              let browser = event.originalTarget;
   1344 
   1345              // For non-remote browsers, this event is dispatched on the document
   1346              // rather than on the <browser>.  But either way we have a node here.
   1347              if (browser.nodeType == browser.DOCUMENT_NODE) {
   1348                browser = browser.docShell.chromeEventHandler;
   1349              }
   1350 
   1351              if (!context.canAccessWindow(browser.ownerGlobal)) {
   1352                return;
   1353              }
   1354 
   1355              let { gBrowser } = browser.ownerGlobal;
   1356              let nativeTab = gBrowser.getTabForBrowser(browser);
   1357              if (!nativeTab) {
   1358                // We only care about zoom events in the top-level browser of a tab.
   1359                return;
   1360              }
   1361 
   1362              let oldZoomFactor = zoomLevels.get(browser);
   1363              let newZoomFactor = getZoomLevel(browser);
   1364 
   1365              if (oldZoomFactor != newZoomFactor) {
   1366                zoomLevels.set(browser, newZoomFactor);
   1367 
   1368                let tabId = tabTracker.getId(nativeTab);
   1369                fire.async({
   1370                  tabId,
   1371                  oldZoomFactor,
   1372                  newZoomFactor,
   1373                  zoomSettings: await tabsApi.tabs.getZoomSettings(tabId),
   1374                });
   1375              }
   1376            };
   1377 
   1378            tabTracker.on("tab-attached", tabCreated);
   1379            tabTracker.on("tab-created", tabCreated);
   1380 
   1381            windowTracker.addListener("FullZoomChange", zoomListener);
   1382            windowTracker.addListener("TextZoomChange", zoomListener);
   1383            return () => {
   1384              tabTracker.off("tab-attached", tabCreated);
   1385              tabTracker.off("tab-created", tabCreated);
   1386 
   1387              windowTracker.removeListener("FullZoomChange", zoomListener);
   1388              windowTracker.removeListener("TextZoomChange", zoomListener);
   1389            };
   1390          },
   1391        }).api(),
   1392 
   1393        print() {
   1394          let activeTab = getTabOrActive(null);
   1395          let { PrintUtils } = activeTab.ownerGlobal;
   1396          PrintUtils.startPrintWindow(activeTab.linkedBrowser.browsingContext);
   1397        },
   1398 
   1399        // Legacy API
   1400        printPreview() {
   1401          return Promise.resolve(this.print());
   1402        },
   1403 
   1404        saveAsPDF(pageSettings) {
   1405          let activeTab = getTabOrActive(null);
   1406          let picker = Cc["@mozilla.org/filepicker;1"].createInstance(
   1407            Ci.nsIFilePicker
   1408          );
   1409          let title = strBundle.GetStringFromName(
   1410            "saveaspdf.saveasdialog.title"
   1411          );
   1412          let filename;
   1413          if (
   1414            pageSettings.toFileName !== null &&
   1415            pageSettings.toFileName != ""
   1416          ) {
   1417            filename = pageSettings.toFileName;
   1418          } else if (activeTab.linkedBrowser.contentTitle != "") {
   1419            filename = activeTab.linkedBrowser.contentTitle;
   1420          } else {
   1421            let url = new URL(activeTab.linkedBrowser.currentURI.spec);
   1422            let path = decodeURIComponent(url.pathname);
   1423            path = path.replace(/\/$/, "");
   1424            filename = path.split("/").pop();
   1425            if (filename == "") {
   1426              filename = url.hostname;
   1427            }
   1428          }
   1429          filename = DownloadPaths.sanitize(filename);
   1430 
   1431          picker.init(
   1432            activeTab.ownerGlobal.browsingContext,
   1433            title,
   1434            Ci.nsIFilePicker.modeSave
   1435          );
   1436          picker.appendFilter("PDF", "*.pdf");
   1437          picker.defaultExtension = "pdf";
   1438          picker.defaultString = filename;
   1439 
   1440          return new Promise(resolve => {
   1441            picker.open(function (retval) {
   1442              if (retval == 0 || retval == 2) {
   1443                // OK clicked (retval == 0) or replace confirmed (retval == 2)
   1444 
   1445                // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file),
   1446                // the print progress listener is never called. This workaround ensures that a correct status is always returned.
   1447                try {
   1448                  let fstream = Cc[
   1449                    "@mozilla.org/network/file-output-stream;1"
   1450                  ].createInstance(Ci.nsIFileOutputStream);
   1451                  fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
   1452                  fstream.close();
   1453                } catch (e) {
   1454                  resolve(retval == 0 ? "not_saved" : "not_replaced");
   1455                  return;
   1456                }
   1457 
   1458                let psService = Cc[
   1459                  "@mozilla.org/gfx/printsettings-service;1"
   1460                ].getService(Ci.nsIPrintSettingsService);
   1461                let printSettings = psService.createNewPrintSettings();
   1462 
   1463                printSettings.printerName = "";
   1464                printSettings.isInitializedFromPrinter = true;
   1465                printSettings.isInitializedFromPrefs = true;
   1466 
   1467                printSettings.outputDestination =
   1468                  Ci.nsIPrintSettings.kOutputDestinationFile;
   1469                printSettings.toFileName = picker.file.path;
   1470 
   1471                printSettings.printSilent = true;
   1472 
   1473                printSettings.outputFormat =
   1474                  Ci.nsIPrintSettings.kOutputFormatPDF;
   1475 
   1476                if (pageSettings.paperSizeUnit !== null) {
   1477                  printSettings.paperSizeUnit = pageSettings.paperSizeUnit;
   1478                }
   1479                if (pageSettings.paperWidth !== null) {
   1480                  printSettings.paperWidth = pageSettings.paperWidth;
   1481                }
   1482                if (pageSettings.paperHeight !== null) {
   1483                  printSettings.paperHeight = pageSettings.paperHeight;
   1484                }
   1485                if (pageSettings.orientation !== null) {
   1486                  printSettings.orientation = pageSettings.orientation;
   1487                }
   1488                if (pageSettings.scaling !== null) {
   1489                  printSettings.scaling = pageSettings.scaling;
   1490                }
   1491                if (pageSettings.shrinkToFit !== null) {
   1492                  printSettings.shrinkToFit = pageSettings.shrinkToFit;
   1493                }
   1494                if (pageSettings.showBackgroundColors !== null) {
   1495                  printSettings.printBGColors =
   1496                    pageSettings.showBackgroundColors;
   1497                }
   1498                if (pageSettings.showBackgroundImages !== null) {
   1499                  printSettings.printBGImages =
   1500                    pageSettings.showBackgroundImages;
   1501                }
   1502                if (pageSettings.edgeLeft !== null) {
   1503                  printSettings.edgeLeft = pageSettings.edgeLeft;
   1504                }
   1505                if (pageSettings.edgeRight !== null) {
   1506                  printSettings.edgeRight = pageSettings.edgeRight;
   1507                }
   1508                if (pageSettings.edgeTop !== null) {
   1509                  printSettings.edgeTop = pageSettings.edgeTop;
   1510                }
   1511                if (pageSettings.edgeBottom !== null) {
   1512                  printSettings.edgeBottom = pageSettings.edgeBottom;
   1513                }
   1514                if (pageSettings.marginLeft !== null) {
   1515                  printSettings.marginLeft = pageSettings.marginLeft;
   1516                }
   1517                if (pageSettings.marginRight !== null) {
   1518                  printSettings.marginRight = pageSettings.marginRight;
   1519                }
   1520                if (pageSettings.marginTop !== null) {
   1521                  printSettings.marginTop = pageSettings.marginTop;
   1522                }
   1523                if (pageSettings.marginBottom !== null) {
   1524                  printSettings.marginBottom = pageSettings.marginBottom;
   1525                }
   1526                if (pageSettings.headerLeft !== null) {
   1527                  printSettings.headerStrLeft = pageSettings.headerLeft;
   1528                }
   1529                if (pageSettings.headerCenter !== null) {
   1530                  printSettings.headerStrCenter = pageSettings.headerCenter;
   1531                }
   1532                if (pageSettings.headerRight !== null) {
   1533                  printSettings.headerStrRight = pageSettings.headerRight;
   1534                }
   1535                if (pageSettings.footerLeft !== null) {
   1536                  printSettings.footerStrLeft = pageSettings.footerLeft;
   1537                }
   1538                if (pageSettings.footerCenter !== null) {
   1539                  printSettings.footerStrCenter = pageSettings.footerCenter;
   1540                }
   1541                if (pageSettings.footerRight !== null) {
   1542                  printSettings.footerStrRight = pageSettings.footerRight;
   1543                }
   1544 
   1545                activeTab.linkedBrowser.browsingContext
   1546                  .print(printSettings)
   1547                  .then(() => resolve(retval == 0 ? "saved" : "replaced"))
   1548                  .catch(() =>
   1549                    resolve(retval == 0 ? "not_saved" : "not_replaced")
   1550                  );
   1551              } else {
   1552                // Cancel clicked (retval == 1)
   1553                resolve("canceled");
   1554              }
   1555            });
   1556          });
   1557        },
   1558 
   1559        async toggleReaderMode(tabId) {
   1560          let tab = await promiseTabWhenReady(tabId);
   1561          if (!tab.isInReaderMode && !tab.isArticle) {
   1562            throw new ExtensionError(
   1563              "The specified tab cannot be placed into reader mode."
   1564            );
   1565          }
   1566          let nativeTab = getTabOrActive(tabId);
   1567 
   1568          nativeTab.linkedBrowser.sendMessageToActor(
   1569            "Reader:ToggleReaderMode",
   1570            {},
   1571            "AboutReader"
   1572          );
   1573        },
   1574 
   1575        moveInSuccession(tabIds, tabId, options) {
   1576          const { insert, append } = options || {};
   1577          const tabIdSet = new Set(tabIds);
   1578          if (tabIdSet.size !== tabIds.length) {
   1579            throw new ExtensionError(
   1580              "IDs must not occur more than once in tabIds"
   1581            );
   1582          }
   1583          if ((append || insert) && tabIdSet.has(tabId)) {
   1584            throw new ExtensionError(
   1585              "Value of tabId must not occur in tabIds if append or insert is true"
   1586            );
   1587          }
   1588 
   1589          const referenceTab = tabTracker.getTab(tabId, null);
   1590          let referenceWindow = referenceTab && referenceTab.ownerGlobal;
   1591          if (referenceWindow && !context.canAccessWindow(referenceWindow)) {
   1592            throw new ExtensionError(`Invalid tab ID: ${tabId}`);
   1593          }
   1594          let previousTab, lastSuccessor;
   1595          if (append) {
   1596            previousTab = referenceTab;
   1597            lastSuccessor =
   1598              (insert && referenceTab && referenceTab.successor) || null;
   1599          } else {
   1600            lastSuccessor = referenceTab;
   1601          }
   1602 
   1603          let firstTab;
   1604          for (const tabId of tabIds) {
   1605            const tab = tabTracker.getTab(tabId, null);
   1606            if (tab === null) {
   1607              continue;
   1608            }
   1609            if (!tabManager.canAccessTab(tab)) {
   1610              throw new ExtensionError(`Invalid tab ID: ${tabId}`);
   1611            }
   1612            if (referenceWindow === null) {
   1613              referenceWindow = tab.ownerGlobal;
   1614            } else if (tab.ownerGlobal !== referenceWindow) {
   1615              continue;
   1616            }
   1617            referenceWindow.gBrowser.replaceInSuccession(tab, tab.successor);
   1618            if (append && tab === lastSuccessor) {
   1619              lastSuccessor = tab.successor;
   1620            }
   1621            if (previousTab) {
   1622              referenceWindow.gBrowser.setSuccessor(previousTab, tab);
   1623            } else {
   1624              firstTab = tab;
   1625            }
   1626            previousTab = tab;
   1627          }
   1628 
   1629          if (previousTab) {
   1630            if (!append && insert && lastSuccessor !== null) {
   1631              referenceWindow.gBrowser.replaceInSuccession(
   1632                lastSuccessor,
   1633                firstTab
   1634              );
   1635            }
   1636            referenceWindow.gBrowser.setSuccessor(previousTab, lastSuccessor);
   1637          }
   1638        },
   1639 
   1640        show(tabIds) {
   1641          for (let tab of getNativeTabsFromIDArray(tabIds)) {
   1642            if (tab.ownerGlobal) {
   1643              tab.ownerGlobal.gBrowser.showTab(tab);
   1644            }
   1645          }
   1646        },
   1647 
   1648        hide(tabIds) {
   1649          let hidden = [];
   1650          for (let tab of getNativeTabsFromIDArray(tabIds)) {
   1651            if (tab.ownerGlobal && !tab.hidden) {
   1652              tab.ownerGlobal.gBrowser.hideTab(tab, extension.id);
   1653              if (tab.hidden) {
   1654                hidden.push(tabTracker.getId(tab));
   1655              }
   1656            }
   1657          }
   1658          if (hidden.length) {
   1659            let win = Services.wm.getMostRecentWindow("navigator:browser");
   1660 
   1661            // Before showing the hidden tabs warning,
   1662            // move alltabs-button to somewhere visible if it isn't already.
   1663            if (!CustomizableUI.widgetIsLikelyVisible("alltabs-button", win)) {
   1664              CustomizableUI.addWidgetToArea(
   1665                "alltabs-button",
   1666                CustomizableUI.verticalTabsEnabled
   1667                  ? CustomizableUI.AREA_NAVBAR
   1668                  : CustomizableUI.AREA_TABSTRIP
   1669              );
   1670            }
   1671            tabHidePopup.open(win, extension.id);
   1672          }
   1673          return hidden;
   1674        },
   1675 
   1676        highlight(highlightInfo) {
   1677          let { windowId, tabs, populate } = highlightInfo;
   1678          if (windowId == null) {
   1679            windowId = Window.WINDOW_ID_CURRENT;
   1680          }
   1681          let window = windowTracker.getWindow(windowId, context);
   1682          if (!context.canAccessWindow(window)) {
   1683            throw new ExtensionError(`Invalid window ID: ${windowId}`);
   1684          }
   1685 
   1686          if (!Array.isArray(tabs)) {
   1687            tabs = [tabs];
   1688          } else if (!tabs.length) {
   1689            throw new ExtensionError("No highlighted tab.");
   1690          }
   1691          window.gBrowser.selectedTabs = tabs.map(tabIndex => {
   1692            let tab = window.gBrowser.tabs[tabIndex];
   1693            if (!tab || !tabManager.canAccessTab(tab)) {
   1694              throw new ExtensionError("No tab at index: " + tabIndex);
   1695            }
   1696            return tab;
   1697          });
   1698          return windowManager.convert(window, { populate });
   1699        },
   1700 
   1701        goForward(tabId) {
   1702          let nativeTab = getTabOrActive(tabId);
   1703          nativeTab.linkedBrowser.goForward(false);
   1704        },
   1705 
   1706        goBack(tabId) {
   1707          let nativeTab = getTabOrActive(tabId);
   1708          nativeTab.linkedBrowser.goBack(false);
   1709        },
   1710 
   1711        group(options) {
   1712          let nativeTabs = getNativeTabsFromIDArray(options.tabIds);
   1713          let window = windowTracker.getWindow(
   1714            options.createProperties?.windowId ?? Window.WINDOW_ID_CURRENT,
   1715            context
   1716          );
   1717          const windowIsPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
   1718          for (const nativeTab of nativeTabs) {
   1719            if (
   1720              PrivateBrowsingUtils.isWindowPrivate(nativeTab.ownerGlobal) !==
   1721              windowIsPrivate
   1722            ) {
   1723              if (windowIsPrivate) {
   1724                throw new ExtensionError(
   1725                  "Cannot move non-private tabs to private window"
   1726                );
   1727              }
   1728              throw new ExtensionError(
   1729                "Cannot move private tabs to non-private window"
   1730              );
   1731            }
   1732          }
   1733          function unpinTabsBeforeGrouping() {
   1734            for (const nativeTab of nativeTabs) {
   1735              nativeTab.ownerGlobal.gBrowser.unpinTab(nativeTab);
   1736            }
   1737          }
   1738          let group;
   1739          if (options.groupId == null) {
   1740            // By default, tabs are appended after all other tabs in the
   1741            // window. But if we are grouping tabs within a window, ideally the
   1742            // tabs should just be grouped without moving positions.
   1743            // TODO bug 1939214: when addTabGroup inserts tabs at the front as
   1744            // needed (instead of always appending), simplify this logic.
   1745            const tabInWin = nativeTabs.find(t => t.ownerGlobal === window);
   1746            let insertBefore = tabInWin;
   1747            if (tabInWin?.group) {
   1748              if (tabInWin.group.tabs[0] === tabInWin) {
   1749                // When tabInWin is at the front of a tab group, insert before
   1750                // the tab group (instead of after it).
   1751                insertBefore = tabInWin.group;
   1752              } else {
   1753                insertBefore = insertBefore.group.nextElementSibling;
   1754              }
   1755            }
   1756            unpinTabsBeforeGrouping();
   1757            group = window.gBrowser.addTabGroup(nativeTabs, { insertBefore });
   1758            // Note: group is never null, because the only condition for which
   1759            // it could be null is when all tabs are pinned, and we are already
   1760            // explicitly unpinning them before moving.
   1761          } else {
   1762            group = window.gBrowser.getTabGroupById(
   1763              getInternalTabGroupIdForExtTabGroupId(options.groupId)
   1764            );
   1765            if (!group) {
   1766              throw new ExtensionError(`No group with id: ${options.groupId}`);
   1767            }
   1768            unpinTabsBeforeGrouping();
   1769            // When moving tabs within the same window, try to maintain their
   1770            // relative positions.
   1771            const tabsBefore = [];
   1772            const tabsAfter = [];
   1773            const firstTabInGroup = group.tabs[0];
   1774            for (const nativeTab of nativeTabs) {
   1775              if (
   1776                nativeTab.ownerGlobal === window &&
   1777                nativeTab._tPos < firstTabInGroup._tPos
   1778              ) {
   1779                tabsBefore.push(nativeTab);
   1780              } else {
   1781                tabsAfter.push(nativeTab);
   1782              }
   1783            }
   1784            if (tabsBefore.length) {
   1785              window.gBrowser.moveTabsBefore(tabsBefore, firstTabInGroup);
   1786            }
   1787            if (tabsAfter.length) {
   1788              group.addTabs(tabsAfter);
   1789            }
   1790          }
   1791          return getExtTabGroupIdForInternalTabGroupId(group.id);
   1792        },
   1793 
   1794        ungroup(tabIds) {
   1795          const nativeTabs = getNativeTabsFromIDArray(tabIds);
   1796          // Ungroup tabs while trying to preserve the relative order of tabs
   1797          // within the tab strip as much as possible. This is not always
   1798          // possible, e.g. when a tab group is only partially ungrouped.
   1799          const ungroupOrder = new DefaultMap(() => []);
   1800          for (const nativeTab of nativeTabs) {
   1801            if (nativeTab.group) {
   1802              ungroupOrder.get(nativeTab.group).push(nativeTab);
   1803            }
   1804          }
   1805          for (const [group, tabs] of ungroupOrder) {
   1806            // Preserve original order of ungrouped tabs.
   1807            tabs.sort((a, b) => a._tPos - b._tPos);
   1808            if (tabs[0] === tabs[0].group.tabs[0]) {
   1809              // The tab is the front of the tab group, so insert before
   1810              // current tab group to preserve order.
   1811              tabs[0].ownerGlobal.gBrowser.moveTabsBefore(tabs, group);
   1812            } else {
   1813              tabs[0].ownerGlobal.gBrowser.moveTabsAfter(tabs, group);
   1814            }
   1815          }
   1816        },
   1817      },
   1818    };
   1819    return tabsApi;
   1820  }
   1821 };