tor-browser

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

BrowserWindowTracker.sys.mjs (15935B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /*
      6 * This module tracks each browser window and informs network module
      7 * the current selected tab's content outer window ID.
      8 */
      9 
     10 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     11 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     12 
     13 const lazy = {};
     14 
     15 // Lazy getters
     16 
     17 XPCOMUtils.defineLazyServiceGetters(lazy, {
     18  BrowserHandler: ["@mozilla.org/browser/clh;1", Ci.nsIBrowserHandler],
     19 });
     20 
     21 ChromeUtils.defineESModuleGetters(lazy, {
     22  AIWindow:
     23    "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
     24  HomePage: "resource:///modules/HomePage.sys.mjs",
     25  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     26 });
     27 
     28 XPCOMUtils.defineLazyPreferenceGetter(
     29  lazy,
     30  "gPreferWindowsOnCurrentVirtualDesktop",
     31  "widget.prefer_windows_on_current_virtual_desktop"
     32 );
     33 
     34 // Constants
     35 const TAB_EVENTS = ["TabBrowserInserted", "TabSelect"];
     36 const WINDOW_EVENTS = ["activate", "unload"];
     37 const DEBUG = false;
     38 
     39 // Variables
     40 let _lastCurrentBrowserId = 0;
     41 let _trackedWindows = [];
     42 
     43 // Global methods
     44 function debug(s) {
     45  if (DEBUG) {
     46    dump("-*- UpdateBrowserIDHelper: " + s + "\n");
     47  }
     48 }
     49 
     50 function _updateCurrentBrowserId(browser) {
     51  if (
     52    !browser.browserId ||
     53    browser.browserId === _lastCurrentBrowserId ||
     54    browser.ownerGlobal != _trackedWindows[0]
     55  ) {
     56    return;
     57  }
     58 
     59  // Guard on DEBUG here because materializing a long data URI into
     60  // a JS string for concatenation is not free.
     61  if (DEBUG) {
     62    debug(
     63      `Current window uri=${browser.currentURI?.spec} browser id=${browser.browserId}`
     64    );
     65  }
     66 
     67  _lastCurrentBrowserId = browser.browserId;
     68  let idWrapper = Cc["@mozilla.org/supports-PRUint64;1"].createInstance(
     69    Ci.nsISupportsPRUint64
     70  );
     71  idWrapper.data = _lastCurrentBrowserId;
     72  Services.obs.notifyObservers(idWrapper, "net:current-browser-id");
     73 }
     74 
     75 function _handleEvent(event) {
     76  switch (event.type) {
     77    case "TabBrowserInserted":
     78      if (
     79        event.target.ownerGlobal.gBrowser.selectedBrowser ===
     80        event.target.linkedBrowser
     81      ) {
     82        _updateCurrentBrowserId(event.target.linkedBrowser);
     83      }
     84      break;
     85    case "TabSelect":
     86      _updateCurrentBrowserId(event.target.linkedBrowser);
     87      break;
     88    case "activate":
     89      WindowHelper.onActivate(event.target);
     90      break;
     91    case "unload":
     92      WindowHelper.removeWindow(event.currentTarget);
     93      break;
     94  }
     95 }
     96 
     97 function _trackWindowOrder(window) {
     98  if (window.windowState == window.STATE_MINIMIZED) {
     99    let firstMinimizedWindow = _trackedWindows.findIndex(
    100      w => w.windowState == w.STATE_MINIMIZED
    101    );
    102    if (firstMinimizedWindow == -1) {
    103      firstMinimizedWindow = _trackedWindows.length;
    104    }
    105    _trackedWindows.splice(firstMinimizedWindow, 0, window);
    106  } else {
    107    _trackedWindows.unshift(window);
    108  }
    109 }
    110 
    111 function _untrackWindowOrder(window) {
    112  let idx = _trackedWindows.indexOf(window);
    113  if (idx >= 0) {
    114    _trackedWindows.splice(idx, 1);
    115  }
    116 }
    117 
    118 function topicObserved(observeTopic, checkFn) {
    119  return new Promise((resolve, reject) => {
    120    function observer(subject, topic, data) {
    121      try {
    122        if (checkFn && !checkFn(subject, data)) {
    123          return;
    124        }
    125        Services.obs.removeObserver(observer, topic);
    126        checkFn = null;
    127        resolve([subject, data]);
    128      } catch (ex) {
    129        Services.obs.removeObserver(observer, topic);
    130        checkFn = null;
    131        reject(ex);
    132      }
    133    }
    134    Services.obs.addObserver(observer, observeTopic);
    135  });
    136 }
    137 
    138 // Methods that impact a window. Put into single object for organization.
    139 var WindowHelper = {
    140  addWindow(window) {
    141    // Add event listeners
    142    TAB_EVENTS.forEach(function (event) {
    143      window.gBrowser.tabContainer.addEventListener(event, _handleEvent);
    144    });
    145    WINDOW_EVENTS.forEach(function (event) {
    146      window.addEventListener(event, _handleEvent);
    147    });
    148 
    149    _trackWindowOrder(window);
    150 
    151    // Update the selected tab's content outer window ID.
    152    _updateCurrentBrowserId(window.gBrowser.selectedBrowser);
    153  },
    154 
    155  removeWindow(window) {
    156    _untrackWindowOrder(window);
    157 
    158    // Remove the event listeners
    159    TAB_EVENTS.forEach(function (event) {
    160      window.gBrowser.tabContainer.removeEventListener(event, _handleEvent);
    161    });
    162    WINDOW_EVENTS.forEach(function (event) {
    163      window.removeEventListener(event, _handleEvent);
    164    });
    165  },
    166 
    167  onActivate(window) {
    168    // If this window was the last focused window, we don't need to do anything
    169    if (window == _trackedWindows[0]) {
    170      return;
    171    }
    172 
    173    _untrackWindowOrder(window);
    174    _trackWindowOrder(window);
    175 
    176    _updateCurrentBrowserId(window.gBrowser.selectedBrowser);
    177  },
    178 };
    179 
    180 export const BrowserWindowTracker = {
    181  pendingWindows: new Map(),
    182 
    183  /**
    184   * Get the most recent browser window.
    185   * Note that with the default options this may return null on Windows if
    186   * there are no open windows in the current virtual desktop. To prevent this,
    187   * set `options.allowFromInactiveWorkspace` to true.
    188   *
    189   * @param {object} options - An object accepting the arguments for the search.
    190   * @param {boolean} [options.private]
    191   *   true to only search for private windows.
    192   *   false to restrict the search to non-private windows.
    193   *   If the property is not provided, search for either. If permanent private
    194   *   browsing is enabled this option will be ignored!
    195   * @param {boolean} [options.allowPopups]: true if popup windows are
    196   *   permitted.
    197   * @param {boolean} [options.allowTaskbarTabs] true if taskbar tab windows
    198   *  are permitted.
    199   * @param {boolean} [options.allowFromInactiveWorkspace] true if window is allowed to
    200   *  be from a different virtual desktop (what Windows calls workspaces).
    201   *  Only has an effect on Windows.
    202   *
    203   * @returns {Window | null} The current top/selected window.
    204   *  Can return null on MacOS when there is no open window.
    205   */
    206  getTopWindow(options = {}) {
    207    let cloakedWin = null;
    208    for (let win of _trackedWindows) {
    209      if (
    210        !win.closed &&
    211        (options.allowPopups || win.toolbar.visible) &&
    212        (options.allowTaskbarTabs ||
    213          !win.document.documentElement.hasAttribute("taskbartab")) &&
    214        (!("private" in options) ||
    215          lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
    216          lazy.PrivateBrowsingUtils.isWindowPrivate(win) == options.private)
    217      ) {
    218        // On Windows, windows on a different virtual desktop (what Windows calls
    219        // workspaces) are cloaked.
    220        if (win.isCloaked && lazy.gPreferWindowsOnCurrentVirtualDesktop) {
    221          // Even if we allow from an inactive workspace, prefer windows that
    222          // are not cloaked, so that we don't switch workspaces unnecessarily.
    223          if (!cloakedWin && options.allowFromInactiveWorkspace) {
    224            cloakedWin = win;
    225          }
    226          continue;
    227        }
    228        return win;
    229      }
    230    }
    231    // If we didn't find a non-cloaked window, return the cloaked one if it exists and
    232    // the options allow us to do so.
    233    return cloakedWin;
    234  },
    235 
    236  /**
    237   * Get a window that is in the process of loading. Only supports windows
    238   * opened via the `openWindow` function in this module or that have been
    239   * registered with the `registerOpeningWindow` function.
    240   *
    241   * @param {object} options
    242   *   Options for the search.
    243   * @param {boolean} [options.private]
    244   *   true to restrict the search to private windows only, false to restrict
    245   *   the search to non-private only. Omit the property to search in both
    246   *   groups.
    247   *
    248   * @returns {Promise<Window> | null}
    249   */
    250  getPendingWindow(options = {}) {
    251    for (let pending of this.pendingWindows.values()) {
    252      if (
    253        !("private" in options) ||
    254        lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
    255        pending.isPrivate == options.private
    256      ) {
    257        return pending.deferred.promise;
    258      }
    259    }
    260    return null;
    261  },
    262 
    263  /**
    264   * Registers a browser window that is in the process of opening. Normally it
    265   * would be preferable to use the standard method for opening the window from
    266   * this module.
    267   *
    268   * @param {Window} window
    269   *   The opening window.
    270   * @param {boolean} isPrivate
    271   *   Whether the opening window is a private browsing window.
    272   */
    273  registerOpeningWindow(window, isPrivate) {
    274    let deferred = Promise.withResolvers();
    275 
    276    this.pendingWindows.set(window, {
    277      isPrivate,
    278      deferred,
    279    });
    280 
    281    // Prevent leaks in case the window closes before we track it as an open
    282    // window.
    283    const topic = "browsing-context-discarded";
    284    const observer = aSubject => {
    285      if (window.browsingContext == aSubject) {
    286        let pending = this.pendingWindows.get(window);
    287        if (pending) {
    288          this.pendingWindows.delete(window);
    289          pending.deferred.resolve(window);
    290        }
    291        Services.obs.removeObserver(observer, topic);
    292      }
    293    };
    294    Services.obs.addObserver(observer, topic);
    295  },
    296 
    297  /**
    298   * A standard function for opening a new browser window.
    299   *
    300   * @param {object} [options]
    301   *   Options for the new window.
    302   * @param {Window} [options.openerWindow]
    303   *   An existing browser window to open the new one from.
    304   * @param {boolean} [options.private]
    305   *   True to make the window a private browsing window.
    306   * @param {boolean} [options.aiWindow]
    307   *   True to make the window an AI browsing window.
    308   * @param {string} [options.features]
    309   *   Additional window features to give the new window.
    310   * @param {boolean} [options.all]
    311   *   True if "all" should be included as a window feature. If omitted, defaults
    312   *   to true.
    313   * @param {nsIArray | nsISupportsString} [options.args]
    314   *   Arguments to pass to the new window.
    315   * @param {boolean} [options.remote]
    316   *   A boolean indicating if the window should run remote browser tabs or
    317   *   not. If omitted, the window  will choose the profile default state.
    318   * @param {boolean} [options.fission]
    319   *   A boolean indicating if the window should run with fission enabled or
    320   *   not. If omitted, the window will choose the profile default state.
    321   *
    322   * @returns {Window}
    323   */
    324  openWindow(options = {}) {
    325    let {
    326      openerWindow = undefined,
    327      private: isPrivate = false,
    328      aiWindow = false,
    329      features = undefined,
    330      all = true,
    331      args = null,
    332      remote = undefined,
    333      fission = undefined,
    334    } = options;
    335 
    336    args = lazy.AIWindow.handleAIWindowOptions(options);
    337 
    338    let windowFeatures = "chrome,dialog=no";
    339    if (all) {
    340      windowFeatures += ",all";
    341    }
    342    if (features) {
    343      windowFeatures += `,${features}`;
    344    }
    345    let loadURIString;
    346    if (isPrivate && lazy.PrivateBrowsingUtils.enabled) {
    347      windowFeatures += ",private";
    348      if (
    349        (!args && !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) ||
    350        args?.private === "no-home"
    351      ) {
    352        // Force the new window to load about:privatebrowsing instead of the
    353        // default home page.
    354        loadURIString = "about:privatebrowsing";
    355      }
    356    } else {
    357      windowFeatures += ",non-private";
    358    }
    359    if (aiWindow) {
    360      windowFeatures += ",ai-window";
    361    }
    362    if (!args) {
    363      loadURIString ??= lazy.BrowserHandler.defaultArgs;
    364      args = Cc["@mozilla.org/supports-string;1"].createInstance(
    365        Ci.nsISupportsString
    366      );
    367      args.data = loadURIString;
    368    }
    369 
    370    if (remote) {
    371      windowFeatures += ",remote";
    372    } else if (remote === false) {
    373      windowFeatures += ",non-remote";
    374    }
    375 
    376    if (fission) {
    377      windowFeatures += ",fission";
    378    } else if (fission === false) {
    379      windowFeatures += ",non-fission";
    380    }
    381 
    382    // If the opener window is maximized, we want to skip the animation, since
    383    // we're going to be taking up most of the screen anyways, and we want to
    384    // optimize for showing the user a useful window as soon as possible.
    385    if (openerWindow?.windowState == openerWindow?.STATE_MAXIMIZED) {
    386      windowFeatures += ",suppressanimation";
    387    }
    388 
    389    let win = Services.ww.openWindow(
    390      openerWindow,
    391      AppConstants.BROWSER_CHROME_URL,
    392      "_blank",
    393      windowFeatures,
    394      args
    395    );
    396    this.registerOpeningWindow(win, isPrivate);
    397 
    398    win.addEventListener(
    399      "MozAfterPaint",
    400      () => {
    401        if (
    402          Services.prefs.getIntPref("browser.startup.page") == 1 &&
    403          loadURIString == lazy.HomePage.get()
    404        ) {
    405          // A notification for when a user has triggered their homepage. This
    406          // is used to display a doorhanger explaining that an extension has
    407          // modified the homepage, if necessary.
    408          Services.obs.notifyObservers(win, "browser-open-homepage-start");
    409        }
    410      },
    411      { once: true }
    412    );
    413 
    414    return win;
    415  },
    416 
    417  /**
    418   * Async version of `openWindow` waiting for delayed startup of the new
    419   * window before returning.
    420   *
    421   * @param {object} [options]
    422   *   Options for the new window. See `openWindow` for details.
    423   *
    424   * @returns {Window}
    425   */
    426  async promiseOpenWindow(options) {
    427    let win = this.openWindow(options);
    428    await topicObserved(
    429      "browser-delayed-startup-finished",
    430      subject => subject == win
    431    );
    432    return win;
    433  },
    434 
    435  /**
    436   * Number of currently open browser windows.
    437   */
    438  get windowCount() {
    439    return _trackedWindows.length;
    440  },
    441 
    442  get orderedWindows() {
    443    return this.getOrderedWindows();
    444  },
    445 
    446  /**
    447   * Array of browser windows ordered by z-index, in reverse order.
    448   * This means that the top-most browser window will be the first item.
    449   *
    450   * @param {object} options
    451   * @param {boolean}  [options.private]
    452   *   If set, returns only windows with the specified privateness. i.e. `true`
    453   *   will return only private windows. The default value, `null`, will return
    454   *   all windows.
    455   */
    456  getOrderedWindows({ private: isPrivate = undefined } = {}) {
    457    // Clone the windows array immediately as it may change during iteration.
    458    // We'd rather have an outdated order than skip/revisit windows.
    459    const windows = [..._trackedWindows];
    460    if (
    461      typeof isPrivate !== "boolean" ||
    462      (isPrivate && lazy.PrivateBrowsingUtils.permanentPrivateBrowsing)
    463    ) {
    464      return windows;
    465    }
    466 
    467    return windows.filter(
    468      w => lazy.PrivateBrowsingUtils.isWindowPrivate(w) === isPrivate
    469    );
    470  },
    471 
    472  getAllVisibleTabs() {
    473    let tabs = [];
    474    for (let win of BrowserWindowTracker.orderedWindows) {
    475      for (let tab of win.gBrowser.visibleTabs) {
    476        // Only use tabs which are not discarded / unrestored
    477        if (tab.linkedPanel) {
    478          let { contentTitle, browserId } = tab.linkedBrowser;
    479          tabs.push({ contentTitle, browserId });
    480        }
    481      }
    482    }
    483    return tabs;
    484  },
    485 
    486  track(window) {
    487    let pending = this.pendingWindows.get(window);
    488    if (pending) {
    489      this.pendingWindows.delete(window);
    490      // Waiting for delayed startup to complete ensures that this new window
    491      // has started loading its initial urls.
    492      window.delayedStartupPromise.then(() => pending.deferred.resolve(window));
    493    }
    494 
    495    return WindowHelper.addWindow(window);
    496  },
    497 
    498  getBrowserById(browserId) {
    499    for (let win of BrowserWindowTracker.orderedWindows) {
    500      for (let tab of win.gBrowser.visibleTabs) {
    501        if (tab.linkedPanel && tab.linkedBrowser.browserId === browserId) {
    502          return tab.linkedBrowser;
    503        }
    504      }
    505    }
    506    return null;
    507  },
    508 
    509  // For tests only, this function will remove this window from the list of
    510  // tracked windows. Please don't forget to add it back at the end of your
    511  // tests using BrowserWindowTracker.track(window)!
    512  untrackForTestsOnly(window) {
    513    return WindowHelper.removeWindow(window);
    514  },
    515 };