Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  NavigationManager.sys.mjs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BrowsingContextListener:
    "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
  generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
  Log: "chrome://remote/content/shared/Log.sys.mjs",
  PromptListener:
    "chrome://remote/content/shared/listeners/PromptListener.sys.mjs",
  registerNavigationListenerActor:
    "chrome://remote/content/shared/js-window-actors/NavigationListenerActor.sys.mjs",
  TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
  truncate: "chrome://remote/content/shared/Format.sys.mjs",
  unregisterNavigationListenerActor:
    "chrome://remote/content/shared/js-window-actors/NavigationListenerActor.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());

/**
 * @typedef {object} BrowsingContextDetails
 * @property {string} browsingContextId - The browsing context id.
 * @property {string} browserId - The id of the Browser owning the browsing
 *     context.
 * @property {BrowsingContext=} context - The BrowsingContext itself, if
 *     available.
 * @property {boolean} isTopBrowsingContext - Whether the browsing context is
 *     top level.
 */

/**
 * @typedef {object} NavigationInfo
 * @property {'initial-about-blank'|'registered'|'started'|'finished'} state - The navigation state.
 * @property {string} navigationId - The UUID for the navigation.
 * @property {string} navigable - The UUID for the navigable.
 * @property {string} url - The target url for the navigation.
 */

/**
 * The NavigationRegistry is responsible for monitoring all navigations happening
 * in the browser.
 *
 * It relies on a JSWindowActor pair called NavigationListener{Parent|Child},
 * found under remote/shared/js-window-actors. As a simple overview, the
 * NavigationListenerChild will monitor navigations in all window globals using
 * content process WebProgressListener, and will forward each relevant update to
 * the NavigationListenerParent
 *
 * The NavigationRegistry singleton holds the map of navigations, from navigable
 * to NavigationInfo. It will also be called by NavigationListenerParent
 * whenever a navigation event happens.
 *
 * This singleton is not exported outside of this class, and consumers instead
 * need to use the NavigationManager class. The NavigationRegistry keeps track
 * of how many NavigationListener instances are currently listening in order to
 * know if the NavigationListenerActor should be registered or not.
 *
 * The NavigationRegistry exposes an API to retrieve the current or last
 * navigation for a given navigable, and also forwards events to notify about
 * navigation updates to individual NavigationManager instances.
 *
 * @class NavigationRegistry
 */
class NavigationRegistry extends EventEmitter {
  #contextListener;
  #managers;
  #navigations;
  #promptListener;

  constructor() {
    super();

    // Set of NavigationManager instances currently used.
    this.#managers = new Set();

    // Maps navigable id to NavigationInfo.
    this.#navigations = new Map();

    this.#contextListener = new lazy.BrowsingContextListener();
    this.#contextListener.on("attached", this.#onContextAttached);
    this.#contextListener.on("discarded", this.#onContextDiscarded);

    this.#promptListener = new lazy.PromptListener();
    this.#promptListener.on("closed", this.#onPromptClosed);
    this.#promptListener.on("opened", this.#onPromptOpened);
  }

  /**
   * Retrieve the last known navigation data for a given browsing context.
   *
   * @param {BrowsingContext} context
   *     The browsing context for which the navigation event was recorded.
   * @returns {NavigationInfo|null}
   *     The last known navigation data, or null.
   */
  getNavigationForBrowsingContext(context) {
    if (!lazy.TabManager.isValidCanonicalBrowsingContext(context)) {
      // Bail out if the provided context is not a valid CanonicalBrowsingContext
      // instance.
      return null;
    }

    const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
    if (!this.#navigations.has(navigableId)) {
      return null;
    }

    return this.#navigations.get(navigableId);
  }

  /**
   * Start monitoring navigations in all browsing contexts. This will register
   * the NavigationListener JSWindowActor and will initialize them in all
   * existing browsing contexts.
   */
  startMonitoring(listener) {
    if (this.#managers.size == 0) {
      lazy.registerNavigationListenerActor();
      this.#contextListener.startListening();
      this.#promptListener.startListening();
    }

    this.#managers.add(listener);
  }

  /**
   * Stop monitoring navigations. This will unregister the NavigationListener
   * JSWindowActor and clear the information collected about navigations so far.
   */
  stopMonitoring(listener) {
    if (!this.#managers.has(listener)) {
      return;
    }

    this.#managers.delete(listener);
    if (this.#managers.size == 0) {
      this.#contextListener.stopListening();
      this.#promptListener.stopListening();
      lazy.unregisterNavigationListenerActor();
      // Clear the map.
      this.#navigations = new Map();
    }
  }

  /**
   * Called when a fragment navigation is recorded from the
   * NavigationListener actors.
   *
   * This entry point is only intended to be called from
   * NavigationListenerParent, to avoid setting up observers or listeners,
   * which are unnecessary since NavigationManager has to be a singleton.
   *
   * @param {object} data
   * @param {BrowsingContext} data.context
   *     The browsing context for which the navigation event was recorded.
   * @param {string} data.url
   *     The URL as string for the navigation.
   * @returns {NavigationInfo}
   *     The navigation created for this hash changed navigation.
   */
  notifyFragmentNavigated(data) {
    const { contextDetails, url } = data;

    const context = this.#getContextFromContextDetails(contextDetails);
    const navigableId = lazy.TabManager.getIdForBrowsingContext(context);

    const navigationId = this.#getOrCreateNavigationId(navigableId);
    const navigation = { state: "finished", navigationId, url };

    // Update the current navigation for the navigable only if there is no
    // ongoing navigation for the navigable.
    const currentNavigation = this.#navigations.get(navigableId);
    if (!currentNavigation || currentNavigation.state == "finished") {
      this.#navigations.set(navigableId, navigation);
    }

    // Hash change navigations are immediately done, fire a single event.
    this.emit("fragment-navigated", { navigationId, navigableId, url });

    return navigation;
  }
  /**
   * Called when a same-document navigation is recorded from the
   * NavigationListener actors.
   *
   * This entry point is only intended to be called from
   * NavigationListenerParent, to avoid setting up observers or listeners,
   * which are unnecessary since NavigationManager has to be a singleton.
   *
   * @param {object} data
   * @param {BrowsingContext} data.context
   *     The browsing context for which the navigation event was recorded.
   * @param {string} data.url
   *     The URL as string for the navigation.
   * @returns {NavigationInfo}
   *     The navigation created for this same-document navigation.
   */
  notifySameDocumentChanged(data) {
    const { contextDetails, url } = data;

    const context = this.#getContextFromContextDetails(contextDetails);
    const navigableId = lazy.TabManager.getIdForBrowsingContext(context);

    const navigationId = this.#getOrCreateNavigationId(navigableId);
    const navigation = { state: "finished", navigationId, url };

    // Update the current navigation for the navigable only if there is no
    // ongoing navigation for the navigable.
    const currentNavigation = this.#navigations.get(navigableId);
    if (!currentNavigation || currentNavigation.state == "finished") {
      this.#navigations.set(navigableId, navigation);
    }

    // Same document navigations are immediately done, fire a single event.

    this.emit("same-document-changed", { navigationId, navigableId, url });

    return navigation;
  }

  /**
   * Called when a navigation-failed event is recorded from the
   * NavigationListener actors.
   *
   * This entry point is only intended to be called from
   * NavigationListenerParent, to avoid setting up observers or listeners,
   * which are unnecessary since NavigationManager has to be a singleton.
   *
   * @param {object} data
   * @param {BrowsingContextDetails} data.contextDetails
   *     The details about the browsing context for this navigation.
   * @param {string} data.errorName
   *     The error message.
   * @param {string} data.url
   *     The URL as string for the navigation.
   * @returns {NavigationInfo}
   *     The created navigation or the ongoing navigation, if applicable.
   */
  notifyNavigationFailed(data) {
    const { contextDetails, errorName, url } = data;

    const context = this.#getContextFromContextDetails(contextDetails);
    const navigableId = lazy.TabManager.getIdForBrowsingContext(context);

    const navigation = this.#navigations.get(navigableId);

    if (!navigation) {
      lazy.logger.trace(
        lazy.truncate`[${navigableId}] No navigation found to fail for url: ${url}`
      );
      return null;
    }

    if (navigation.state === "finished") {
      lazy.logger.trace(
        `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}`
      );
      return navigation;
    }

    lazy.logger.trace(
      lazy.truncate`[${navigableId}] Navigation failed for url: ${url} (${navigation.navigationId})`
    );

    navigation.state = "finished";

    this.emit("navigation-failed", {
      contextId: context.id,
      errorName,
      navigationId: navigation.navigationId,
      navigableId,
      url,
    });

    return navigation;
  }

  /**
   * Called when a navigation-started event is recorded from the
   * NavigationListener actors.
   *
   * This entry point is only intended to be called from
   * NavigationListenerParent, to avoid setting up observers or listeners,
   * which are unnecessary since NavigationManager has to be a singleton.
   *
   * @param {object} data
   * @param {BrowsingContextDetails} data.contextDetails
   *     The details about the browsing context for this navigation.
   * @param {string} data.url
   *     The URL as string for the navigation.
   * @returns {NavigationInfo}
   *     The created navigation or the ongoing navigation, if applicable.
   */
  notifyNavigationStarted(data) {
    const { contextDetails, url } = data;

    const context = this.#getContextFromContextDetails(contextDetails);
    const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
    let navigation = this.#navigations.get(navigableId);
    if (navigation) {
      if (navigation.state === "started") {
        // Bug 1908952. As soon as we have support for the "url" field in case of beforeunload
        // prompt being open, we can remove "!navigation.url" check.
        if (!navigation.url || navigation.url === url) {
          // If we are already monitoring a navigation for this navigable and the same url,
          // for which we did not receive a navigation-stopped event, this navigation
          // is already tracked and we don't want to create another id & event.
          lazy.logger.trace(
            `[${navigableId}] Skipping already tracked navigation, navigationId: ${navigation.navigationId}`
          );
          return navigation;
        }

        lazy.logger.trace(
          `[${navigableId}] We're going to fail the navigation for url: ${navigation.url} (${navigation.navigationId}), ` +
            "since it was interrupted by a new navigation."
        );

        // If there is already a navigation in progress but with a different url,
        // it means that this navigation was interrupted by a new navigation.
        // Note: ideally we should monitor this using NS_BINDING_ABORTED,
        // but due to intermittent issues, when monitoring this in content processes,
        // we can't reliable use it.
        notifyNavigationFailed({
          contextDetails,
          errorName: "A new navigation interrupted an unfinished navigation",
          url: navigation.url,
        });
      }

      // We don't want to notify that navigation for "about:blank" (or "about:blank" with parameter)
      // has started if it happens when the top-level browsing context is created.
      if (
        navigation.state === "initial-about-blank" &&
        new URL(url).pathname == "blank"
      ) {
        lazy.logger.trace(
          `[${navigableId}] Skipping this navigation for url: ${navigation.url}, since it's an initial navigation.`
        );
        return navigation;
      }
    }

    const navigationId = this.#getOrCreateNavigationId(navigableId);
    navigation = { state: "started", navigationId, url };
    this.#navigations.set(navigableId, navigation);

    lazy.logger.trace(
      lazy.truncate`[${navigableId}] Navigation started for url: ${url} (${navigationId})`
    );

    this.emit("navigation-started", { navigationId, navigableId, url });

    return navigation;
  }

  /**
   * Called when a navigation-stopped event is recorded from the
   * NavigationListener actors.
   *
   * @param {object} data
   * @param {BrowsingContextDetails} data.contextDetails
   *     The details about the browsing context for this navigation.
   * @param {string} data.url
   *     The URL as string for the navigation.
   * @returns {NavigationInfo}
   *     The stopped navigation if any, or null.
   */
  notifyNavigationStopped(data) {
    const { contextDetails, url } = data;

    const context = this.#getContextFromContextDetails(contextDetails);
    const navigableId = lazy.TabManager.getIdForBrowsingContext(context);

    const navigation = this.#navigations.get(navigableId);
    if (!navigation) {
      lazy.logger.trace(
        lazy.truncate`[${navigableId}] No navigation found to stop for url: ${url}`
      );
      return null;
    }

    if (navigation.state === "finished") {
      lazy.logger.trace(
        `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}`
      );
      return navigation;
    }

    lazy.logger.trace(
      lazy.truncate`[${navigableId}] Navigation finished for url: ${url} (${navigation.navigationId})`
    );

    navigation.state = "finished";

    this.emit("navigation-stopped", {
      navigationId: navigation.navigationId,
      navigableId,
      url,
    });

    return navigation;
  }

  /**
   * Register a navigation id to be used for the next navigation for the
   * provided browsing context details.
   *
   * @param {object} data
   * @param {BrowsingContextDetails} data.contextDetails
   *     The details about the browsing context for this navigation.
   * @returns {string}
   *     The UUID created the upcoming navigation.
   */
  registerNavigationId(data) {
    const { contextDetails } = data;
    const context = this.#getContextFromContextDetails(contextDetails);
    const navigableId = lazy.TabManager.getIdForBrowsingContext(context);

    let navigation = this.#navigations.get(navigableId);
    if (navigation && navigation.state === "started") {
      lazy.logger.trace(
        `[${navigableId}] We're going to fail the navigation for url: ${navigation.url} (${navigation.navigationId}), ` +
          "since it was interrupted by a new navigation."
      );

      // If there is already a navigation in progress but with a different url,
      // it means that this navigation was interrupted by a new navigation.
      // Note: ideally we should monitor this using NS_BINDING_ABORTED,
      // but due to intermittent issues, when monitoring this in content processes,
      // we can't reliable use it.
      notifyNavigationFailed({
        contextDetails,
        errorName: "A new navigation interrupted an unfinished navigation",
        url: navigation.url,
      });
    }

    const navigationId = lazy.generateUUID();
    this.#navigations.set(navigableId, { state: "registered", navigationId });

    return navigationId;
  }

  #getContextFromContextDetails(contextDetails) {
    if (contextDetails.context) {
      return contextDetails.context;
    }

    return contextDetails.isTopBrowsingContext
      ? BrowsingContext.getCurrentTopByBrowserId(contextDetails.browserId)
      : BrowsingContext.get(contextDetails.browsingContextId);
  }

  #getOrCreateNavigationId(navigableId) {
    const navigation = this.#navigations.get(navigableId);
    if (navigation !== undefined && navigation.state === "registered") {
      return navigation.navigationId;
    }
    return lazy.generateUUID();
  }

  #onContextAttached = async (eventName, data) => {
    const { browsingContext, why } = data;

    // We only care about top-level browsing contexts.
    if (browsingContext.parent !== null) {
      return;
    }
    // Filter out top-level browsing contexts that are created because of a
    // cross-group navigation.
    if (why === "replace") {
      return;
    }

    const navigableId =
      lazy.TabManager.getIdForBrowsingContext(browsingContext);
    let navigation = this.#navigations.get(navigableId);

    if (navigation) {
      return;
    }

    const navigationId = this.#getOrCreateNavigationId(navigableId);
    navigation = {
      state: "initial-about-blank",
      navigationId,
      url: browsingContext.currentURI.displaySpec,
    };
    this.#navigations.set(navigableId, navigation);
  };

  #onContextDiscarded = async (eventName, data = {}) => {
    const { browsingContext, why } = data;

    // Filter out top-level browsing contexts that are destroyed because of a
    // cross-group navigation.
    if (why === "replace") {
      return;
    }

    // TODO: Bug 1852941. We should also filter out events which are emitted
    // for DevTools frames.

    // Filter out notifications for chrome context until support gets
    // added (bug 1722679).
    if (!browsingContext.webProgress) {
      return;
    }

    const navigableId =
      lazy.TabManager.getIdForBrowsingContext(browsingContext);
    const navigation = this.#navigations.get(navigableId);

    // No need to fail navigation, if there is no navigation in progress.
    if (!navigation) {
      return;
    }

    notifyNavigationFailed({
      contextDetails: {
        context: browsingContext,
      },
      errorName: "Browsing context got discarded",
      url: navigation.url,
    });

    // If the navigable is discarded, we can safely clean up the navigation info.
    this.#navigations.delete(navigableId);
  };

  #onPromptClosed = (eventName, data) => {
    const { contentBrowser, detail } = data;
    const { accepted, promptType } = detail;

    // Send navigation failed event if beforeunload prompt was rejected.
    if (promptType === "beforeunload" && accepted === false) {
      const browsingContext = contentBrowser.browsingContext;

      notifyNavigationFailed({
        contextDetails: {
          context: browsingContext,
        },
        errorName: "Beforeunload prompt was rejected",
        // Bug 1908952. Add support for the "url" field.
      });
    }
  };

  #onPromptOpened = (eventName, data) => {
    const { contentBrowser, prompt } = data;
    const { promptType } = prompt;

    // We should start the navigation when beforeunload prompt is open.
    if (promptType === "beforeunload") {
      const browsingContext = contentBrowser.browsingContext;

      notifyNavigationStarted({
        contextDetails: {
          context: browsingContext,
        },
        // Bug 1908952. Add support for the "url" field.
      });
    }
  };
}

// Create a private NavigationRegistry singleton.
const navigationRegistry = new NavigationRegistry();

/**
 * See NavigationRegistry.notifyHashChanged.
 *
 * This entry point is only intended to be called from NavigationListenerParent,
 * to avoid setting up observers or listeners, which are unnecessary since
 * NavigationRegistry has to be a singleton.
 */
export function notifyFragmentNavigated(data) {
  return navigationRegistry.notifyFragmentNavigated(data);
}

/**
 * See NavigationRegistry.notifySameDocumentChanged.
 *
 * This entry point is only intended to be called from NavigationListenerParent,
 * to avoid setting up observers or listeners, which are unnecessary since
 * NavigationRegistry has to be a singleton.
 */
export function notifySameDocumentChanged(data) {
  return navigationRegistry.notifySameDocumentChanged(data);
}

/**
 * See NavigationRegistry.notifyNavigationFailed.
 *
 * This entry point is only intended to be called from NavigationListenerParent,
 * to avoid setting up observers or listeners, which are unnecessary since
 * NavigationRegistry has to be a singleton.
 */
export function notifyNavigationFailed(data) {
  return navigationRegistry.notifyNavigationFailed(data);
}

/**
 * See NavigationRegistry.notifyNavigationStarted.
 *
 * This entry point is only intended to be called from NavigationListenerParent,
 * to avoid setting up observers or listeners, which are unnecessary since
 * NavigationRegistry has to be a singleton.
 */
export function notifyNavigationStarted(data) {
  return navigationRegistry.notifyNavigationStarted(data);
}

/**
 * See NavigationRegistry.notifyNavigationStopped.
 *
 * This entry point is only intended to be called from NavigationListenerParent,
 * to avoid setting up observers or listeners, which are unnecessary since
 * NavigationRegistry has to be a singleton.
 */
export function notifyNavigationStopped(data) {
  return navigationRegistry.notifyNavigationStopped(data);
}

export function registerNavigationId(data) {
  return navigationRegistry.registerNavigationId(data);
}

/**
 * The NavigationManager exposes the NavigationRegistry data via a class which
 * needs to be individually instantiated by each consumer. This allow to track
 * how many consumers need navigation data at any point so that the
 * NavigationRegistry can register or unregister the underlying JSWindowActors
 * correctly.
 *
 * @fires navigation-started
 *    The NavigationManager emits "navigation-started" when a new navigation is
 *    detected, with the following object as payload:
 *      - {string} navigationId - The UUID for the navigation.
 *      - {string} navigableId - The UUID for the navigable.
 *      - {string} url - The target url for the navigation.
 * @fires navigation-stopped
 *    The NavigationManager emits "navigation-stopped" when a known navigation
 *    is stopped, with the following object as payload:
 *      - {string} navigationId - The UUID for the navigation.
 *      - {string} navigableId - The UUID for the navigable.
 *      - {string} url - The target url for the navigation.
 */
export class NavigationManager extends EventEmitter {
  #monitoring;

  constructor() {
    super();

    this.#monitoring = false;
  }

  destroy() {
    this.stopMonitoring();
  }

  getNavigationForBrowsingContext(context) {
    return navigationRegistry.getNavigationForBrowsingContext(context);
  }

  startMonitoring() {
    if (this.#monitoring) {
      return;
    }

    this.#monitoring = true;
    navigationRegistry.startMonitoring(this);
    navigationRegistry.on("fragment-navigated", this.#onNavigationEvent);
    navigationRegistry.on("navigation-failed", this.#onNavigationEvent);
    navigationRegistry.on("navigation-started", this.#onNavigationEvent);
    navigationRegistry.on("navigation-stopped", this.#onNavigationEvent);
    navigationRegistry.on("same-document-changed", this.#onNavigationEvent);
  }

  stopMonitoring() {
    if (!this.#monitoring) {
      return;
    }

    this.#monitoring = false;
    navigationRegistry.stopMonitoring(this);
    navigationRegistry.off("fragment-navigated", this.#onNavigationEvent);
    navigationRegistry.off("navigation-failed", this.#onNavigationEvent);
    navigationRegistry.off("navigation-started", this.#onNavigationEvent);
    navigationRegistry.off("navigation-stopped", this.#onNavigationEvent);
    navigationRegistry.off("same-document-changed", this.#onNavigationEvent);
  }

  #onNavigationEvent = (eventName, data) => {
    this.emit(eventName, data);
  };
}

[ Dauer der Verarbeitung: 0.7 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge