860 lines
29 KiB
JavaScript
860 lines
29 KiB
JavaScript
/* 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";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.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",
|
|
ParentWebProgressListener:
|
|
"chrome://remote/content/shared/listeners/ParentWebProgressListener.sys.mjs",
|
|
PromptListener:
|
|
"chrome://remote/content/shared/listeners/PromptListener.sys.mjs",
|
|
registerWebDriverDocumentInsertedActor:
|
|
"chrome://remote/content/shared/js-process-actors/WebDriverDocumentInsertedActor.sys.mjs",
|
|
registerWebProgressListenerActor:
|
|
"chrome://remote/content/shared/js-window-actors/WebProgressListenerActor.sys.mjs",
|
|
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
|
|
truncate: "chrome://remote/content/shared/Format.sys.mjs",
|
|
unregisterWebProgressListenerActor:
|
|
"chrome://remote/content/shared/js-window-actors/WebProgressListenerActor.sys.mjs",
|
|
unregisterWebDriverDocumentInsertedActor:
|
|
"chrome://remote/content/shared/js-process-actors/WebDriverDocumentInsertedActor.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"useParentWebProgressListener",
|
|
"remote.experimental-parent-navigation.enabled",
|
|
false
|
|
);
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
|
|
/**
|
|
* Enum of navigation states.
|
|
*
|
|
* @enum {string}
|
|
*/
|
|
export const NavigationState = {
|
|
Registered: "registered",
|
|
InitialAboutBlank: "initial-about-blank",
|
|
Started: "started",
|
|
Finished: "finished",
|
|
};
|
|
|
|
/**
|
|
* @typedef {object} NavigationInfo
|
|
* @property {NavigationState} 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 WebProgressListener{Parent|Child},
|
|
* found under remote/shared/js-window-actors. As a simple overview, the
|
|
* WebProgressListenerChild will monitor navigations in all window globals using
|
|
* content process WebProgressListener, and will forward each relevant update to
|
|
* the WebProgressListenerParent
|
|
*
|
|
* The NavigationRegistry singleton holds the map of navigations, from navigable
|
|
* to NavigationInfo. It will also be called by WebProgressListenerParent
|
|
* 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 WebProgressListenerActor 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;
|
|
#parentWebProgressListener;
|
|
#promptListener;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
// Set of NavigationManager instances currently used.
|
|
this.#managers = new Set();
|
|
|
|
// Maps navigable id to NavigationInfo.
|
|
this.#navigations = new Map();
|
|
|
|
if (lazy.useParentWebProgressListener) {
|
|
this.#parentWebProgressListener = new lazy.ParentWebProgressListener();
|
|
}
|
|
|
|
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 WebProgressListener JSWindowActor and will initialize them in all
|
|
* existing browsing contexts.
|
|
*/
|
|
startMonitoring(listener) {
|
|
if (this.#managers.size == 0) {
|
|
if (lazy.useParentWebProgressListener) {
|
|
this.#parentWebProgressListener.startListening();
|
|
} else {
|
|
lazy.registerWebProgressListenerActor();
|
|
}
|
|
lazy.registerWebDriverDocumentInsertedActor();
|
|
this.#contextListener.startListening();
|
|
this.#promptListener.startListening();
|
|
}
|
|
|
|
this.#managers.add(listener);
|
|
}
|
|
|
|
/**
|
|
* Stop monitoring navigations. This will unregister the WebProgressListener
|
|
* 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();
|
|
if (lazy.useParentWebProgressListener) {
|
|
this.#parentWebProgressListener.stopListening();
|
|
} else {
|
|
lazy.unregisterWebProgressListenerActor();
|
|
}
|
|
lazy.unregisterWebDriverDocumentInsertedActor();
|
|
// Clear the map.
|
|
this.#navigations = new Map();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when a fragment navigation is recorded from the
|
|
* WebProgressListener actors.
|
|
*
|
|
* This entry point is only intended to be called from
|
|
* WebProgressListenerParent, 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: NavigationState.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 == NavigationState.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
|
|
* WebProgressListener actors.
|
|
*
|
|
* This entry point is only intended to be called from
|
|
* WebProgressListenerParent, 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: NavigationState.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 == NavigationState.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-committed event is recorded from the
|
|
* WebProgressListener actors.
|
|
*
|
|
* This entry point is only intended to be called from
|
|
* WebProgressListenerParent, 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.
|
|
*/
|
|
notifyNavigationCommitted(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 commit for url: ${url}`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// We don't want to notify that navigation for "about:blank" (or "about:blank" with parameter)
|
|
// is committed if it happens when the top-level browsing context is created.
|
|
if (
|
|
navigation.state === NavigationState.InitialAboutBlank &&
|
|
new URL(url).pathname == "blank"
|
|
) {
|
|
lazy.logger.trace(
|
|
`[${navigableId}] Skipping this navigation for url: ${navigation.url}, since it's an initial navigation.`
|
|
);
|
|
return navigation;
|
|
}
|
|
|
|
lazy.logger.trace(
|
|
lazy.truncate`[${navigableId}] Navigation committed for url: ${url} (${navigation.navigationId})`
|
|
);
|
|
|
|
this.emit("navigation-committed", {
|
|
contextId: context.id,
|
|
errorName,
|
|
navigationId: navigation.navigationId,
|
|
navigableId,
|
|
url,
|
|
});
|
|
|
|
return navigation;
|
|
}
|
|
|
|
/**
|
|
* Called when a navigation-failed event is recorded from the
|
|
* WebProgressListener actors.
|
|
*
|
|
* This entry point is only intended to be called from
|
|
* WebProgressListenerParent, 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 === NavigationState.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 = NavigationState.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
|
|
* WebProgressListener actors.
|
|
*
|
|
* This entry point is only intended to be called from
|
|
* WebProgressListenerParent, 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);
|
|
|
|
// For top-level navigations, `context` is the current browsing context for
|
|
// the browser with id = `contextDetails.browserId`.
|
|
// If the navigation replaced the browsing contexts, retrieve the original
|
|
// browsing context to check if the event is relevant.
|
|
const originalContext = BrowsingContext.get(
|
|
contextDetails.browsingContextId
|
|
);
|
|
|
|
// If we have a previousNavigation for the same URL, and the browsing
|
|
// context for this event (originalContext) is outdated, skip the event.
|
|
// Any further event from this browsing context will come with the aborted
|
|
// flag set and will also be ignored.
|
|
// Bug 1930616: Moving the NavigationManager to the parent process should
|
|
// hopefully make this irrelevant.
|
|
if (
|
|
url == navigation?.url &&
|
|
context != originalContext &&
|
|
!context.isReplaced &&
|
|
originalContext?.isReplaced
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (navigation) {
|
|
if (navigation.state === NavigationState.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 === NavigationState.InitialAboutBlank &&
|
|
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: NavigationState.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
|
|
* WebProgressListener 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 === NavigationState.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 = NavigationState.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 === NavigationState.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: NavigationState.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 === NavigationState.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: NavigationState.InitialAboutBlank,
|
|
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;
|
|
}
|
|
|
|
// Filter out notifications for webextension contexts until support gets
|
|
// added (bug 1755014).
|
|
if (browsingContext.currentRemoteType === "extension") {
|
|
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 WebProgressListenerParent,
|
|
* 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 WebProgressListenerParent,
|
|
* 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.notifyNavigationCommitted.
|
|
*
|
|
* This entry point is only intended to be called from WebProgressListenerParent,
|
|
* to avoid setting up observers or listeners, which are unnecessary since
|
|
* NavigationRegistry has to be a singleton.
|
|
*/
|
|
export function notifyNavigationCommitted(data) {
|
|
return navigationRegistry.notifyNavigationCommitted(data);
|
|
}
|
|
|
|
/**
|
|
* See NavigationRegistry.notifyNavigationFailed.
|
|
*
|
|
* This entry point is only intended to be called from WebProgressListenerParent,
|
|
* 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 WebProgressListenerParent,
|
|
* 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 WebProgressListenerParent,
|
|
* 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-committed", 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-committed", 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);
|
|
};
|
|
}
|