tor-browser

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

NavigationManager.sys.mjs (34326B)


      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 { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
     11  BrowsingContextListener:
     12    "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
     13  DownloadListener:
     14    "chrome://remote/content/shared/listeners/DownloadListener.sys.mjs",
     15  generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
     16  Log: "chrome://remote/content/shared/Log.sys.mjs",
     17  NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
     18  ParentWebProgressListener:
     19    "chrome://remote/content/shared/listeners/ParentWebProgressListener.sys.mjs",
     20  PromptListener:
     21    "chrome://remote/content/shared/listeners/PromptListener.sys.mjs",
     22  registerWebDriverDocumentInsertedActor:
     23    "chrome://remote/content/shared/js-process-actors/WebDriverDocumentInsertedActor.sys.mjs",
     24  TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
     25  truncate: "chrome://remote/content/shared/Format.sys.mjs",
     26  unregisterWebDriverDocumentInsertedActor:
     27    "chrome://remote/content/shared/js-process-actors/WebDriverDocumentInsertedActor.sys.mjs",
     28 });
     29 
     30 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
     31 
     32 /**
     33 * @typedef {object} BrowsingContextDetails
     34 * @property {string} browsingContextId - The browsing context id.
     35 * @property {string} browserId - The id of the Browser owning the browsing
     36 *     context.
     37 * @property {BrowsingContext=} context - The BrowsingContext itself, if
     38 *     available.
     39 * @property {boolean} isTopBrowsingContext - Whether the browsing context is
     40 *     top level.
     41 */
     42 
     43 /**
     44 * Enum of all supported navigation manager events.
     45 *
     46 * @enum {string}
     47 */
     48 export const NAVIGATION_EVENTS = {
     49  DownloadEnd: "download-end",
     50  DownloadStarted: "download-started",
     51  FragmentNavigated: "fragment-navigated",
     52  HistoryUpdated: "history-updated",
     53  NavigationCommitted: "navigation-committed",
     54  NavigationFailed: "navigation-failed",
     55  NavigationStarted: "navigation-started",
     56  NavigationStopped: "navigation-stopped",
     57  SameDocumentChanged: "same-document-changed",
     58 };
     59 
     60 /**
     61 * Enum of navigation states.
     62 *
     63 * @enum {string}
     64 */
     65 export const NavigationState = {
     66  Registered: "registered",
     67  InitialAboutBlank: "initial-about-blank",
     68  Started: "started",
     69  Finished: "finished",
     70 };
     71 
     72 /**
     73 * @typedef {object} NavigationInfo
     74 * @property {boolean} committed - Whether the navigation was ever committed.
     75 * @property {string} contextId - ID of the browsing context.
     76 * @property {string} navigable - The UUID for the navigable.
     77 * @property {string} navigationId - The UUID for the navigation.
     78 * @property {NavigationState} state - The navigation state.
     79 * @property {string} url - The target url for the navigation.
     80 */
     81 
     82 /**
     83 * The NavigationRegistry is responsible for monitoring all navigations happening
     84 * in the browser.
     85 *
     86 * The NavigationRegistry singleton holds the map of navigations, from navigable
     87 * to NavigationInfo. It will also be called by WebProgressListenerParent
     88 * whenever a navigation event happens.
     89 *
     90 * This singleton is not exported outside of this class, and consumers instead
     91 * need to use the NavigationManager class. The NavigationRegistry keeps track
     92 * of how many NavigationListener instances are currently listening in order to
     93 * know if the WebProgressListenerActor should be registered or not.
     94 *
     95 * The NavigationRegistry exposes an API to retrieve the current or last
     96 * navigation for a given navigable, and also forwards events to notify about
     97 * navigation updates to individual NavigationManager instances.
     98 *
     99 * @class NavigationRegistry
    100 */
    101 class NavigationRegistry extends EventEmitter {
    102  #contextListener;
    103  #downloadListener;
    104  #downloadNavigations;
    105  #managers;
    106  #navigations;
    107  #promptListener;
    108  #webProgressListener;
    109 
    110  constructor() {
    111    super();
    112 
    113    // Set of NavigationManager instances currently used.
    114    this.#managers = new Set();
    115 
    116    // Maps navigable id to NavigationInfo.
    117    this.#navigations = new Map();
    118 
    119    // Keep track of ongoing download navigations, from Download object to
    120    // navigation id.
    121    this.#downloadNavigations = new WeakMap();
    122 
    123    this.#webProgressListener = new lazy.ParentWebProgressListener();
    124 
    125    this.#contextListener = new lazy.BrowsingContextListener();
    126    this.#contextListener.on("attached", this.#onContextAttached);
    127    this.#contextListener.on("discarded", this.#onContextDiscarded);
    128 
    129    this.#downloadListener = new lazy.DownloadListener();
    130    this.#downloadListener.on("download-started", this.#onDownloadStarted);
    131    this.#downloadListener.on("download-stopped", this.#onDownloadStopped);
    132 
    133    this.#promptListener = new lazy.PromptListener();
    134    this.#promptListener.on("closed", this.#onPromptClosed);
    135    this.#promptListener.on("opened", this.#onPromptOpened);
    136  }
    137 
    138  /**
    139   * Retrieve the last known navigation data for a given browsing context.
    140   *
    141   * @param {BrowsingContext} context
    142   *     The browsing context for which the navigation event was recorded.
    143   * @returns {NavigationInfo|null}
    144   *     The last known navigation data, or null.
    145   */
    146  getNavigationForBrowsingContext(context) {
    147    if (!lazy.TabManager.isValidCanonicalBrowsingContext(context)) {
    148      // Bail out if the provided context is not a valid CanonicalBrowsingContext
    149      // instance.
    150      return null;
    151    }
    152 
    153    const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context);
    154    if (!this.#navigations.has(navigableId)) {
    155      return null;
    156    }
    157 
    158    return this.#navigations.get(navigableId);
    159  }
    160 
    161  /**
    162   * Start monitoring navigations in all browsing contexts.
    163   */
    164  startMonitoring(listener) {
    165    if (this.#managers.size == 0) {
    166      lazy.registerWebDriverDocumentInsertedActor();
    167 
    168      this.#contextListener.startListening();
    169      this.#webProgressListener.startListening();
    170      this.#downloadListener.startListening();
    171      this.#promptListener.startListening();
    172    }
    173 
    174    this.#managers.add(listener);
    175  }
    176 
    177  /**
    178   * Stop monitoring navigations. This will clear the information collected
    179   * about navigations so far.
    180   */
    181  stopMonitoring(listener) {
    182    if (!this.#managers.has(listener)) {
    183      return;
    184    }
    185 
    186    this.#managers.delete(listener);
    187    if (this.#managers.size == 0) {
    188      this.#contextListener.stopListening();
    189      this.#webProgressListener.stopListening();
    190      this.#downloadListener.stopListening();
    191      this.#promptListener.stopListening();
    192 
    193      lazy.unregisterWebDriverDocumentInsertedActor();
    194 
    195      // Clear the map.
    196      this.#navigations = new Map();
    197    }
    198  }
    199 
    200  /**
    201   * This entry point is only intended to be called from
    202   * WebProgressListenerParent, to avoid setting up observers or listeners,
    203   * which are unnecessary since NavigationManager has to be a singleton.
    204   *
    205   * @param {object} data
    206   * @param {BrowsingContext} data.context
    207   *     The browsing context for which the navigation event was recorded.
    208   * @param {string} data.url
    209   *     The URL as string for the navigation.
    210   * @returns {NavigationInfo}
    211   *     The navigation created for this hash changed navigation.
    212   */
    213  notifyFragmentNavigated(data) {
    214    const { contextDetails, url } = data;
    215 
    216    const context = this.#getContextFromContextDetails(contextDetails);
    217    const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context);
    218 
    219    const navigationId = this.#getOrCreateNavigationId(navigableId);
    220    const navigation = this.#createNavigationObject({
    221      contextId: context.id,
    222      state: NavigationState.Finished,
    223      navigationId,
    224      url,
    225    });
    226 
    227    // Update the current navigation for the navigable only if there is no
    228    // ongoing navigation for the navigable.
    229    const currentNavigation = this.#navigations.get(navigableId);
    230    if (
    231      !currentNavigation ||
    232      currentNavigation.state == NavigationState.Finished
    233    ) {
    234      this.#navigations.set(navigableId, navigation);
    235    }
    236 
    237    // Hash change navigations are immediately done, fire a single event.
    238    this.emit(NAVIGATION_EVENTS.FragmentNavigated, {
    239      navigationId,
    240      navigableId,
    241      url,
    242    });
    243 
    244    return navigation;
    245  }
    246 
    247  /**
    248   * Called when a history updated event is recorded from the
    249   * WebProgressListener actors.
    250   *
    251   * This entry point is only intended to be called from
    252   * WebProgressListenerParent, to avoid setting up observers or listeners,
    253   * which are unnecessary since NavigationManager has to be a singleton.
    254   *
    255   * Note that a history-updated event should not create a new navigation, or
    256   * generate a new navigation id.
    257   *
    258   * @param {object} data
    259   * @param {BrowsingContext} data.context
    260   *     The browsing context for which the navigation event was recorded.
    261   * @param {string} data.url
    262   *     The URL as string for the navigation.
    263   */
    264  notifyHistoryUpdated(data) {
    265    const { contextDetails, url } = data;
    266 
    267    const context = this.#getContextFromContextDetails(contextDetails);
    268    const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context);
    269 
    270    // History updates are immediately done, fire a single event.
    271    this.emit(NAVIGATION_EVENTS.HistoryUpdated, {
    272      contextId: context.id,
    273      navigableId,
    274      url,
    275    });
    276  }
    277 
    278  /**
    279   * Called when a same-document navigation is recorded from the
    280   * WebProgressListener actors.
    281   *
    282   * This entry point is only intended to be called from
    283   * WebProgressListenerParent, to avoid setting up observers or listeners,
    284   * which are unnecessary since NavigationManager has to be a singleton.
    285   *
    286   * @param {object} data
    287   * @param {BrowsingContext} data.context
    288   *     The browsing context for which the navigation event was recorded.
    289   * @param {string} data.url
    290   *     The URL as string for the navigation.
    291   * @returns {NavigationInfo}
    292   *     The navigation created for this same-document navigation.
    293   */
    294  notifySameDocumentChanged(data) {
    295    const { contextDetails, url } = data;
    296 
    297    const context = this.#getContextFromContextDetails(contextDetails);
    298    const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context);
    299 
    300    const navigationId = this.#getOrCreateNavigationId(navigableId);
    301    const navigation = this.#createNavigationObject({
    302      state: NavigationState.Finished,
    303      navigationId,
    304      url,
    305    });
    306 
    307    // Update the current navigation for the navigable only if there is no
    308    // ongoing navigation for the navigable.
    309    const currentNavigation = this.#navigations.get(navigableId);
    310    if (
    311      !currentNavigation ||
    312      currentNavigation.state == NavigationState.Finished
    313    ) {
    314      this.#navigations.set(navigableId, navigation);
    315    }
    316 
    317    // Same document navigations are immediately done, fire a single event.
    318 
    319    this.emit(NAVIGATION_EVENTS.SameDocumentChanged, {
    320      navigationId,
    321      navigableId,
    322      url,
    323    });
    324 
    325    return navigation;
    326  }
    327 
    328  /**
    329   * Called when a `document-inserted` event is recorded from the
    330   * WebDriverDocumentInserted actors.
    331   *
    332   * This entry point is only intended to be called from
    333   * WebDriverDocumentInsertedParent, to avoid setting up
    334   * observers or listeners, which are unnecessary since
    335   * NavigationManager has to be a singleton.
    336   *
    337   * @param {object} data
    338   * @param {BrowsingContextDetails} data.contextDetails
    339   *     The details about the browsing context for this navigation.
    340   * @param {string} data.errorName
    341   *     The error message.
    342   * @param {string} data.url
    343   *     The URL as string for the navigation.
    344   * @returns {NavigationInfo}
    345   *     The created navigation or the ongoing navigation, if applicable.
    346   */
    347  notifyNavigationCommitted(data) {
    348    const { contextDetails, errorName, url } = data;
    349 
    350    const context = this.#getContextFromContextDetails(contextDetails);
    351    const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context);
    352    const navigation = this.#navigations.get(navigableId);
    353 
    354    if (!navigation) {
    355      lazy.logger.trace(
    356        lazy.truncate`[${navigableId}] No navigation found to commit for url: ${url}`
    357      );
    358      return null;
    359    }
    360 
    361    // We don't want to notify that navigation for "about:blank" (or "about:blank" with parameter)
    362    // is committed if it happens when the top-level browsing context is created.
    363    if (
    364      navigation.state === NavigationState.InitialAboutBlank &&
    365      new URL(url).pathname == "blank"
    366    ) {
    367      lazy.logger.trace(
    368        `[${navigableId}] Skipping this navigation for url: ${navigation.url}, since it's an initial navigation.`
    369      );
    370      return navigation;
    371    }
    372 
    373    // Flag the navigation as committed. We don't set it as the state, because
    374    // we need to know if at some point a navigation was committed, regardless
    375    // of its current state (eg finished).
    376    navigation.committed = true;
    377 
    378    lazy.logger.trace(
    379      lazy.truncate`[${navigableId}] Navigation committed for url: ${url} (${navigation.navigationId})`
    380    );
    381 
    382    this.emit(NAVIGATION_EVENTS.NavigationCommitted, {
    383      contextId: context.id,
    384      errorName,
    385      navigationId: navigation.navigationId,
    386      navigableId,
    387      url,
    388    });
    389 
    390    return navigation;
    391  }
    392 
    393  /**
    394   * Called when a navigation-failed event is recorded from the
    395   * WebProgressListener actors.
    396   *
    397   * This entry point is only intended to be called from
    398   * WebProgressListenerParent, to avoid setting up observers or listeners,
    399   * which are unnecessary since NavigationManager has to be a singleton.
    400   *
    401   * @param {object} data
    402   * @param {BrowsingContextDetails} data.contextDetails
    403   *     The details about the browsing context for this navigation.
    404   * @param {string} data.errorName
    405   *     The error message.
    406   * @param {string} data.url
    407   *     The URL as string for the navigation.
    408   * @returns {NavigationInfo}
    409   *     The created navigation or the ongoing navigation, if applicable.
    410   */
    411  notifyNavigationFailed(data) {
    412    const { contextDetails, errorName, url } = data;
    413 
    414    const context = this.#getContextFromContextDetails(contextDetails);
    415    const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context);
    416 
    417    const navigation = this.#navigations.get(navigableId);
    418 
    419    if (!navigation) {
    420      lazy.logger.trace(
    421        lazy.truncate`[${navigableId}] No navigation found to fail for url: ${url}`
    422      );
    423      return null;
    424    }
    425 
    426    if (navigation.state === NavigationState.Finished) {
    427      lazy.logger.trace(
    428        `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}`
    429      );
    430      return navigation;
    431    }
    432 
    433    lazy.logger.trace(
    434      lazy.truncate`[${navigableId}] Navigation failed for url: ${url} (${navigation.navigationId})`
    435    );
    436 
    437    navigation.state = NavigationState.Finished;
    438 
    439    this.emit(NAVIGATION_EVENTS.NavigationFailed, {
    440      contextId: context.id,
    441      errorName,
    442      navigationId: navigation.navigationId,
    443      navigableId,
    444      url,
    445    });
    446 
    447    return navigation;
    448  }
    449 
    450  /**
    451   * Called when a navigation-started event is recorded from the
    452   * WebProgressListener actors.
    453   *
    454   * This entry point is only intended to be called from
    455   * WebProgressListenerParent, to avoid setting up observers or listeners,
    456   * which are unnecessary since NavigationManager has to be a singleton.
    457   *
    458   * @param {object} data
    459   * @param {BrowsingContextDetails} data.contextDetails
    460   *     The details about the browsing context for this navigation.
    461   * @param {string} data.url
    462   *     The URL as string for the navigation.
    463   * @returns {NavigationInfo}
    464   *     The created navigation or the ongoing navigation, if applicable.
    465   */
    466  notifyNavigationStarted(data) {
    467    const { contextDetails, url } = data;
    468    const context = this.#getContextFromContextDetails(contextDetails);
    469    const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context);
    470 
    471    let navigation = this.#navigations.get(navigableId);
    472 
    473    // For top-level navigations, `context` is the current browsing context for
    474    // the browser with id = `contextDetails.browserId`.
    475    // If the navigation replaced the browsing contexts, retrieve the original
    476    // browsing context to check if the event is relevant.
    477    const originalContext = BrowsingContext.get(
    478      contextDetails.browsingContextId
    479    );
    480 
    481    // If we have a previousNavigation for the same URL, and the browsing
    482    // context for this event (originalContext) is outdated, skip the event.
    483    // Any further event from this browsing context will come with the aborted
    484    // flag set and will also be ignored.
    485    // Bug 1930616: Moving the NavigationManager to the parent process should
    486    // hopefully make this irrelevant.
    487    if (
    488      url == navigation?.url &&
    489      context != originalContext &&
    490      !context.isReplaced &&
    491      originalContext?.isReplaced
    492    ) {
    493      return null;
    494    }
    495 
    496    if (navigation) {
    497      if (navigation.state === NavigationState.Started) {
    498        // Bug 1908952. As soon as we have support for the "url" field in case of beforeunload
    499        // prompt being open, we can remove "!navigation.url" check.
    500        if (!navigation.url || navigation.url === url) {
    501          // If we are already monitoring a navigation for this navigable and the same url,
    502          // for which we did not receive a navigation-stopped event, this navigation
    503          // is already tracked and we don't want to create another id & event.
    504          lazy.logger.trace(
    505            `[${navigableId}] Skipping already tracked navigation, navigationId: ${navigation.navigationId}`
    506          );
    507          return navigation;
    508        }
    509 
    510        lazy.logger.trace(
    511          `[${navigableId}] We're going to fail the navigation for url: ${navigation.url} (${navigation.navigationId}), ` +
    512            "since it was interrupted by a new navigation."
    513        );
    514 
    515        // If there is already a navigation in progress but with a different url,
    516        // it means that this navigation was interrupted by a new navigation.
    517        // Note: ideally we should monitor this using NS_BINDING_ABORTED,
    518        // but due to intermittent issues, when monitoring this in content processes,
    519        // we can't reliable use it.
    520        notifyNavigationFailed({
    521          contextDetails,
    522          errorName: "A new navigation interrupted an unfinished navigation",
    523          url: navigation.url,
    524        });
    525      }
    526 
    527      // We don't want to notify that navigation for "about:blank" (or "about:blank" with parameter)
    528      // has started if it happens when the top-level browsing context is created.
    529      if (
    530        navigation.state === NavigationState.InitialAboutBlank &&
    531        new URL(url).pathname == "blank"
    532      ) {
    533        lazy.logger.trace(
    534          `[${navigableId}] Skipping this navigation for url: ${navigation.url}, since it's an initial navigation.`
    535        );
    536        return navigation;
    537      }
    538    }
    539 
    540    const navigationId = this.#getOrCreateNavigationId(navigableId);
    541    navigation = this.#createNavigationObject({
    542      state: NavigationState.Started,
    543      navigationId,
    544      url,
    545    });
    546    this.#navigations.set(navigableId, navigation);
    547 
    548    lazy.logger.trace(
    549      lazy.truncate`[${navigableId}] Navigation started for url: ${url} (${navigationId})`
    550    );
    551 
    552    this.emit(NAVIGATION_EVENTS.NavigationStarted, {
    553      contextId: context.id,
    554      navigationId,
    555      navigableId,
    556      url,
    557    });
    558 
    559    return navigation;
    560  }
    561 
    562  /**
    563   * Called when a navigation-stopped event is recorded from the
    564   * WebProgressListener actors.
    565   *
    566   * @param {object} data
    567   * @param {BrowsingContextDetails} data.contextDetails
    568   *     The details about the browsing context for this navigation.
    569   * @param {string} data.url
    570   *     The URL as string for the navigation.
    571   * @returns {NavigationInfo}
    572   *     The stopped navigation if any, or null.
    573   */
    574  notifyNavigationStopped(data) {
    575    const { contextDetails, url } = data;
    576 
    577    const context = this.#getContextFromContextDetails(contextDetails);
    578    const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context);
    579 
    580    const navigation = this.#navigations.get(navigableId);
    581    if (!navigation) {
    582      lazy.logger.trace(
    583        lazy.truncate`[${navigableId}] No navigation found to stop for url: ${url}`
    584      );
    585      return null;
    586    }
    587 
    588    if (navigation.state === NavigationState.Finished) {
    589      lazy.logger.trace(
    590        `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}`
    591      );
    592      return navigation;
    593    }
    594 
    595    lazy.logger.trace(
    596      lazy.truncate`[${navigableId}] Navigation finished for url: ${url} (${navigation.navigationId})`
    597    );
    598 
    599    navigation.state = NavigationState.Finished;
    600 
    601    this.emit(NAVIGATION_EVENTS.NavigationStopped, {
    602      navigationId: navigation.navigationId,
    603      navigableId,
    604      url,
    605    });
    606 
    607    return navigation;
    608  }
    609 
    610  /**
    611   * Register a navigation id to be used for the next navigation for the
    612   * provided browsing context details.
    613   *
    614   * @param {object} data
    615   * @param {BrowsingContextDetails} data.contextDetails
    616   *     The details about the browsing context for this navigation.
    617   * @returns {string}
    618   *     The UUID created the upcoming navigation.
    619   */
    620  registerNavigationId(data) {
    621    const { contextDetails } = data;
    622    const context = this.#getContextFromContextDetails(contextDetails);
    623    const navigableId = lazy.NavigableManager.getIdForBrowsingContext(context);
    624 
    625    const existingNavigation = this.#navigations.get(navigableId);
    626    if (
    627      existingNavigation &&
    628      existingNavigation.state === NavigationState.Started
    629    ) {
    630      lazy.logger.trace(
    631        `[${navigableId}] We're going to fail the navigation for url: ${existingNavigation.url} (${existingNavigation.navigationId}), ` +
    632          "since it was interrupted by a new navigation."
    633      );
    634 
    635      // If there is already a navigation in progress but with a different url,
    636      // it means that this navigation was interrupted by a new navigation.
    637      // Note: ideally we should monitor this using NS_BINDING_ABORTED,
    638      // but due to intermittent issues, when monitoring this in content processes,
    639      // we can't reliable use it.
    640      notifyNavigationFailed({
    641        contextDetails,
    642        errorName: "A new navigation interrupted an unfinished navigation",
    643        url: existingNavigation.url,
    644      });
    645    }
    646 
    647    const navigationId = lazy.generateUUID();
    648    const navigation = this.#createNavigationObject({
    649      state: NavigationState.registered,
    650      navigationId,
    651    });
    652    this.#navigations.set(navigableId, navigation);
    653 
    654    return navigationId;
    655  }
    656 
    657  #createNavigationObject(params) {
    658    const { state, navigationId, url } = params;
    659    return {
    660      committed: false,
    661      state,
    662      navigationId,
    663      url,
    664    };
    665  }
    666 
    667  #getContextFromContextDetails(contextDetails) {
    668    if (contextDetails.context) {
    669      return contextDetails.context;
    670    }
    671 
    672    return contextDetails.isContent && contextDetails.isTopBrowsingContext
    673      ? BrowsingContext.getCurrentTopByBrowserId(contextDetails.browserId)
    674      : BrowsingContext.get(contextDetails.browsingContextId);
    675  }
    676 
    677  #getOrCreateNavigationId(navigableId) {
    678    const navigation = this.#navigations.get(navigableId);
    679    if (
    680      navigation !== undefined &&
    681      navigation.state === NavigationState.registered
    682    ) {
    683      return navigation.navigationId;
    684    }
    685    return lazy.generateUUID();
    686  }
    687 
    688  #onContextAttached = async (eventName, data) => {
    689    const { browsingContext, why } = data;
    690 
    691    // We only care about top-level browsing contexts.
    692    if (browsingContext.parent !== null) {
    693      return;
    694    }
    695    // Filter out top-level browsing contexts that are created because of a
    696    // cross-group navigation.
    697    if (why === "replace") {
    698      return;
    699    }
    700 
    701    const navigableId =
    702      lazy.NavigableManager.getIdForBrowsingContext(browsingContext);
    703    let navigation = this.#navigations.get(navigableId);
    704 
    705    if (navigation) {
    706      return;
    707    }
    708 
    709    const navigationId = this.#getOrCreateNavigationId(navigableId);
    710    navigation = {
    711      state: NavigationState.InitialAboutBlank,
    712      navigationId,
    713      url: browsingContext.currentURI.displaySpec,
    714    };
    715    this.#navigations.set(navigableId, navigation);
    716  };
    717 
    718  #onContextDiscarded = async (eventName, data = {}) => {
    719    const { browsingContext, why } = data;
    720 
    721    // Filter out top-level browsing contexts that are destroyed because of a
    722    // cross-group navigation.
    723    if (why === "replace") {
    724      return;
    725    }
    726 
    727    // TODO: Bug 1852941. We should also filter out events which are emitted
    728    // for DevTools frames.
    729 
    730    // Filter out notifications for chrome context until support gets
    731    // added (bug 1722679).
    732    if (!browsingContext.webProgress) {
    733      return;
    734    }
    735 
    736    // Filter out notifications for webextension contexts until support gets
    737    // added (bug 1755014).
    738    if (browsingContext.currentRemoteType === "extension") {
    739      return;
    740    }
    741 
    742    const navigableId =
    743      lazy.NavigableManager.getIdForBrowsingContext(browsingContext);
    744    const navigation = this.#navigations.get(navigableId);
    745 
    746    // No need to fail navigation, if there is no navigation in progress.
    747    if (!navigation) {
    748      return;
    749    }
    750 
    751    notifyNavigationFailed({
    752      contextDetails: {
    753        context: browsingContext,
    754      },
    755      errorName: "Browsing context got discarded",
    756      url: navigation.url,
    757    });
    758 
    759    // If the navigable is discarded, we can safely clean up the navigation info.
    760    this.#navigations.delete(navigableId);
    761  };
    762 
    763  #onDownloadStarted = (eventName, data) => {
    764    const { download } = data;
    765 
    766    const contextId = download.source.browsingContextId;
    767    const browsingContext = BrowsingContext.get(contextId);
    768    if (!browsingContext) {
    769      return;
    770    }
    771 
    772    const navigableId =
    773      lazy.NavigableManager.getIdForBrowsingContext(browsingContext);
    774    const url = download.source.url;
    775 
    776    const navigation = this.#navigations.get(navigableId);
    777    let navigationId = null;
    778    if (navigation && navigation.state === NavigationState.Started) {
    779      // navigationId is optional and should only be set if there is an ongoing
    780      // navigation.
    781      navigationId = navigation.navigationId;
    782      // Track the navigation id for this download object, for the upcoming
    783      // NAVIGATION_EVENTS.DownloadEnd event.
    784      this.#downloadNavigations.set(download, navigationId);
    785    }
    786 
    787    // Tracking navigations is delegated to the DownloadListener. It is exposed
    788    // via the DownloadManager for consistency and also to enforce having a
    789    // singleton and consistent navigation ids across sessions.
    790    this.emit(NAVIGATION_EVENTS.DownloadStarted, {
    791      contextId: browsingContext.id,
    792      navigationId,
    793      navigableId,
    794      suggestedFilename: PathUtils.filename(download.target.path),
    795      timestamp: download.startTime.getTime(),
    796      url,
    797    });
    798  };
    799 
    800  #onDownloadStopped = (eventName, data) => {
    801    const { download } = data;
    802 
    803    const contextId = download.source.browsingContextId;
    804    const browsingContext = BrowsingContext.get(contextId);
    805    if (!browsingContext) {
    806      return;
    807    }
    808 
    809    const navigableId =
    810      lazy.NavigableManager.getIdForBrowsingContext(browsingContext);
    811    const url = download.source.url;
    812 
    813    let navigationId = null;
    814    if (this.#downloadNavigations.has(download)) {
    815      navigationId = this.#downloadNavigations.get(download);
    816    }
    817 
    818    const canceled = download.canceled || download.error;
    819    this.emit(NAVIGATION_EVENTS.DownloadEnd, {
    820      canceled,
    821      contextId: browsingContext.id,
    822      filepath: download.target.path,
    823      navigableId,
    824      navigationId,
    825      timestamp: download.endTime,
    826      url,
    827    });
    828  };
    829 
    830  #onPromptClosed = (eventName, data) => {
    831    const { contentBrowser, detail } = data;
    832    const { accepted, browsingContext, promptType } = detail;
    833 
    834    // Send navigation failed event if beforeunload prompt was rejected.
    835    if (promptType === "beforeunload" && accepted === false) {
    836      // TODO: Bug 2007385. We can remove this fallback
    837      // when we have support for browsing context property in event details on Android.
    838      const context = lazy.AppInfo.isAndroid
    839        ? contentBrowser.browsingContext
    840        : browsingContext;
    841      notifyNavigationFailed({
    842        contextDetails: {
    843          context,
    844        },
    845        errorName: "Beforeunload prompt was rejected",
    846        // Bug 1908952. Add support for the "url" field.
    847      });
    848    }
    849  };
    850 
    851  #onPromptOpened = (eventName, data) => {
    852    const { browsingContext, contentBrowser, prompt } = data;
    853    const { promptType } = prompt;
    854 
    855    // We should start the navigation when beforeunload prompt is open.
    856    if (promptType === "beforeunload") {
    857      // TODO: Bug 2007385. We can remove this fallback
    858      // when we have support for browsing context property in event details on Android.
    859      const context = lazy.AppInfo.isAndroid
    860        ? contentBrowser.browsingContext
    861        : browsingContext;
    862      notifyNavigationStarted({
    863        contextDetails: {
    864          context,
    865        },
    866        // Bug 1908952. Add support for the "url" field.
    867      });
    868    }
    869  };
    870 }
    871 
    872 // Create a private NavigationRegistry singleton.
    873 const navigationRegistry = new NavigationRegistry();
    874 
    875 /**
    876 * See NavigationRegistry.notifyFragmentNavigated.
    877 *
    878 * This entry point is only intended to be called from WebProgressListenerParent,
    879 * to avoid setting up observers or listeners, which are unnecessary since
    880 * NavigationRegistry has to be a singleton.
    881 */
    882 export function notifyFragmentNavigated(data) {
    883  return navigationRegistry.notifyFragmentNavigated(data);
    884 }
    885 
    886 /**
    887 * See NavigationRegistry.notifyHistoryUpdated.
    888 *
    889 * This entry point is only intended to be called from WebProgressListenerParent,
    890 * to avoid setting up observers or listeners, which are unnecessary since
    891 * NavigationRegistry has to be a singleton.
    892 */
    893 export function notifyHistoryUpdated(data) {
    894  return navigationRegistry.notifyHistoryUpdated(data);
    895 }
    896 
    897 /**
    898 * See NavigationRegistry.notifySameDocumentChanged.
    899 *
    900 * This entry point is only intended to be called from WebProgressListenerParent,
    901 * to avoid setting up observers or listeners, which are unnecessary since
    902 * NavigationRegistry has to be a singleton.
    903 */
    904 export function notifySameDocumentChanged(data) {
    905  return navigationRegistry.notifySameDocumentChanged(data);
    906 }
    907 
    908 /**
    909 * See NavigationRegistry.notifyNavigationCommitted.
    910 *
    911 * This entry point is only intended to be called from WebProgressListenerParent,
    912 * to avoid setting up observers or listeners, which are unnecessary since
    913 * NavigationRegistry has to be a singleton.
    914 */
    915 export function notifyNavigationCommitted(data) {
    916  return navigationRegistry.notifyNavigationCommitted(data);
    917 }
    918 
    919 /**
    920 * See NavigationRegistry.notifyNavigationFailed.
    921 *
    922 * This entry point is only intended to be called from WebProgressListenerParent,
    923 * to avoid setting up observers or listeners, which are unnecessary since
    924 * NavigationRegistry has to be a singleton.
    925 */
    926 export function notifyNavigationFailed(data) {
    927  return navigationRegistry.notifyNavigationFailed(data);
    928 }
    929 
    930 /**
    931 * See NavigationRegistry.notifyNavigationStarted.
    932 *
    933 * This entry point is only intended to be called from WebProgressListenerParent,
    934 * to avoid setting up observers or listeners, which are unnecessary since
    935 * NavigationRegistry has to be a singleton.
    936 */
    937 export function notifyNavigationStarted(data) {
    938  return navigationRegistry.notifyNavigationStarted(data);
    939 }
    940 
    941 /**
    942 * See NavigationRegistry.notifyNavigationStopped.
    943 *
    944 * This entry point is only intended to be called from WebProgressListenerParent,
    945 * to avoid setting up observers or listeners, which are unnecessary since
    946 * NavigationRegistry has to be a singleton.
    947 */
    948 export function notifyNavigationStopped(data) {
    949  return navigationRegistry.notifyNavigationStopped(data);
    950 }
    951 
    952 export function registerNavigationId(data) {
    953  return navigationRegistry.registerNavigationId(data);
    954 }
    955 
    956 /**
    957 * The NavigationManager exposes the NavigationRegistry data via a class which
    958 * needs to be individually instantiated by each consumer. This allow to track
    959 * how many consumers need navigation data at any point so that the
    960 * NavigationRegistry can register or unregister the underlying listeners/actors
    961 * correctly.
    962 *
    963 * @fires NavigationManager#"navigation-started"
    964 *    The NavigationManager emits "navigation-started" when a new navigation is
    965 *    detected, with the following object as payload:
    966 *      - {string} navigationId - The UUID for the navigation.
    967 *      - {string} navigableId - The UUID for the navigable.
    968 *      - {string} url - The target url for the navigation.
    969 * @fires NavigationManager#"navigation-stopped"
    970 *    The NavigationManager emits "navigation-stopped" when a known navigation
    971 *    is stopped, with the following object as payload:
    972 *      - {string} navigationId - The UUID for the navigation.
    973 *      - {string} navigableId - The UUID for the navigable.
    974 *      - {string} url - The target url for the navigation.
    975 */
    976 export class NavigationManager extends EventEmitter {
    977  #monitoring;
    978 
    979  constructor() {
    980    super();
    981 
    982    this.#monitoring = false;
    983  }
    984 
    985  destroy() {
    986    this.stopMonitoring();
    987  }
    988 
    989  getNavigationForBrowsingContext(context) {
    990    return navigationRegistry.getNavigationForBrowsingContext(context);
    991  }
    992 
    993  startMonitoring() {
    994    if (this.#monitoring) {
    995      return;
    996    }
    997 
    998    this.#monitoring = true;
    999    navigationRegistry.startMonitoring(this);
   1000    for (const eventName of Object.values(NAVIGATION_EVENTS)) {
   1001      navigationRegistry.on(eventName, this.#onNavigationEvent);
   1002    }
   1003  }
   1004 
   1005  stopMonitoring() {
   1006    if (!this.#monitoring) {
   1007      return;
   1008    }
   1009 
   1010    this.#monitoring = false;
   1011    navigationRegistry.stopMonitoring(this);
   1012    for (const eventName of Object.values(NAVIGATION_EVENTS)) {
   1013      navigationRegistry.off(eventName, this.#onNavigationEvent);
   1014    }
   1015  }
   1016 
   1017  #onNavigationEvent = (eventName, data) => {
   1018    this.emit(eventName, data);
   1019  };
   1020 }