tor-browser

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

URILoadingHelper.sys.mjs (38080B)


      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 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs";
      7 import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
     13  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     14  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     15  TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
     16  TorConnectParent: "resource://gre/actors/TorConnectParent.sys.mjs",
     17 });
     18 
     19 ChromeUtils.defineLazyGetter(lazy, "ReferrerInfo", () =>
     20  Components.Constructor(
     21    "@mozilla.org/referrer-info;1",
     22    "nsIReferrerInfo",
     23    "init"
     24  )
     25 );
     26 
     27 function saveLink(window, url, params) {
     28  if ("isContentWindowPrivate" in params) {
     29    window.saveURL(
     30      url,
     31      null,
     32      null,
     33      null,
     34      true,
     35      true,
     36      params.referrerInfo,
     37      null,
     38      null,
     39      params.isContentWindowPrivate,
     40      params.originPrincipal
     41    );
     42  } else {
     43    if (!params.initiatingDoc) {
     44      console.error(
     45        "openUILink/openLinkIn was called with " +
     46          "where == 'save' but without initiatingDoc.  See bug 814264."
     47      );
     48      return;
     49    }
     50    window.saveURL(
     51      url,
     52      null,
     53      null,
     54      null,
     55      true,
     56      true,
     57      params.referrerInfo,
     58      null,
     59      params.initiatingDoc
     60    );
     61  }
     62 }
     63 
     64 function openInWindow(url, params, sourceWindow) {
     65  let {
     66    referrerInfo,
     67    forceNonPrivate,
     68    triggeringRemoteType,
     69    forceAllowDataURI,
     70    globalHistoryOptions,
     71    allowThirdPartyFixup,
     72    userContextId,
     73    postData,
     74    originPrincipal,
     75    originStoragePrincipal,
     76    triggeringPrincipal,
     77    policyContainer,
     78    resolveOnContentBrowserCreated,
     79    chromeless,
     80  } = params;
     81  const CHROMELESS_FEATURES = `resizable,minimizable,titlebar,close`;
     82  let features = `chrome,dialog=no,${chromeless ? CHROMELESS_FEATURES : "all"}`;
     83  if (params.private) {
     84    features += ",private";
     85    // To prevent regular browsing data from leaking to private browsing sites,
     86    // strip the referrer when opening a new private window. (See Bug: 1409226)
     87    referrerInfo = new lazy.ReferrerInfo(
     88      referrerInfo.referrerPolicy,
     89      false,
     90      referrerInfo.originalReferrer
     91    );
     92  } else if (forceNonPrivate) {
     93    features += ",non-private";
     94  }
     95 
     96  // This propagates to window.arguments.
     97  var sa = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
     98 
     99  var wuri = Cc["@mozilla.org/supports-string;1"].createInstance(
    100    Ci.nsISupportsString
    101  );
    102  wuri.data = url;
    103 
    104  let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
    105    Ci.nsIWritablePropertyBag2
    106  );
    107  if (triggeringRemoteType) {
    108    extraOptions.setPropertyAsACString(
    109      "triggeringRemoteType",
    110      triggeringRemoteType
    111    );
    112  }
    113  if (params.hasValidUserGestureActivation !== undefined) {
    114    extraOptions.setPropertyAsBool(
    115      "hasValidUserGestureActivation",
    116      params.hasValidUserGestureActivation
    117    );
    118  }
    119  if (params.textDirectiveUserActivation !== undefined) {
    120    extraOptions.setPropertyAsBool(
    121      "textDirectiveUserActivation",
    122      params.textDirectiveUserActivation
    123    );
    124  }
    125  if (forceAllowDataURI) {
    126    extraOptions.setPropertyAsBool("forceAllowDataURI", true);
    127  }
    128  if (params.fromExternal !== undefined) {
    129    extraOptions.setPropertyAsBool("fromExternal", params.fromExternal);
    130  }
    131  if (globalHistoryOptions?.triggeringSponsoredURL) {
    132    extraOptions.setPropertyAsACString(
    133      "triggeringSponsoredURL",
    134      globalHistoryOptions.triggeringSponsoredURL
    135    );
    136    if (globalHistoryOptions.triggeringSponsoredURLVisitTimeMS) {
    137      extraOptions.setPropertyAsUint64(
    138        "triggeringSponsoredURLVisitTimeMS",
    139        globalHistoryOptions.triggeringSponsoredURLVisitTimeMS
    140      );
    141    }
    142    if (globalHistoryOptions.triggeringSource) {
    143      extraOptions.setPropertyAsACString(
    144        "triggeringSource",
    145        globalHistoryOptions.triggeringSource
    146      );
    147    }
    148  }
    149  if (params.schemelessInput !== undefined) {
    150    extraOptions.setPropertyAsUint32("schemelessInput", params.schemelessInput);
    151  }
    152 
    153  var allowThirdPartyFixupSupports = Cc[
    154    "@mozilla.org/supports-PRBool;1"
    155  ].createInstance(Ci.nsISupportsPRBool);
    156  allowThirdPartyFixupSupports.data = allowThirdPartyFixup;
    157 
    158  var userContextIdSupports = Cc[
    159    "@mozilla.org/supports-PRUint32;1"
    160  ].createInstance(Ci.nsISupportsPRUint32);
    161  userContextIdSupports.data = userContextId;
    162 
    163  sa.appendElement(wuri);
    164  sa.appendElement(extraOptions);
    165  sa.appendElement(referrerInfo);
    166  sa.appendElement(postData);
    167  sa.appendElement(allowThirdPartyFixupSupports);
    168  sa.appendElement(userContextIdSupports);
    169  sa.appendElement(originPrincipal);
    170  sa.appendElement(originStoragePrincipal);
    171  sa.appendElement(triggeringPrincipal);
    172  sa.appendElement(null); // allowInheritPrincipal
    173  sa.appendElement(policyContainer);
    174 
    175  let win;
    176 
    177  // Returns a promise that will be resolved when the new window's startup is finished.
    178  function waitForWindowStartup() {
    179    return new Promise(resolve => {
    180      const delayedStartupObserver = aSubject => {
    181        if (aSubject == win) {
    182          Services.obs.removeObserver(
    183            delayedStartupObserver,
    184            "browser-delayed-startup-finished"
    185          );
    186          resolve();
    187        }
    188      };
    189      Services.obs.addObserver(
    190        delayedStartupObserver,
    191        "browser-delayed-startup-finished"
    192      );
    193    });
    194  }
    195 
    196  if (params.frameID != undefined && sourceWindow) {
    197    // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget
    198    // event if it contains the expected frameID params.
    199    // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is
    200    // opening a new window using the keyboard shortcut).
    201    const sourceTabBrowser = sourceWindow.gBrowser.selectedBrowser;
    202    waitForWindowStartup().then(() => {
    203      Services.obs.notifyObservers(
    204        {
    205          wrappedJSObject: {
    206            url,
    207            createdTabBrowser: win.gBrowser.selectedBrowser,
    208            sourceTabBrowser,
    209            sourceFrameID: params.frameID,
    210          },
    211        },
    212        "webNavigation-createdNavigationTarget"
    213      );
    214    });
    215  }
    216 
    217  if (resolveOnContentBrowserCreated) {
    218    waitForWindowStartup().then(() =>
    219      resolveOnContentBrowserCreated(win.gBrowser.selectedBrowser)
    220    );
    221  }
    222 
    223  win = Services.ww.openWindow(
    224    sourceWindow,
    225    AppConstants.BROWSER_CHROME_URL,
    226    null,
    227    features,
    228    sa
    229  );
    230 }
    231 
    232 function openInCurrentTab(targetBrowser, url, uriObj, params) {
    233  let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
    234 
    235  if (params.allowThirdPartyFixup) {
    236    loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
    237    loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
    238  }
    239  // LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL isn't supported for javascript URIs,
    240  // i.e. it causes them not to load at all. Callers should strip
    241  // "javascript:" from pasted strings to prevent blank tabs
    242  if (!params.allowInheritPrincipal) {
    243    loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
    244  }
    245 
    246  if (params.allowPopups) {
    247    loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS;
    248  }
    249  if (params.indicateErrorPageLoad) {
    250    loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV;
    251  }
    252  if (params.forceAllowDataURI) {
    253    loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
    254  }
    255  if (params.fromExternal) {
    256    loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
    257  }
    258 
    259  let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
    260  if (
    261    params.forceAboutBlankViewerInCurrent &&
    262    (!uriObj ||
    263      Services.io.getDynamicProtocolFlags(uriObj) &
    264        URI_INHERITS_SECURITY_CONTEXT)
    265  ) {
    266    // Unless we know for sure we're not inheriting principals,
    267    // force the about:blank viewer to have the right principal:
    268    targetBrowser.createAboutBlankDocumentViewer(
    269      params.originPrincipal,
    270      params.originStoragePrincipal
    271    );
    272  }
    273 
    274  let {
    275    triggeringPrincipal,
    276    policyContainer,
    277    referrerInfo,
    278    postData,
    279    userContextId,
    280    hasValidUserGestureActivation,
    281    textDirectiveUserActivation,
    282    globalHistoryOptions,
    283    triggeringRemoteType,
    284    schemelessInput,
    285  } = params;
    286 
    287  targetBrowser.fixupAndLoadURIString(url, {
    288    triggeringPrincipal,
    289    policyContainer,
    290    loadFlags,
    291    referrerInfo,
    292    postData,
    293    userContextId,
    294    hasValidUserGestureActivation,
    295    textDirectiveUserActivation,
    296    globalHistoryOptions,
    297    triggeringRemoteType,
    298    schemelessInput,
    299  });
    300 
    301  params.resolveOnContentBrowserCreated?.(targetBrowser);
    302 }
    303 
    304 function updatePrincipals(window, params) {
    305  let { userContextId } = params;
    306  // Teach the principal about the right OA to use, e.g. in case when
    307  // opening a link in a new private window, or in a new container tab.
    308  // Please note we do not have to do that for SystemPrincipals and we
    309  // can not do it for NullPrincipals since NullPrincipals are only
    310  // identical if they actually are the same object (See Bug: 1346759)
    311  function useOAForPrincipal(principal) {
    312    if (principal && principal.isContentPrincipal) {
    313      let privateBrowsingId =
    314        params.private ||
    315        (window && PrivateBrowsingUtils.isWindowPrivate(window));
    316      let attrs = {
    317        userContextId,
    318        privateBrowsingId,
    319        firstPartyDomain: principal.originAttributes.firstPartyDomain,
    320      };
    321      return Services.scriptSecurityManager.principalWithOA(principal, attrs);
    322    }
    323    return principal;
    324  }
    325  params.originPrincipal = useOAForPrincipal(params.originPrincipal);
    326  params.originStoragePrincipal = useOAForPrincipal(
    327    params.originStoragePrincipal
    328  );
    329  params.triggeringPrincipal = useOAForPrincipal(params.triggeringPrincipal);
    330 }
    331 
    332 /* Creates a null principal using the userContextId
    333 from the current selected tab or a passed in tab argument */
    334 function _createNullPrincipalFromTabUserContextId(tab = null) {
    335  const window = lazy.BrowserWindowTracker.getTopWindow();
    336  if (!tab) {
    337    tab = window.gBrowser.selectedTab;
    338  }
    339 
    340  let userContextId;
    341  if (tab.hasAttribute("usercontextid")) {
    342    userContextId = tab.getAttribute("usercontextid");
    343  }
    344  return Services.scriptSecurityManager.createNullPrincipal({
    345    userContextId,
    346  });
    347 }
    348 
    349 export const URILoadingHelper = {
    350  /**
    351   * openLinkIn opens a URL in a place specified by the parameter |where|.
    352   *
    353   * The params object is the same as for `openLinkIn` and documented below.
    354   *
    355   * @param {string}  where
    356   *   |where| can be:
    357   *    "current"     current tab            (if there aren't any browser windows, then in a new window instead)
    358   *    "tab"         new tab                (if there aren't any browser windows, then in a new window instead)
    359   *    "tabshifted"  same as "tab" but in background if default is to select new tabs, and vice versa
    360   *    "window"      new window
    361   *    "chromeless"  new minimal window     (no browser navigation UI)
    362   *    "save"        save to disk (with no filename hint!)
    363   *
    364   * @param {object}  params
    365   *
    366   * Options relating to what tab/window to use and how to open it:
    367   *
    368   * @param {boolean} params.private
    369   *                  Load the URL in a private window.
    370   * @param {boolean} params.forceNonPrivate
    371   *                  Force the load to happen in non-private windows.
    372   * @param {boolean} params.relatedToCurrent
    373   *                  Whether new tabs should go immediately next to the current tab.
    374   * @param {Element} params.targetBrowser
    375   *                  The browser to use for the load. Only used if where == "current".
    376   * @param {boolean} params.inBackground
    377   *                  If explicitly true or false, whether to switch to the tab immediately.
    378   *                  If null, will switch to the tab if `forceForeground` was true. If
    379   *                  neither is passed, will defer to the user preference browser.tabs.loadInBackground.
    380   * @param {boolean} params.forceForeground
    381   *                  Ignore the user preference and load in the foreground.
    382   * @param {boolean} params.allowPinnedTabHostChange
    383   *                  Allow even a pinned tab to change hosts.
    384   * @param {boolean} params.allowPopups
    385   *                  whether the link is allowed to open in a popup window (ie one with no browser
    386   *                  chrome)
    387   * @param {boolean} params.skipTabAnimation
    388   *                  Skip the tab opening animation.
    389   * @param {Element} params.openerBrowser
    390   *                  The browser that started the load.
    391   * @param {boolean} params.avoidBrowserFocus
    392   *                  Don't focus the browser element immediately after starting
    393   *                  the load. Used by the URL bar to avoid leaking user input
    394   *                  into web content, see bug 1641287.
    395   *
    396   * Options relating to the load itself:
    397   *
    398   * @param {boolean} params.allowThirdPartyFixup
    399   *                  Allow transforming the 'url' into a search query.
    400   * @param {nsIInputStream} params.postData
    401   *                  Data to post as part of the request.
    402   * @param {nsIReferrerInfo} params.referrerInfo
    403   *                  Referrer info for the request.
    404   * @param {boolean} params.indicateErrorPageLoad
    405   *                  Whether docshell should throw an exception (i.e. return non-NS_OK)
    406   *                  if the load fails.
    407   * @param {string}  params.charset
    408   *                  Character set to use for the load. Only honoured for tabs.
    409   *                  Legacy argument - do not use.
    410   * @param {SchemelessInputType}  params.schemelessInput
    411   *                  Whether the search/URL term was without an explicit scheme.
    412   *
    413   * Options relating to security, whether the load is allowed to happen,
    414   * and what cookie container to use for the load:
    415   *
    416   * @param {boolean} params.forceAllowDataURI
    417   *                  Force allow a data URI to load as a toplevel load.
    418   * @param {number}  params.userContextId
    419   *                  The userContextId (container identifier) to use for the load.
    420   * @param {boolean} params.allowInheritPrincipal
    421   *                  Allow the load to inherit the triggering principal.
    422   * @param {boolean} params.forceAboutBlankViewerInCurrent
    423   *                  Force load an about:blank page first. Only used if
    424   *                  allowInheritPrincipal is passed or no URL was provided.
    425   * @param {nsIPrincipal} params.triggeringPrincipal
    426   *                  Triggering principal to pass to docshell for the load.
    427   * @param {nsIPrincipal} params.originPrincipal
    428   *                  Origin principal to pass to docshell for the load.
    429   * @param {nsIPrincipal} params.originStoragePrincipal
    430   *                  Storage principal to pass to docshell for the load.
    431   * @param {string}  params.triggeringRemoteType
    432   *                  The remoteType triggering this load.
    433   * @param {nsIPolicyContainer} params.policyContainer
    434   *                  The policyContainer that should apply to the load.
    435   * @param {boolean} params.hasValidUserGestureActivation
    436   *                  Indicates if a valid user gesture caused this load. This
    437   *                  informs e.g. popup blocker decisions.
    438   * @param {boolean} params.fromExternal
    439   *                  Indicates the load was started outside of the browser,
    440   *                  e.g. passed on the commandline or through OS mechanisms.
    441   *
    442   * Options used to track the load elsewhere
    443   *
    444   * @param {function} params.resolveOnNewTabCreated
    445   *                   This callback will be called when a new tab is created.
    446   * @param {function} params.resolveOnContentBrowserCreated
    447   *                   This callback will be called with the content browser once it's created.
    448   * @param {object}   params.globalHistoryOptions
    449   *                   Used by places to keep track of search related metadata for loads.
    450   * @param {number}   params.frameID
    451   *                   Used by webextensions for their loads.
    452   *
    453   * Options used for where="save" only:
    454   *
    455   * @param {boolean}  params.isContentWindowPrivate
    456   *                   Save content as coming from a private window.
    457   * @param {Document} params.initiatingDoc
    458   *                   Used to determine where to prompt for a filename.
    459   */
    460  openLinkIn(window, url, where, params) {
    461    if (!where || !url) {
    462      return;
    463    }
    464 
    465    // make sure users are not faced with the scary red 'tor isn't working' screen
    466    // if they navigate to about:tor before bootstrapped
    467    //
    468    // fixes tor-browser#40752
    469    // new tabs also redirect to about:tor if browser.newtabpage.enabled is true
    470    // otherwise they go to about:blank
    471    if (lazy.TorConnect.shouldShowTorConnect) {
    472      const homeURLs = [
    473        "about:home",
    474        "about:privatebrowsing",
    475        "about:tor",
    476        "about:welcome",
    477      ];
    478      if (
    479        homeURLs.includes(url) ||
    480        (url === "about:newtab" &&
    481          Services.prefs.getBoolPref("browser.newtabpage.enabled", false))
    482      ) {
    483        url = lazy.TorConnectParent.getRedirectURL(url);
    484      }
    485    }
    486 
    487    let {
    488      allowThirdPartyFixup,
    489      postData,
    490      charset,
    491      allowInheritPrincipal,
    492      forceAllowDataURI,
    493      forceNonPrivate,
    494      skipTabAnimation,
    495      allowPinnedTabHostChange,
    496      userContextId,
    497      triggeringPrincipal,
    498      originPrincipal,
    499      originStoragePrincipal,
    500      triggeringRemoteType,
    501      policyContainer,
    502      resolveOnNewTabCreated,
    503      resolveOnContentBrowserCreated,
    504      globalHistoryOptions,
    505      hasValidUserGestureActivation,
    506      textDirectiveUserActivation,
    507    } = params;
    508 
    509    // We want to overwrite some things for convenience when passing it to other
    510    // methods. To avoid impacting callers, copy the params.
    511    params = Object.assign({}, params);
    512 
    513    if (!params.referrerInfo) {
    514      params.referrerInfo = new lazy.ReferrerInfo(
    515        Ci.nsIReferrerInfo.EMPTY,
    516        true,
    517        null
    518      );
    519    }
    520 
    521    if (!triggeringPrincipal) {
    522      throw new Error("Must load with a triggering Principal");
    523    }
    524 
    525    if (where == "save") {
    526      saveLink(window, url, params);
    527      return;
    528    }
    529 
    530    // Establish which window we'll load the link in.
    531    let w = this._resolveInitialTargetWindow(
    532      where,
    533      params,
    534      window,
    535      forceNonPrivate
    536    );
    537 
    538    updatePrincipals(w, params);
    539 
    540    if (where == "chromeless") {
    541      params.chromeless = true;
    542      where = "window";
    543    }
    544 
    545    if (!w || where == "window") {
    546      openInWindow(url, params, w || window);
    547      return;
    548    }
    549 
    550    // We're now committed to loading the link in an existing browser window.
    551 
    552    // Raise the target window before loading the URI, since loading it may
    553    // result in a new frontmost window (e.g. "javascript:window.open('');").
    554    w.focus();
    555 
    556    let targetBrowser;
    557    let loadInBackground;
    558    let uriObj;
    559 
    560    if (where == "current") {
    561      targetBrowser = params.targetBrowser || w.gBrowser.selectedBrowser;
    562      loadInBackground = false;
    563      uriObj = URL.parse(url)?.URI;
    564 
    565      // In certain tabs, we restrict what if anything may replace the loaded
    566      // page. If a load request bounces off for the currently selected tab,
    567      // we'll open a new tab instead.
    568      let tab = w.gBrowser.getTabForBrowser(targetBrowser);
    569      if (tab == w.FirefoxViewHandler.tab) {
    570        where = "tab";
    571        targetBrowser = null;
    572      } else if (
    573        !allowPinnedTabHostChange &&
    574        tab.pinned &&
    575        url != "about:crashcontent"
    576      ) {
    577        try {
    578          // nsIURI.host can throw for non-nsStandardURL nsIURIs.
    579          if (
    580            !uriObj ||
    581            (!uriObj.schemeIs("javascript") &&
    582              targetBrowser.currentURI.host != uriObj.host)
    583          ) {
    584            where = "tab";
    585            targetBrowser = null;
    586          }
    587        } catch (err) {
    588          where = "tab";
    589          targetBrowser = null;
    590        }
    591      }
    592    } else {
    593      // `where` is "tab" or "tabshifted", so we'll load the link in a new tab.
    594      loadInBackground = params.inBackground;
    595      if (loadInBackground == null) {
    596        loadInBackground = params.forceForeground
    597          ? false
    598          : Services.prefs.getBoolPref("browser.tabs.loadInBackground");
    599      }
    600    }
    601 
    602    let focusUrlBar = false;
    603 
    604    switch (where) {
    605      case "current":
    606        openInCurrentTab(targetBrowser, url, uriObj, params);
    607 
    608        // Don't focus the content area if focus is in the address bar and we're
    609        // loading the New Tab page.
    610        focusUrlBar =
    611          w.document.activeElement == w.gURLBar.inputField &&
    612          w.isBlankPageURL(url);
    613        break;
    614      case "tabshifted":
    615        loadInBackground = !loadInBackground;
    616      // fall through
    617      case "tab": {
    618        focusUrlBar =
    619          !loadInBackground &&
    620          w.isBlankPageURL(url) &&
    621          !lazy.AboutNewTab.willNotifyUser;
    622 
    623        let tabUsedForLoad = w.gBrowser.addTab(url, {
    624          referrerInfo: params.referrerInfo,
    625          charset,
    626          postData,
    627          inBackground: loadInBackground,
    628          allowThirdPartyFixup,
    629          relatedToCurrent: params.relatedToCurrent,
    630          skipAnimation: skipTabAnimation,
    631          userContextId,
    632          originPrincipal,
    633          originStoragePrincipal,
    634          triggeringPrincipal,
    635          allowInheritPrincipal,
    636          triggeringRemoteType,
    637          policyContainer,
    638          forceAllowDataURI,
    639          focusUrlBar,
    640          openerBrowser: params.openerBrowser,
    641          fromExternal: params.fromExternal,
    642          globalHistoryOptions,
    643          schemelessInput: params.schemelessInput,
    644          hasValidUserGestureActivation,
    645          textDirectiveUserActivation,
    646        });
    647        targetBrowser = tabUsedForLoad.linkedBrowser;
    648 
    649        resolveOnNewTabCreated?.(targetBrowser);
    650        resolveOnContentBrowserCreated?.(targetBrowser);
    651 
    652        if (params.frameID != undefined && w) {
    653          // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget
    654          // event if it contains the expected frameID params.
    655          // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is
    656          // opening a new tab using the keyboard shortcut).
    657          Services.obs.notifyObservers(
    658            {
    659              wrappedJSObject: {
    660                url,
    661                createdTabBrowser: targetBrowser,
    662                sourceTabBrowser: w.gBrowser.selectedBrowser,
    663                sourceFrameID: params.frameID,
    664              },
    665            },
    666            "webNavigation-createdNavigationTarget"
    667          );
    668        }
    669        break;
    670      }
    671    }
    672 
    673    // Potentially trigger a URL bar telemetry bounce event when navigating
    674    // away from a page using the browser chrome.
    675    // We avoid triggering for URL bar initiated loads since this gets called
    676    // right after a result is picked and the bounce event tracking is started.
    677    // We instead check for potential URL bar initiated bounce events directly
    678    // in gURLBar.controller.engagementEvent.startTrackingBounceEvent().
    679    if (!params.initiatedByURLBar && targetBrowser) {
    680      w.gURLBar.controller.engagementEvent.handleBounceEventTrigger(
    681        targetBrowser
    682      );
    683    }
    684 
    685    if (
    686      !params.avoidBrowserFocus &&
    687      !focusUrlBar &&
    688      targetBrowser == w.gBrowser.selectedBrowser
    689    ) {
    690      // Focus the content, but only if the browser used for the load is selected.
    691      targetBrowser.focus();
    692    }
    693  },
    694  /**
    695   * Resolve the initial browser window to use for a load, based on `where`.
    696   *
    697   * @param {string} where
    698   *        The target location for the load (e.g. "current", "tab", "window").
    699   * @param {object} params
    700   *        The full params object passed to openLinkIn.
    701   * @param {Window} win
    702   *        The reference window used as a fallback for getTargetWindow.
    703   * @param {boolean} forceNonPrivate
    704   *        Whether to force choosing a non-private target window.
    705   * @returns {Window}
    706   *          The browser window that should be used as the initial target.
    707   */
    708  _resolveInitialTargetWindow(where, params, win, forceNonPrivate) {
    709    if (where === "current" && params.targetBrowser) {
    710      return params.targetBrowser.ownerGlobal;
    711    }
    712 
    713    if (where === "tab" || where === "tabshifted") {
    714      const target = this.getTargetWindow(win, {
    715        skipPopups: true,
    716        skipTaskbarTabs: true,
    717        forceNonPrivate,
    718      });
    719      if (win.top !== target) {
    720        params.relatedToCurrent = false;
    721      }
    722      return target;
    723    }
    724    return this.getTargetWindow(win, { forceNonPrivate });
    725  },
    726  /**
    727   * Finds a browser window suitable for opening a link matching the
    728   * requirements given in the `params` argument. If the current window matches
    729   * the requirements then it is returned otherwise the top-most window that
    730   * matches will be returned.
    731   *
    732   * @param {Window} window - The current window.
    733   * @param {object} params - Parameters for selecting the window.
    734   * @param {boolean} params.skipPopups - Require a non-popup window.
    735   * @param {boolean} params.skipTaskbarTabs - Require a non-taskbartab window.
    736   * @param {boolean} params.forceNonPrivate - Require a non-private window.
    737   * @returns {Window | null} A matching browser window or null if none matched.
    738   */
    739  getTargetWindow(
    740    window,
    741    { skipPopups, skipTaskbarTabs, forceNonPrivate } = {}
    742  ) {
    743    let { top } = window;
    744    // If this is called in a browser window, use that window regardless of
    745    // whether it's the frontmost window, since commands can be executed in
    746    // background windows (bug 626148).
    747    if (
    748      top.document.documentElement.getAttribute("windowtype") ==
    749        "navigator:browser" &&
    750      (!skipPopups || top.toolbar.visible) &&
    751      (!skipTaskbarTabs ||
    752        !top.document.documentElement.hasAttribute("taskbartab")) &&
    753      (!forceNonPrivate || !PrivateBrowsingUtils.isWindowPrivate(top))
    754    ) {
    755      return top;
    756    }
    757 
    758    return lazy.BrowserWindowTracker.getTopWindow({
    759      private: !forceNonPrivate && PrivateBrowsingUtils.isWindowPrivate(window),
    760      allowPopups: !skipPopups,
    761      allowTaskbarTabs: !skipTaskbarTabs,
    762    });
    763  },
    764 
    765  /**
    766   * openUILink handles clicks on UI elements that cause URLs to load.
    767   *
    768   * @param {string} url
    769   * @param {Event | object} event Event or JSON object representing an Event
    770   * @param {boolean | object} aIgnoreButton
    771   *                           Boolean or object with the same properties as
    772   *                           accepted by openLinkIn, plus "ignoreButton"
    773   *                           and "ignoreAlt".
    774   * @param {boolean} aIgnoreAlt
    775   * @param {boolean} aAllowThirdPartyFixup
    776   * @param {object} aPostData
    777   * @param {object} aReferrerInfo
    778   */
    779  openUILink(
    780    window,
    781    url,
    782    event,
    783    aIgnoreButton,
    784    aIgnoreAlt,
    785    aAllowThirdPartyFixup,
    786    aPostData,
    787    aReferrerInfo
    788  ) {
    789    event = BrowserUtils.getRootEvent(event);
    790    let params;
    791 
    792    if (aIgnoreButton && typeof aIgnoreButton == "object") {
    793      params = aIgnoreButton;
    794 
    795      // don't forward "ignoreButton" and "ignoreAlt" to openLinkIn
    796      aIgnoreButton = params.ignoreButton;
    797      aIgnoreAlt = params.ignoreAlt;
    798      delete params.ignoreButton;
    799      delete params.ignoreAlt;
    800    } else {
    801      params = {
    802        allowThirdPartyFixup: aAllowThirdPartyFixup,
    803        postData: aPostData,
    804        referrerInfo: aReferrerInfo,
    805        initiatingDoc: event ? event.target.ownerDocument : null,
    806      };
    807    }
    808 
    809    if (!params.triggeringPrincipal) {
    810      throw new Error(
    811        "Required argument triggeringPrincipal missing within openUILink"
    812      );
    813    }
    814 
    815    let where = BrowserUtils.whereToOpenLink(event, aIgnoreButton, aIgnoreAlt);
    816    params.forceForeground ??= true;
    817    this.openLinkIn(window, url, where, params);
    818  },
    819 
    820  /* openTrustedLinkIn will attempt to open the given URI using the SystemPrincipal
    821   * as the trigeringPrincipal, unless a more specific Principal is provided.
    822   *
    823   * Otherwise, parameters are the same as openLinkIn, but we will set `forceForeground`
    824   * to true.
    825   */
    826  openTrustedLinkIn(window, url, where, params = {}) {
    827    if (!params.triggeringPrincipal) {
    828      params.triggeringPrincipal =
    829        Services.scriptSecurityManager.getSystemPrincipal();
    830    }
    831 
    832    params.forceForeground ??= true;
    833    this.openLinkIn(window, url, where, params);
    834  },
    835 
    836  /* openWebLinkIn will attempt to open the given URI using the NullPrincipal
    837   * as the triggeringPrincipal, unless a more specific Principal is provided.
    838   *
    839   * Otherwise, parameters are the same as openLinkIn, but we will set `forceForeground`
    840   * to true.
    841   */
    842  openWebLinkIn(window, url, where, params = {}) {
    843    if (!params.triggeringPrincipal) {
    844      params.triggeringPrincipal =
    845        Services.scriptSecurityManager.createNullPrincipal({});
    846    }
    847    if (params.triggeringPrincipal.isSystemPrincipal) {
    848      throw new Error(
    849        "System principal should never be passed into openWebLinkIn()"
    850      );
    851    }
    852    params.forceForeground ??= true;
    853    this.openLinkIn(window, url, where, params);
    854  },
    855 
    856  /**
    857   * Given a URI, guess which container to use to open it. This is used for external
    858   * openers as a quality of life improvement (e.g. to open a document into the container
    859   * where you are logged in to the service that hosts it).
    860   * matches will be returned.
    861   * For now this can only use currently-open tabs, until history is tagged with the
    862   * container id (https://bugzilla.mozilla.org/show_bug.cgi?id=1283320).
    863   *
    864   * @param {nsIURI} aURI - The URI being opened.
    865   * @returns {number | null} The guessed userContextId, or null if none.
    866   */
    867  guessUserContextId(aURI) {
    868    let host;
    869    try {
    870      host = aURI.host;
    871    } catch (e) {}
    872    if (!host) {
    873      return null;
    874    }
    875    const containerScores = new Map();
    876    let guessedUserContextId = null;
    877    let maxCount = 0;
    878    for (let win of lazy.BrowserWindowTracker.orderedWindows) {
    879      for (let tab of win.gBrowser.visibleTabs) {
    880        let { userContextId } = tab;
    881        let currentURIHost = null;
    882        try {
    883          currentURIHost = tab.linkedBrowser.currentURI.host;
    884        } catch (e) {}
    885 
    886        if (currentURIHost == host) {
    887          let count = (containerScores.get(userContextId) ?? 0) + 1;
    888          containerScores.set(userContextId, count);
    889          if (count > maxCount) {
    890            guessedUserContextId = userContextId;
    891            maxCount = count;
    892          }
    893        }
    894      }
    895    }
    896 
    897    return guessedUserContextId;
    898  },
    899  /**
    900   * Switch to a tab that has a given URI, and focuses its browser window.
    901   * If a matching tab is in this window, it will be switched to. Otherwise, other
    902   * windows will be searched.
    903   *
    904   * @param window
    905   *        The current window
    906   * @param aURI
    907   *        URI to search for
    908   * @param aOpenNew
    909   *        True to open a new tab and switch to it, if no existing tab is found.
    910   *        If no suitable window is found, a new one will be opened.
    911   * @param aOpenParams
    912   *        If switching to this URI results in us opening a tab, aOpenParams
    913   *        will be the parameter object that gets passed to openTrustedLinkIn. Please
    914   *        see the documentation for openTrustedLinkIn to see what parameters can be
    915   *        passed via this object.
    916   *        This object also allows:
    917   *        - 'ignoreFragment' property to be set to true to exclude fragment-portion
    918   *        matching when comparing URIs.
    919   *          If set to "whenComparing", the fragment will be unmodified.
    920   *          If set to "whenComparingAndReplace", the fragment will be replaced.
    921   *        - 'ignoreQueryString' boolean property to be set to true to exclude query string
    922   *        matching when comparing URIs.
    923   *        - 'replaceQueryString' boolean property to be set to true to exclude query string
    924   *        matching when comparing URIs and overwrite the initial query string with
    925   *        the one from the new URI.
    926   *        - 'adoptIntoActiveWindow' boolean property to be set to true to adopt the tab
    927   *        into the current window.
    928   * @param aUserContextId
    929   *        If not null, will switch to the first found tab having the provided
    930   *        userContextId.
    931   * @return True if an existing tab was found, false otherwise
    932   */
    933  switchToTabHavingURI(
    934    window,
    935    aURI,
    936    aOpenNew,
    937    aOpenParams = {},
    938    aUserContextId = null
    939  ) {
    940    // Certain URLs can be switched to irrespective of the source or destination
    941    // window being in private browsing mode:
    942    const kPrivateBrowsingURLs = new Set(["about:addons"]);
    943 
    944    let ignoreFragment = aOpenParams.ignoreFragment;
    945    let ignoreQueryString = aOpenParams.ignoreQueryString;
    946    let replaceQueryString = aOpenParams.replaceQueryString;
    947    let adoptIntoActiveWindow = aOpenParams.adoptIntoActiveWindow;
    948 
    949    // These properties are only used by switchToTabHavingURI and should
    950    // not be used as a parameter for the new load.
    951    delete aOpenParams.ignoreFragment;
    952    delete aOpenParams.ignoreQueryString;
    953    delete aOpenParams.replaceQueryString;
    954    delete aOpenParams.adoptIntoActiveWindow;
    955 
    956    let isBrowserWindow = !!window.gBrowser;
    957 
    958    // This will switch to the tab in aWindow having aURI, if present.
    959    function switchIfURIInWindow(aWindow) {
    960      // We can switch tab only if if both the source and destination windows have
    961      // the same private-browsing status.
    962      if (
    963        !kPrivateBrowsingURLs.has(aURI.spec) &&
    964        PrivateBrowsingUtils.isWindowPrivate(window) !==
    965          PrivateBrowsingUtils.isWindowPrivate(aWindow)
    966      ) {
    967        return false;
    968      }
    969 
    970      // Remove the query string, fragment, both, or neither from a given url.
    971      function cleanURL(url, removeQuery, removeFragment) {
    972        let ret = url;
    973        if (removeFragment) {
    974          ret = ret.split("#")[0];
    975          if (removeQuery) {
    976            // This removes a query, if present before the fragment.
    977            ret = ret.split("?")[0];
    978          }
    979        } else if (removeQuery) {
    980          // This is needed in case there is a fragment after the query.
    981          let fragment = ret.split("#")[1];
    982          ret = ret
    983            .split("?")[0]
    984            .concat(fragment != undefined ? "#".concat(fragment) : "");
    985        }
    986        return ret;
    987      }
    988 
    989      // Need to handle nsSimpleURIs here too (e.g. about:...), which don't
    990      // work correctly with URL objects - so treat them as strings
    991      let ignoreFragmentWhenComparing =
    992        typeof ignoreFragment == "string" &&
    993        ignoreFragment.startsWith("whenComparing");
    994      let requestedCompare = cleanURL(
    995        aURI.displaySpec,
    996        ignoreQueryString || replaceQueryString,
    997        ignoreFragmentWhenComparing
    998      );
    999      let browsers = aWindow.gBrowser.browsers;
   1000      for (let i = 0; i < browsers.length; i++) {
   1001        let browser = browsers[i];
   1002        let browserCompare = cleanURL(
   1003          browser.currentURI.displaySpec,
   1004          ignoreQueryString || replaceQueryString,
   1005          ignoreFragmentWhenComparing
   1006        );
   1007        let browserUserContextId = browser.getAttribute("usercontextid") || "";
   1008        if (aUserContextId != null && aUserContextId != browserUserContextId) {
   1009          continue;
   1010        }
   1011        if (requestedCompare == browserCompare) {
   1012          // If adoptIntoActiveWindow is set, and this is a cross-window switch,
   1013          // adopt the tab into the current window, after the active tab.
   1014          let doAdopt =
   1015            adoptIntoActiveWindow && isBrowserWindow && aWindow != window;
   1016 
   1017          if (doAdopt) {
   1018            const newTab = window.gBrowser.adoptTab(
   1019              aWindow.gBrowser.getTabForBrowser(browser),
   1020              {
   1021                tabIndex: window.gBrowser.tabContainer.selectedIndex + 1,
   1022                selectTab: true,
   1023              }
   1024            );
   1025            if (!newTab) {
   1026              doAdopt = false;
   1027            }
   1028          }
   1029          if (!doAdopt) {
   1030            aWindow.focus();
   1031          }
   1032 
   1033          if (
   1034            ignoreFragment == "whenComparingAndReplace" ||
   1035            replaceQueryString
   1036          ) {
   1037            browser.loadURI(aURI, {
   1038              triggeringPrincipal:
   1039                aOpenParams.triggeringPrincipal ||
   1040                _createNullPrincipalFromTabUserContextId(),
   1041            });
   1042          }
   1043 
   1044          if (!doAdopt) {
   1045            aWindow.gBrowser.tabContainer.selectedIndex = i;
   1046          }
   1047 
   1048          return true;
   1049        }
   1050      }
   1051      return false;
   1052    }
   1053 
   1054    // This can be passed either nsIURI or a string.
   1055    if (!(aURI instanceof Ci.nsIURI)) {
   1056      aURI = Services.io.newURI(aURI);
   1057    }
   1058 
   1059    // Prioritise this window.
   1060    if (isBrowserWindow && switchIfURIInWindow(window)) {
   1061      return true;
   1062    }
   1063 
   1064    for (let browserWin of lazy.BrowserWindowTracker.orderedWindows) {
   1065      // Skip closed (but not yet destroyed) windows,
   1066      // and the current window (which was checked earlier).
   1067      if (browserWin.closed || browserWin == window) {
   1068        continue;
   1069      }
   1070      if (switchIfURIInWindow(browserWin)) {
   1071        return true;
   1072      }
   1073    }
   1074 
   1075    // No opened tab has that url.
   1076    if (aOpenNew) {
   1077      if (
   1078        lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") &&
   1079        aUserContextId != null
   1080      ) {
   1081        aOpenParams.userContextId = aUserContextId;
   1082      }
   1083      if (isBrowserWindow && window.gBrowser.selectedTab.isEmpty) {
   1084        this.openTrustedLinkIn(window, aURI.spec, "current", aOpenParams);
   1085      } else {
   1086        this.openTrustedLinkIn(window, aURI.spec, "tab", aOpenParams);
   1087      }
   1088    }
   1089 
   1090    return false;
   1091  },
   1092 };