tor-browser

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

browsing-context-helpers.sys.mjs (17604B)


      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 export const WEBEXTENSION_FALLBACK_DOC_URL =
      6  "chrome://devtools/content/shared/webextension-fallback.html";
      7 
      8 /**
      9 * Retrieve the addon id corresponding to a given window global.
     10 * This is usually extracted from the principal, but in case we are dealing
     11 * with a DevTools webextension fallback window, the addon id will be available
     12 * in the URL.
     13 *
     14 * @param {WindowGlobalChild|WindowGlobalParent} windowGlobal
     15 *        The WindowGlobal from which we want to extract the addonId. Either a
     16 *        WindowGlobalParent or a WindowGlobalChild depending on where this
     17 *        helper is used from.
     18 * @return {string} Returns the addon id if any could found, null otherwise.
     19 */
     20 export function getAddonIdForWindowGlobal(windowGlobal) {
     21  const browsingContext = windowGlobal.browsingContext;
     22  const isParent = CanonicalBrowsingContext.isInstance(browsingContext);
     23  // documentPrincipal is only exposed on WindowGlobalParent,
     24  // use a fallback for WindowGlobalChild.
     25  const principal = isParent
     26    ? windowGlobal.documentPrincipal
     27    : browsingContext.window.document.nodePrincipal;
     28 
     29  // On Android we can get parent process windows where `documentPrincipal` and
     30  // `documentURI` are both unavailable. Bail out early.
     31  if (!principal) {
     32    return null;
     33  }
     34 
     35  // Most webextension documents are loaded from moz-extension://{addonId} and
     36  // the principal provides the addon id.
     37  if (principal.addonId) {
     38    return principal.addonId;
     39  }
     40 
     41  // If no addon id was available on the principal, check if the window is the
     42  // DevTools fallback window and extract the addon id from the URL.
     43  const href = isParent
     44    ? windowGlobal.documentURI?.displaySpec
     45    : browsingContext.window.document.location.href;
     46 
     47  if (href && href.startsWith(WEBEXTENSION_FALLBACK_DOC_URL)) {
     48    const [, addonId] = href.split("#");
     49    return addonId;
     50  }
     51 
     52  return null;
     53 }
     54 
     55 /**
     56 * Helper function to know if a given BrowsingContext should be debugged by scope
     57 * described by the given session context.
     58 *
     59 * @param {BrowsingContext} browsingContext
     60 *        The browsing context we want to check if it is part of debugged context
     61 * @param {object} sessionContext
     62 *        The Session Context to help know what is debugged.
     63 *        See devtools/server/actors/watcher/session-context.js
     64 * @param {object} options
     65 *        Optional arguments passed via a dictionary.
     66 * @param {boolean} options.forceAcceptTopLevelTarget
     67 *        If true, we will accept top level browsing context even when server target switching
     68 *        is disabled. In case of client side target switching, the top browsing context
     69 *        is debugged via a target actor that is being instantiated manually by the frontend.
     70 *        And this target actor isn't created, nor managed by the watcher actor.
     71 * @param {boolean} options.acceptUncommitedInitialDocument
     72 *        By default, we ignore initial about:blank documents/WindowGlobals.
     73 *        But some code cares about all the WindowGlobals, this flag allows to also accept them.
     74 *        (Used by _validateWindowGlobal)
     75 * @param {boolean} options.acceptNoWindowGlobal
     76 *        By default, we will reject BrowsingContext that don't have any WindowGlobal,
     77 *        either retrieved via BrowsingContext.currentWindowGlobal in the parent process,
     78 *        or via the options.windowGlobal argument.
     79 *        But in some case, we are processing BrowsingContext very early, before any
     80 *        WindowGlobal has been created for it. But they are still relevant BrowsingContexts
     81 *        to debug.
     82 * @param {WindowGlobal} options.windowGlobal
     83 *        When we are in the content process, we can't easily retrieve the WindowGlobal
     84 *        for a given BrowsingContext. So allow to pass it via this argument.
     85 *        Also, there is some race conditions where browsingContext.currentWindowGlobal
     86 *        is null, while the callsite may have a reference to the WindowGlobal.
     87 */
     88 // The goal of this method is to gather all checks done against BrowsingContext and WindowGlobal interfaces
     89 // which leads it to be a lengthy method. So disable the complexity rule which is counter productive here.
     90 // eslint-disable-next-line complexity
     91 export function isBrowsingContextPartOfContext(
     92  browsingContext,
     93  sessionContext,
     94  options = {}
     95 ) {
     96  let {
     97    forceAcceptTopLevelTarget = false,
     98    acceptNoWindowGlobal = false,
     99    windowGlobal,
    100  } = options;
    101 
    102  // For now, reject debugging chrome BrowsingContext.
    103  // This is for example top level chrome windows like browser.xhtml or webconsole/index.html (only the browser console)
    104  //
    105  // Tab and WebExtension debugging shouldn't target any such privileged document.
    106  // All their document should be of type "content".
    107  //
    108  // This may only be an issue for the Browser Toolbox.
    109  // For now, we expect the ParentProcessTargetActor to debug these.
    110  // Note that we should probably revisit that, and have each WindowGlobal be debugged
    111  // by one dedicated WindowGlobalTargetActor (bug 1685500). This requires some tweaks, at least in console-message
    112  // resource watcher, which makes the ParentProcessTarget's console message resource watcher watch
    113  // for all documents messages. It should probably only care about window-less messages and have one target per window global,
    114  // each target fetching one window global messages.
    115  //
    116  // Such project would be about applying "EFT" to the browser toolbox and non-content documents
    117  if (
    118    CanonicalBrowsingContext.isInstance(browsingContext) &&
    119    !browsingContext.isContent
    120  ) {
    121    return false;
    122  }
    123 
    124  if (!windowGlobal) {
    125    // When we are in the parent process, WindowGlobal can be retrieved from the BrowsingContext,
    126    // while in the content process, the callsites have to pass it manually as an argument
    127    if (CanonicalBrowsingContext.isInstance(browsingContext)) {
    128      windowGlobal = browsingContext.currentWindowGlobal;
    129    } else if (!windowGlobal && !acceptNoWindowGlobal) {
    130      throw new Error(
    131        "isBrowsingContextPartOfContext expect a windowGlobal argument when called from the content process"
    132      );
    133    }
    134  }
    135  // If we have a WindowGlobal, there is some additional checks we can do
    136  if (
    137    windowGlobal &&
    138    !_validateWindowGlobal(windowGlobal, sessionContext, options)
    139  ) {
    140    return false;
    141  }
    142  // Loading or destroying BrowsingContext won't have any associated WindowGlobal.
    143  // Ignore them by default. They should be either handled via DOMWindowCreated event or JSWindowActor destroy
    144  if (!windowGlobal && !acceptNoWindowGlobal) {
    145    return false;
    146  }
    147 
    148  // Now do the checks specific to each session context type
    149  if (sessionContext.type == "all") {
    150    return true;
    151  }
    152  if (sessionContext.type == "browser-element") {
    153    // Check if the document is:
    154    // - part of the Browser element, or,
    155    // - a popup originating from the browser element (the popup being loaded in a distinct browser element)
    156    const isMatchingTheBrowserElement =
    157      browsingContext.browserId == sessionContext.browserId;
    158    if (
    159      !isMatchingTheBrowserElement &&
    160      !isPopupToDebug(browsingContext, sessionContext)
    161    ) {
    162      return false;
    163    }
    164 
    165    // For client-side target switching, only mention the "remote frames".
    166    // i.e. the frames which are in a distinct process compared to their parent document
    167    // If there is no parent, this is most likely the top level document which we want to ignore.
    168    //
    169    // `forceAcceptTopLevelTarget` is set:
    170    // * when navigating to and from pages in the bfcache, we ignore client side target
    171    // and start emitting top level target from the server.
    172    // * when the callsite care about all the debugged browsing contexts,
    173    // no matter if their related targets are created by client or server.
    174    const isClientSideTargetSwitching =
    175      !sessionContext.isServerTargetSwitchingEnabled;
    176    const isTopLevelBrowsingContext = !browsingContext.parent;
    177    if (
    178      isClientSideTargetSwitching &&
    179      !forceAcceptTopLevelTarget &&
    180      isTopLevelBrowsingContext
    181    ) {
    182      return false;
    183    }
    184    return true;
    185  }
    186 
    187  if (sessionContext.type == "webextension") {
    188    // Next and last check expects a WindowGlobal.
    189    // As we have no way to really know if this BrowsingContext is related to this add-on,
    190    // ignore it. Even if callsite accepts browsing context without a window global.
    191    if (!windowGlobal) {
    192      return false;
    193    }
    194 
    195    return getAddonIdForWindowGlobal(windowGlobal) == sessionContext.addonId;
    196  }
    197  throw new Error("Unsupported session context type: " + sessionContext.type);
    198 }
    199 
    200 /**
    201 * Return true for popups to debug when debugging a browser-element.
    202 *
    203 * @param {BrowsingContext} browsingContext
    204 *        The browsing context we want to check if it is part of debugged context
    205 * @param {object} sessionContext
    206 *        WatcherActor's session context. This helps know what is the overall debugged scope.
    207 *        See watcher actor constructor for more info.
    208 */
    209 function isPopupToDebug(browsingContext, sessionContext) {
    210  // If enabled, create targets for popups (i.e. window.open() calls).
    211  // If the opener is the tab we are currently debugging, accept the WindowGlobal and create a target for it.
    212  //
    213  // Note that it is important to do this check *after* the isInitialDocument one.
    214  // Popups end up involving three WindowGlobals:
    215  // - a first WindowGlobal loading an initial about:blank document (so isInitialDocument is true)
    216  // - a second WindowGlobal which looks exactly as the first one
    217  // - a final WindowGlobal which loads the URL passed to window.open() (so isInitialDocument is false)
    218  //
    219  // For now, we only instantiate a target for the last WindowGlobal.
    220  return (
    221    sessionContext.isPopupDebuggingEnabled &&
    222    browsingContext.opener &&
    223    browsingContext.opener.browserId == sessionContext.browserId
    224  );
    225 }
    226 
    227 /**
    228 * Helper function of isBrowsingContextPartOfContext to execute all checks
    229 * against WindowGlobal interface which aren't specific to a given SessionContext type
    230 *
    231 * @param {WindowGlobalParent|WindowGlobalChild} windowGlobal
    232 *        The WindowGlobal we want to check if it is part of debugged context
    233 * @param {object} sessionContext
    234 *        The Session Context to help know what is debugged.
    235 *        See devtools/server/actors/watcher/session-context.js
    236 * @param {object} options
    237 *        Optional arguments passed via a dictionary.
    238 *        See `isBrowsingContextPartOfContext` jsdoc.
    239 * @param {boolean} options.acceptUncommitedInitialDocument
    240 *        By default, we ignore initial uncommitted about:blank documents/WindowGlobals
    241 *        as they might be transient while the initial load is ongoing.
    242 *        But some code cares about all the WindowGlobals.
    243 */
    244 function _validateWindowGlobal(
    245  windowGlobal,
    246  sessionContext,
    247  { acceptUncommitedInitialDocument }
    248 ) {
    249  // When creating a new DocShell and before loading anything, we immediately
    250  // create the initial about:blank. This is expected by the spec.
    251  // Occasionally, multiple such transient empty documents may be created.
    252  // These documents start out in the uncommitted state, but if a navigation to
    253  // `about:blank` occurs, they can become permanent by being committed to, and
    254  // a load event will be fired.
    255  //
    256  // `Document.isUncommittedInitialDocument` helps identify these transient documents,
    257  // which we want to ignore as they would instantiate very short lived targets which
    258  // confuses many tests and triggers race conditions by spamming many targets.
    259  //
    260  // For example, when using `window.open()` with cross-process loads, we may create four
    261  // such documents/WindowGlobals. We first get an initial about:blank document,
    262  // a second one when moving to the right principal, a third one when moving to the correct
    263  // process, and the fourth will is the actual document we expect to debug.
    264  // The second and third documents are implementation artifacts that ideally
    265  // wouldn't exist and aren't expected by the spec. These are implementation
    266  // details that may have already changed or could change in the future.
    267  //
    268  // Note that `window.print` and print preview are using `window.open` and are going through this.
    269  //
    270  // WindowGlobalParent will have `isUncommittedInitialDocument` attribute, while we have to go
    271  // through the Document for WindowGlobalChild.
    272  const isUncommittedInitialDocument =
    273    windowGlobal.isUncommittedInitialDocument ||
    274    windowGlobal.browsingContext.window?.document.isUncommittedInitialDocument;
    275  if (isUncommittedInitialDocument && !acceptUncommitedInitialDocument) {
    276    return false;
    277  }
    278 
    279  return true;
    280 }
    281 
    282 /**
    283 * Helper function to know if a given WindowGlobal should be debugged by scope
    284 * described by the given session context. This method could be called from any process
    285 * as so accept either WindowGlobalParent or WindowGlobalChild instances.
    286 *
    287 * @param {WindowGlobalParent|WindowGlobalChild} windowGlobal
    288 *        The WindowGlobal we want to check if it is part of debugged context
    289 * @param {object} sessionContext
    290 *        The Session Context to help know what is debugged.
    291 *        See devtools/server/actors/watcher/session-context.js
    292 * @param {object} options
    293 *        Optional arguments passed via a dictionary.
    294 *        See `isBrowsingContextPartOfContext` jsdoc.
    295 */
    296 export function isWindowGlobalPartOfContext(
    297  windowGlobal,
    298  sessionContext,
    299  options
    300 ) {
    301  return isBrowsingContextPartOfContext(
    302    windowGlobal.browsingContext,
    303    sessionContext,
    304    {
    305      ...options,
    306      windowGlobal,
    307    }
    308  );
    309 }
    310 
    311 /**
    312 * Get all the BrowsingContexts that should be debugged by the given session context.
    313 * Consider using WatcherActor.getAllBrowsingContexts() which will automatically pass the right sessionContext.
    314 *
    315 * Really all of them:
    316 * - For all the privileged windows (browser.xhtml, browser console, ...)
    317 * - For all chrome *and* content contexts (privileged windows, as well as <browser> elements and their inner content documents)
    318 * - For all nested browsing context. We fetch the contexts recursively.
    319 *
    320 * @param {object} sessionContext
    321 *        The Session Context to help know what is debugged.
    322 *        See devtools/server/actors/watcher/session-context.js
    323 */
    324 export function getAllBrowsingContextsForContext(sessionContext) {
    325  const browsingContexts = [];
    326 
    327  // For a given BrowsingContext, add the `browsingContext`
    328  // all of its children, that, recursively.
    329  function walk(browsingContext) {
    330    if (browsingContexts.includes(browsingContext)) {
    331      return;
    332    }
    333    browsingContexts.push(browsingContext);
    334 
    335    for (const child of browsingContext.children) {
    336      walk(child);
    337    }
    338 
    339    if (
    340      (sessionContext.type == "all" || sessionContext.type == "webextension") &&
    341      browsingContext.window
    342    ) {
    343      // If the document is in the parent process, also iterate over each <browser>'s browsing context.
    344      // BrowsingContext.children doesn't cross chrome to content boundaries,
    345      // so we have to cross these boundaries by ourself.
    346      // (This is also the reason why we aren't using BrowsingContext.getAllBrowsingContextsInSubtree())
    347      for (const browser of browsingContext.window.document.querySelectorAll(
    348        `browser[type="content"]`
    349      )) {
    350        walk(browser.browsingContext);
    351      }
    352    }
    353  }
    354 
    355  // If target a single browser element, only walk through its BrowsingContext
    356  if (sessionContext.type == "browser-element") {
    357    const topBrowsingContext = BrowsingContext.getCurrentTopByBrowserId(
    358      sessionContext.browserId
    359    );
    360    // topBrowsingContext can be null if getCurrentTopByBrowserId is called for a tab that is unloaded.
    361    if (topBrowsingContext?.embedderElement) {
    362      // Unfortunately, getCurrentTopByBrowserId is subject to race conditions and may refer to a BrowsingContext
    363      // that already navigated away.
    364      // Query the current "live" BrowsingContext by going through the embedder element (i.e. the <browser>/<iframe> element)
    365      // devtools/client/responsive/test/browser/browser_navigation.js covers this with fission enabled.
    366      const realTopBrowsingContext =
    367        topBrowsingContext.embedderElement.browsingContext;
    368      walk(realTopBrowsingContext);
    369    }
    370  } else if (
    371    sessionContext.type == "all" ||
    372    sessionContext.type == "webextension"
    373  ) {
    374    // For the browser toolbox and web extension, retrieve all possible BrowsingContext.
    375    // For WebExtension, we will then filter out the BrowsingContexts via `isBrowsingContextPartOfContext`.
    376    //
    377    // Fetch all top level window's browsing contexts
    378    for (const window of Services.ww.getWindowEnumerator()) {
    379      if (window.docShell.browsingContext) {
    380        walk(window.docShell.browsingContext);
    381      }
    382    }
    383  } else {
    384    throw new Error("Unsupported session context type: " + sessionContext.type);
    385  }
    386 
    387  return browsingContexts.filter(bc =>
    388    // We force accepting the top level browsing context, otherwise
    389    // it would only be returned if sessionContext.isServerSideTargetSwitching is enabled.
    390    isBrowsingContextPartOfContext(bc, sessionContext, {
    391      forceAcceptTopLevelTarget: true,
    392    })
    393  );
    394 }
    395 
    396 if (typeof module == "object") {
    397  // eslint-disable-next-line no-undef
    398  module.exports = {
    399    isBrowsingContextPartOfContext,
    400    isWindowGlobalPartOfContext,
    401    getAddonIdForWindowGlobal,
    402    getAllBrowsingContextsForContext,
    403  };
    404 }