From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- .../modules/root/browsingContext.sys.mjs | 663 +++++++++++++++++++++ remote/webdriver-bidi/modules/root/log.sys.mjs | 15 + remote/webdriver-bidi/modules/root/network.sys.mjs | 342 +++++++++++ remote/webdriver-bidi/modules/root/script.sys.mjs | 532 +++++++++++++++++ remote/webdriver-bidi/modules/root/session.sys.mjs | 406 +++++++++++++ 5 files changed, 1958 insertions(+) create mode 100644 remote/webdriver-bidi/modules/root/browsingContext.sys.mjs create mode 100644 remote/webdriver-bidi/modules/root/log.sys.mjs create mode 100644 remote/webdriver-bidi/modules/root/network.sys.mjs create mode 100644 remote/webdriver-bidi/modules/root/script.sys.mjs create mode 100644 remote/webdriver-bidi/modules/root/session.sys.mjs (limited to 'remote/webdriver-bidi/modules/root') diff --git a/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs new file mode 100644 index 0000000000..cd8dff6ca0 --- /dev/null +++ b/remote/webdriver-bidi/modules/root/browsingContext.sys.mjs @@ -0,0 +1,663 @@ +/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs", + assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", + BrowsingContextListener: + "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs", + capture: "chrome://remote/content/shared/Capture.sys.mjs", + ContextDescriptorType: + "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + Log: "chrome://remote/content/shared/Log.sys.mjs", + pprint: "chrome://remote/content/shared/Format.sys.mjs", + ProgressListener: "chrome://remote/content/shared/Navigate.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + waitForInitialNavigationCompleted: + "chrome://remote/content/shared/Navigate.sys.mjs", + WindowGlobalMessageHandler: + "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs", + windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI) +); + +/** + * @typedef {Object} CreateType + **/ + +/** + * Enum of types supported by the browsingContext.create command. + * + * @readonly + * @enum {CreateType} + **/ +const CreateType = { + tab: "tab", + window: "window", +}; + +/** + * @typedef {string} WaitCondition + **/ + +/** + * Wait conditions supported by WebDriver BiDi for navigation. + * + * @enum {WaitCondition} + */ +const WaitCondition = { + None: "none", + Interactive: "interactive", + Complete: "complete", +}; + +class BrowsingContextModule extends Module { + #contextListener; + #subscribedEvents; + + /** + * Create a new module instance. + * + * @param {MessageHandler} messageHandler + * The MessageHandler instance which owns this Module instance. + */ + constructor(messageHandler) { + super(messageHandler); + + // Create the console-api listener and listen on "message" events. + this.#contextListener = new lazy.BrowsingContextListener(); + this.#contextListener.on("attached", this.#onContextAttached); + + // Set of event names which have active subscriptions. + this.#subscribedEvents = new Set(); + } + + destroy() { + this.#contextListener.off("attached", this.#onContextAttached); + this.#contextListener.destroy(); + + this.#subscribedEvents = null; + } + + /** + * Capture a base64-encoded screenshot of the provided browsing context. + * + * @param {Object=} options + * @param {string} context + * Id of the browsing context to screenshot. + * + * @throws {NoSuchFrameError} + * If the browsing context cannot be found. + */ + async captureScreenshot(options = {}) { + const { context: contextId } = options; + + lazy.assert.string( + contextId, + `Expected "context" to be a string, got ${contextId}` + ); + const context = this.#getBrowsingContext(contextId); + + const rect = await this.messageHandler.handleCommand({ + moduleName: "browsingContext", + commandName: "_getScreenshotRect", + destination: { + type: lazy.WindowGlobalMessageHandler.type, + id: context.id, + }, + retryOnAbort: true, + }); + + const canvas = await lazy.capture.canvas( + context.topChromeWindow, + context, + rect.x, + rect.y, + rect.width, + rect.height + ); + + return { + data: lazy.capture.toBase64(canvas), + }; + } + + /** + * Close the provided browsing context. + * + * @param {Object=} options + * @param {string} context + * Id of the browsing context to close. + * + * @throws {NoSuchFrameError} + * If the browsing context cannot be found. + * @throws {InvalidArgumentError} + * If the browsing context is not a top-level one. + */ + async close(options = {}) { + const { context: contextId } = options; + + lazy.assert.string( + contextId, + `Expected "context" to be a string, got ${contextId}` + ); + + const context = lazy.TabManager.getBrowsingContextById(contextId); + if (!context) { + throw new lazy.error.NoSuchFrameError( + `Browsing Context with id ${contextId} not found` + ); + } + + if (context.parent) { + throw new lazy.error.InvalidArgumentError( + `Browsing Context with id ${contextId} is not top-level` + ); + } + + if (lazy.TabManager.getTabCount() === 1) { + // The behavior when closing the last tab is currently unspecified. + // Warn the consumer about potential issues + lazy.logger.warn( + `Closing the last open tab (Browsing Context id ${contextId}), expect inconsistent behavior across platforms` + ); + } + + const tab = lazy.TabManager.getTabForBrowsingContext(context); + await lazy.TabManager.removeTab(tab); + } + + /** + * Create a new browsing context using the provided type "tab" or "window". + * + * @param {Object=} options + * @param {string=} options.referenceContext + * Id of the top-level browsing context to use as reference. + * If options.type is "tab", the new tab will open in the same window as + * the reference context, and will be added next to the reference context. + * If options.type is "window", the reference context is ignored. + * @param {CreateType} options.type + * Type of browsing context to create. + * + * @throws {InvalidArgumentError} + * If the browsing context is not a top-level one. + * @throws {NoSuchFrameError} + * If the browsing context cannot be found. + */ + async create(options = {}) { + const { referenceContext: referenceContextId = null, type } = options; + if (type !== CreateType.tab && type !== CreateType.window) { + throw new lazy.error.InvalidArgumentError( + `Expected "type" to be one of ${Object.values(CreateType)}, got ${type}` + ); + } + + let browser; + switch (type) { + case "window": + let newWindow = await lazy.windowManager.openBrowserWindow(); + browser = lazy.TabManager.getTabBrowser(newWindow).selectedBrowser; + break; + + case "tab": + if (!lazy.TabManager.supportsTabs()) { + throw new lazy.error.UnsupportedOperationError( + `browsingContext.create with type "tab" not supported in ${lazy.AppInfo.name}` + ); + } + + let referenceTab; + if (referenceContextId !== null) { + lazy.assert.string( + referenceContextId, + lazy.pprint`Expected "referenceContext" to be a string, got ${referenceContextId}` + ); + + const referenceBrowsingContext = lazy.TabManager.getBrowsingContextById( + referenceContextId + ); + if (!referenceBrowsingContext) { + throw new lazy.error.NoSuchFrameError( + `Browsing Context with id ${referenceContextId} not found` + ); + } + + if (referenceBrowsingContext.parent) { + throw new lazy.error.InvalidArgumentError( + `referenceContext with id ${referenceContextId} is not a top-level browsing context` + ); + } + + referenceTab = lazy.TabManager.getTabForBrowsingContext( + referenceBrowsingContext + ); + } + + const tab = await lazy.TabManager.addTab({ + focus: false, + referenceTab, + }); + browser = lazy.TabManager.getBrowserForTab(tab); + } + + await lazy.waitForInitialNavigationCompleted( + browser.browsingContext.webProgress + ); + + return { + context: lazy.TabManager.getIdForBrowser(browser), + }; + } + + /** + * An object that holds the WebDriver Bidi browsing context information. + * + * @typedef BrowsingContextInfo + * + * @property {string} context + * The id of the browsing context. + * @property {string=} parent + * The parent of the browsing context if it's the root browsing context + * of the to be processed browsing context tree. + * @property {string} url + * The current documents location. + * @property {Array=} children + * List of child browsing contexts. Only set if maxDepth hasn't been + * reached yet. + */ + + /** + * An object that holds the WebDriver Bidi browsing context tree information. + * + * @typedef BrowsingContextGetTreeResult + * + * @property {Array} contexts + * List of child browsing contexts. + */ + + /** + * Returns a tree of all browsing contexts that are descendents of the + * given context, or all top-level contexts when no root is provided. + * + * @param {Object=} options + * @param {number=} maxDepth + * Depth of the browsing context tree to traverse. If not specified + * the whole tree is returned. + * @param {string=} root + * Id of the root browsing context. + * + * @returns {BrowsingContextGetTreeResult} + * Tree of browsing context information. + * @throws {NoSuchFrameError} + * If the browsing context cannot be found. + */ + getTree(options = {}) { + const { maxDepth = null, root: rootId = null } = options; + + if (maxDepth !== null) { + lazy.assert.positiveInteger( + maxDepth, + `Expected "maxDepth" to be a positive integer, got ${maxDepth}` + ); + } + + let contexts; + if (rootId !== null) { + // With a root id specified return the context info for itself + // and the full tree. + lazy.assert.string( + rootId, + `Expected "root" to be a string, got ${rootId}` + ); + contexts = [this.#getBrowsingContext(rootId)]; + } else { + // Return all top-level browsing contexts. + contexts = lazy.TabManager.browsers.map( + browser => browser.browsingContext + ); + } + + const contextsInfo = contexts.map(context => { + return this.#getBrowsingContextInfo(context, { maxDepth }); + }); + + return { contexts: contextsInfo }; + } + + /** + * An object that holds the WebDriver Bidi navigation information. + * + * @typedef BrowsingContextNavigateResult + * + * @property {String} navigation + * Unique id for this navigation. + * @property {String} url + * The requested or reached URL. + */ + + /** + * Navigate the given context to the provided url, with the provided wait condition. + * + * @param {Object=} options + * @param {string} context + * Id of the browsing context to navigate. + * @param {string} url + * Url for the navigation. + * @param {WaitCondition=} wait + * Wait condition for the navigation, one of "none", "interactive", "complete". + * + * @returns {BrowsingContextNavigateResult} + * Navigation result. + * @throws {InvalidArgumentError} + * Raised if an argument is of an invalid type or value. + * @throws {NoSuchFrameError} + * If the browsing context for contextId cannot be found. + */ + async navigate(options = {}) { + const { context: contextId, url, wait = WaitCondition.None } = options; + + lazy.assert.string( + contextId, + `Expected "context" to be a string, got ${contextId}` + ); + + lazy.assert.string(url, `Expected "url" to be string, got ${url}`); + + const waitConditions = Object.values(WaitCondition); + if (!waitConditions.includes(wait)) { + throw new lazy.error.InvalidArgumentError( + `Expected "wait" to be one of ${waitConditions}, got ${wait}` + ); + } + + const context = this.#getBrowsingContext(contextId); + + // webProgress will be stable even if the context navigates, retrieve it + // immediately before doing any asynchronous call. + const webProgress = context.webProgress; + + const base = await this.messageHandler.handleCommand({ + moduleName: "browsingContext", + commandName: "_getBaseURL", + destination: { + type: lazy.WindowGlobalMessageHandler.type, + id: context.id, + }, + retryOnAbort: true, + }); + + let targetURI; + try { + const baseURI = Services.io.newURI(base); + targetURI = Services.io.newURI(url, null, baseURI); + } catch (e) { + throw new lazy.error.InvalidArgumentError( + `Expected "url" to be a valid URL (${e.message})` + ); + } + + return this.#awaitNavigation(webProgress, targetURI, { + wait, + }); + } + + /** + * Start and await a navigation on the provided BrowsingContext. Returns a + * promise which resolves when the navigation is done according to the provided + * navigation strategy. + * + * @param {WebProgress} webProgress + * The WebProgress instance to observe for this navigation. + * @param {nsIURI} targetURI + * The URI to navigate to. + * @param {Object} options + * @param {WaitCondition} options.wait + * The WaitCondition to use to wait for the navigation. + */ + async #awaitNavigation(webProgress, targetURI, options) { + const { wait } = options; + + const context = webProgress.browsingContext; + const browserId = context.browserId; + + const resolveWhenStarted = wait === WaitCondition.None; + const listener = new lazy.ProgressListener(webProgress, { + expectNavigation: true, + resolveWhenStarted, + // In case the webprogress is already navigating, always wait for an + // explicit start flag. + waitForExplicitStart: true, + }); + + const onDocumentInteractive = (evtName, wrappedEvt) => { + if (webProgress.browsingContext.id !== wrappedEvt.contextId) { + // Ignore load events for unrelated browsing contexts. + return; + } + + if (wrappedEvt.readyState === "interactive") { + listener.stopIfStarted(); + } + }; + + const contextDescriptor = { + type: lazy.ContextDescriptorType.TopBrowsingContext, + id: browserId, + }; + + // For the Interactive wait condition, resolve as soon as + // the document becomes interactive. + if (wait === WaitCondition.Interactive) { + await this.messageHandler.eventsDispatcher.on( + "browsingContext._documentInteractive", + contextDescriptor, + onDocumentInteractive + ); + } + + const navigated = listener.start(); + navigated.finally(async () => { + if (listener.isStarted) { + listener.stop(); + } + + if (wait === WaitCondition.Interactive) { + await this.messageHandler.eventsDispatcher.off( + "browsingContext._documentInteractive", + contextDescriptor, + onDocumentInteractive + ); + } + }); + + context.loadURI(targetURI.spec, { + loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + hasValidUserGestureActivation: true, + }); + await navigated; + + let url; + if (wait === WaitCondition.None) { + // If wait condition is None, the navigation resolved before the current + // context has navigated. + url = listener.targetURI.spec; + } else { + url = listener.currentURI.spec; + } + + return { + // TODO: The navigation id should be a real id mapped to the navigation. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1763122 + navigation: null, + url, + }; + } + + /** + * Retrieves a browsing context based on its id. + * + * @param {Number} contextId + * Id of the browsing context. + * @returns {BrowsingContext=} + * The browsing context or null if contextId is null. + * @throws {NoSuchFrameError} + * If the browsing context cannot be found. + */ + #getBrowsingContext(contextId) { + // The WebDriver BiDi specification expects null to be + // returned if no browsing context id has been specified. + if (contextId === null) { + return null; + } + + const context = lazy.TabManager.getBrowsingContextById(contextId); + if (context === null) { + throw new lazy.error.NoSuchFrameError( + `Browsing Context with id ${contextId} not found` + ); + } + + return context; + } + + /** + * Get the WebDriver BiDi browsing context information. + * + * @param {BrowsingContext} context + * The browsing context to get the information from. + * @param {Object=} options + * @param {boolean=} isRoot + * Flag that indicates if this browsing context is the root of all the + * browsing contexts to be returned. Defaults to true. + * @param {number=} maxDepth + * Depth of the browsing context tree to traverse. If not specified + * the whole tree is returned. + * @returns {BrowsingContextInfo} + * The information about the browsing context. + */ + #getBrowsingContextInfo(context, options = {}) { + const { isRoot = true, maxDepth = null } = options; + + let children = null; + if (maxDepth === null || maxDepth > 0) { + children = context.children.map(context => + this.#getBrowsingContextInfo(context, { + maxDepth: maxDepth === null ? maxDepth : maxDepth - 1, + isRoot: false, + }) + ); + } + + const contextInfo = { + context: lazy.TabManager.getIdForBrowsingContext(context), + url: context.currentURI.spec, + children, + }; + + if (isRoot) { + // Only emit the parent id for the top-most browsing context. + const parentId = lazy.TabManager.getIdForBrowsingContext(context.parent); + contextInfo.parent = parentId; + } + + return contextInfo; + } + + #onContextAttached = async (eventName, data = {}) => { + const { browsingContext, why } = data; + + // Filter out top-level browsing contexts that are created because of a + // cross-group navigation. + if (why === "replace") { + return; + } + + // Filter out notifications for chrome context until support gets + // added (bug 1722679). + if (!browsingContext.webProgress) { + return; + } + + const browsingContextInfo = this.#getBrowsingContextInfo(browsingContext, { + maxDepth: 0, + }); + + // This event is emitted from the parent process but for a given browsing + // context. Set the event's contextInfo to the message handler corresponding + // to this browsing context. + const contextInfo = { + contextId: browsingContext.id, + type: lazy.WindowGlobalMessageHandler.type, + }; + this.emitEvent( + "browsingContext.contextCreated", + browsingContextInfo, + contextInfo + ); + }; + + #subscribeEvent(event) { + if (event === "browsingContext.contextCreated") { + this.#contextListener.startListening(); + this.#subscribedEvents.add(event); + } + } + + #unsubscribeEvent(event) { + if (event === "browsingContext.contextCreated") { + this.#contextListener.stopListening(); + this.#subscribedEvents.delete(event); + } + } + + /** + * Internal commands + */ + + _applySessionData(params) { + // TODO: Bug 1775231. Move this logic to a shared module or an abstract + // class. + const { category } = params; + if (category === "event") { + const filteredSessionData = params.sessionData.filter(item => + this.messageHandler.matchesContext(item.contextDescriptor) + ); + for (const event of this.#subscribedEvents.values()) { + const hasSessionItem = filteredSessionData.some( + item => item.value === event + ); + // If there are no session items for this context, we should unsubscribe from the event. + if (!hasSessionItem) { + this.#unsubscribeEvent(event); + } + } + + // Subscribe to all events, which have an item in SessionData. + for (const { value } of filteredSessionData) { + this.#subscribeEvent(value); + } + } + } + + static get supportedEvents() { + return [ + "browsingContext.contextCreated", + "browsingContext.domContentLoaded", + "browsingContext.load", + ]; + } +} + +export const browsingContext = BrowsingContextModule; diff --git a/remote/webdriver-bidi/modules/root/log.sys.mjs b/remote/webdriver-bidi/modules/root/log.sys.mjs new file mode 100644 index 0000000000..db2390d3ba --- /dev/null +++ b/remote/webdriver-bidi/modules/root/log.sys.mjs @@ -0,0 +1,15 @@ +/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs"; + +class LogModule extends Module { + destroy() {} + + static get supportedEvents() { + return ["log.entryAdded"]; + } +} + +export const log = LogModule; diff --git a/remote/webdriver-bidi/modules/root/network.sys.mjs b/remote/webdriver-bidi/modules/root/network.sys.mjs new file mode 100644 index 0000000000..e82bc3c072 --- /dev/null +++ b/remote/webdriver-bidi/modules/root/network.sys.mjs @@ -0,0 +1,342 @@ +/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + NetworkListener: + "chrome://remote/content/shared/listeners/NetworkListener.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + WindowGlobalMessageHandler: + "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs", +}); + +/** + * @typedef {Object} BaseParameters + * @property {string=} context + * @property {boolean} isRedirect + * @property {Navigation=} navigation + * @property {RequestData} request + * @property {number} timestamp + */ + +/** + * @typedef {Object} Cookie + * @property {Array=} binaryValue + * @property {string} domain + * @property {number=} expires + * @property {boolean} httpOnly + * @property {string} name + * @property {string} path + * @property {('lax' | 'none' | 'strict')} sameSite + * @property {boolean} secure + * @property {number} size + * @property {string=} value + */ + +/** + * @typedef {Object} FetchTimingInfo + * @property {number} originTime + * @property {number} requestTime + * @property {number} redirectStart + * @property {number} redirectEnd + * @property {number} fetchStart + * @property {number} dnsStart + * @property {number} dnsEnd + * @property {number} connectStart + * @property {number} connectEnd + * @property {number} tlsStart + * @property {number} requestStart + * @property {number} responseStart + * @property {number} responseEnd + */ + +/** + * @typedef {Object} Header + * @property {Array=} binaryValue + * @property {string} name + * @property {string=} value + */ + +/** + * @typedef {string} InitiatorType + **/ + +/** + * Enum of possible initiator types. + * + * @readonly + * @enum {InitiatorType} + **/ +const InitiatorType = { + Other: "other", + Parser: "parser", + Preflight: "preflight", + Script: "script", +}; +/** + * @typedef {Object} Initiator + * @property {InitiatorType} type + * @property {number=} columnNumber + * @property {number=} lineNumber + * @property {string=} request + * @property {StackTrace=} stackTrace + */ + +/** + * @typedef {Object} RequestData + * @property {number|null} bodySize + * Defaults to null. + * @property {Array} cookies + * @property {Array
} headers + * @property {number} headersSize + * @property {string} method + * @property {string} request + * @property {FetchTimingInfo} timings + * @property {string} url + */ + +/** + * @typedef {Object} BeforeRequestSentParametersProperties + * @property {Initiator} initiator + */ + +/** + * Parameters for the BeforeRequestSent event + * + * @typedef {BaseParameters & BeforeRequestSentParametersProperties} BeforeRequestSentParameters + */ + +/** + * @typedef {Object} ResponseContent + * @property {number|null} size + * Defaults to null. + */ + +/** + * @typedef {Object} ResponseData + * @property {string} url + * @property {string} protocol + * @property {number} status + * @property {string} statusText + * @property {boolean} fromCache + * @property {Array
} headers + * @property {string} mimeType + * @property {number} bytesReceived + * @property {number|null} headersSize + * Defaults to null. + * @property {number|null} bodySize + * Defaults to null. + * @property {ResponseContent} content + */ + +/** + * @typedef {Object} ResponseStartedParametersProperties + * @property {ResponseData} response + */ + +/** + * Parameters for the ResponseStarted event + * + * @typedef {BaseParameters & ResponseStartedParametersProperties} ResponseStartedParameters + */ + +/** + * @typedef {Object} ResponseCompletedParametersProperties + * @property {ResponseData} response + */ + +/** + * Parameters for the ResponseCompleted event + * + * @typedef {BaseParameters & ResponseCompletedParametersProperties} ResponseCompletedParameters + */ + +class NetworkModule extends Module { + #beforeRequestSentMap; + #networkListener; + #subscribedEvents; + + constructor(messageHandler) { + super(messageHandler); + + // Map of request ids to redirect counts. A WebDriver BiDi request id is + // identical for redirects of a given request, this map allows to know if we + // already emitted a beforeRequestSent event for a given request with a + // specific redirectCount. + this.#beforeRequestSentMap = new Map(); + + // Set of event names which have active subscriptions + this.#subscribedEvents = new Set(); + + this.#networkListener = new lazy.NetworkListener(); + this.#networkListener.on("before-request-sent", this.#onBeforeRequestSent); + this.#networkListener.on("response-completed", this.#onResponseEvent); + this.#networkListener.on("response-started", this.#onResponseEvent); + } + + destroy() { + this.#networkListener.off("before-request-sent", this.#onBeforeRequestSent); + this.#networkListener.off("response-completed", this.#onResponseEvent); + this.#networkListener.off("response-started", this.#onResponseEvent); + + this.#beforeRequestSentMap = null; + this.#subscribedEvents = null; + } + + #getContextInfo(browsingContext) { + return { + contextId: browsingContext.id, + type: lazy.WindowGlobalMessageHandler.type, + }; + } + + #onBeforeRequestSent = (name, data) => { + const { contextId, requestData, timestamp, redirectCount } = data; + + const isRedirect = redirectCount > 0; + this.#beforeRequestSentMap.set(requestData.requestId, redirectCount); + + // Bug 1805479: Handle the initiator, including stacktrace details. + const initiator = { + type: InitiatorType.Other, + }; + + const baseParameters = { + context: contextId, + isRedirect, + redirectCount, + // Bug 1805405: Handle the navigation id. + navigation: null, + request: requestData, + timestamp, + }; + + const beforeRequestSentEvent = { + ...baseParameters, + initiator, + }; + + const browsingContext = lazy.TabManager.getBrowsingContextById(contextId); + this.emitEvent( + "network.beforeRequestSent", + beforeRequestSentEvent, + this.#getContextInfo(browsingContext) + ); + }; + + #onResponseEvent = (name, data) => { + const { + contextId, + requestData, + responseData, + timestamp, + redirectCount, + } = data; + + const isRedirect = redirectCount > 0; + + const requestId = requestData.requestId; + if (this.#beforeRequestSentMap.get(requestId) != redirectCount) { + throw new lazy.error.UnknownError( + `Redirect count of the request ${requestId} does not match the before request sent map` + ); + } + + const baseParameters = { + context: contextId, + isRedirect, + redirectCount, + // Bug 1805405: Handle the navigation id. + navigation: null, + request: requestData, + timestamp, + }; + + const responseEvent = { + ...baseParameters, + response: responseData, + }; + + const protocolEventName = + name === "response-started" + ? "network.responseStarted" + : "network.responseCompleted"; + + const browsingContext = lazy.TabManager.getBrowsingContextById(contextId); + this.emitEvent( + protocolEventName, + responseEvent, + this.#getContextInfo(browsingContext) + ); + }; + + #startListening(event) { + if (this.#subscribedEvents.size == 0) { + this.#networkListener.startListening(); + } + this.#subscribedEvents.add(event); + } + + #stopListening(event) { + this.#subscribedEvents.delete(event); + if (this.#subscribedEvents.size == 0) { + this.#networkListener.stopListening(); + } + } + + #subscribeEvent(event) { + if (this.constructor.supportedEvents.includes(event)) { + this.#startListening(event); + } + } + + #unsubscribeEvent(event) { + if (this.constructor.supportedEvents.includes(event)) { + this.#stopListening(event); + } + } + + /** + * Internal commands + */ + + _applySessionData(params) { + // TODO: Bug 1775231. Move this logic to a shared module or an abstract + // class. + const { category } = params; + if (category === "event") { + const filteredSessionData = params.sessionData.filter(item => + this.messageHandler.matchesContext(item.contextDescriptor) + ); + for (const event of this.#subscribedEvents.values()) { + const hasSessionItem = filteredSessionData.some( + item => item.value === event + ); + // If there are no session items for this context, we should unsubscribe from the event. + if (!hasSessionItem) { + this.#unsubscribeEvent(event); + } + } + + // Subscribe to all events, which have an item in SessionData. + for (const { value } of filteredSessionData) { + this.#subscribeEvent(value); + } + } + } + + static get supportedEvents() { + return [ + "network.beforeRequestSent", + "network.responseCompleted", + "network.responseStarted", + ]; + } +} + +export const network = NetworkModule; diff --git a/remote/webdriver-bidi/modules/root/script.sys.mjs b/remote/webdriver-bidi/modules/root/script.sys.mjs new file mode 100644 index 0000000000..308033c816 --- /dev/null +++ b/remote/webdriver-bidi/modules/root/script.sys.mjs @@ -0,0 +1,532 @@ +/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", + ContextDescriptorType: + "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", + RealmType: "chrome://remote/content/webdriver-bidi/Realm.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + WindowGlobalMessageHandler: + "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs", +}); + +/** + * @typedef {string} ScriptEvaluateResultType + **/ + +/** + * Enum of possible evaluation result types. + * + * @readonly + * @enum {ScriptEvaluateResultType} + **/ +const ScriptEvaluateResultType = { + Exception: "exception", + Success: "success", +}; + +class ScriptModule extends Module { + destroy() {} + + /** + * Used to represent a frame of a JavaScript stack trace. + * + * @typedef StackFrame + * + * @property {number} columnNumber + * @property {string} functionName + * @property {number} lineNumber + * @property {string} url + */ + + /** + * Used to represent a JavaScript stack at a point in script execution. + * + * @typedef StackTrace + * + * @property {Array} callFrames + */ + + /** + * Used to represent a JavaScript exception. + * + * @typedef ExceptionDetails + * + * @property {number} columnNumber + * @property {RemoteValue} exception + * @property {number} lineNumber + * @property {StackTrace} stackTrace + * @property {string} text + */ + + /** + * Used as return value for script.evaluate, as one of the available variants + * {ScriptEvaluateResultException} or {ScriptEvaluateResultSuccess}. + * + * @typedef ScriptEvaluateResult + */ + + /** + * Used as return value for script.evaluate when the script completes with a + * thrown exception. + * + * @typedef ScriptEvaluateResultException + * + * @property {ExceptionDetails} exceptionDetails + * @property {string} realm + * @property {ScriptEvaluateResultType} [type=ScriptEvaluateResultType.Exception] + */ + + /** + * Used as return value for script.evaluate when the script completes + * normally. + * + * @typedef ScriptEvaluateResultSuccess + * + * @property {string} realm + * @property {RemoteValue} result + * @property {ScriptEvaluateResultType} [type=ScriptEvaluateResultType.Success] + */ + + /** + * Calls a provided function with given arguments and scope in the provided + * target, which is either a realm or a browsing context. + * + * @param {Object=} options + * @param {Array=} arguments + * The arguments to pass to the function call. + * @param {boolean} awaitPromise + * Determines if the command should wait for the return value of the + * expression to resolve, if this return value is a Promise. + * @param {string} functionDeclaration + * The expression to evaluate. + * @param {OwnershipModel=} resultOwnership + * The ownership model to use for the results of this evaluation. Defaults + * to `OwnershipModel.None`. + * @param {Object} target + * The target for the evaluation, which either matches the definition for + * a RealmTarget or for ContextTarget. + * @param {RemoteValue=} this + * The value of the this keyword for the function call. + * + * @returns {ScriptEvaluateResult} + * + * @throws {InvalidArgumentError} + * If any of the arguments does not have the expected type. + * @throws {NoSuchFrameError} + * If the target cannot be found. + */ + async callFunction(options = {}) { + const { + arguments: commandArguments = null, + awaitPromise, + functionDeclaration, + resultOwnership = lazy.OwnershipModel.None, + target = {}, + this: thisParameter = null, + } = options; + + lazy.assert.string( + functionDeclaration, + `Expected "functionDeclaration" to be a string, got ${functionDeclaration}` + ); + + lazy.assert.boolean( + awaitPromise, + `Expected "awaitPromise" to be a boolean, got ${awaitPromise}` + ); + + this.#assertResultOwnership(resultOwnership); + + if (commandArguments != null) { + lazy.assert.array( + commandArguments, + `Expected "arguments" to be an array, got ${commandArguments}` + ); + } + + const { contextId, realmId, sandbox } = this.#assertTarget(target); + const context = await this.#getContextFromTarget({ contextId, realmId }); + const evaluationResult = await this.messageHandler.forwardCommand({ + moduleName: "script", + commandName: "callFunctionDeclaration", + destination: { + type: lazy.WindowGlobalMessageHandler.type, + id: context.id, + }, + params: { + awaitPromise, + commandArguments, + functionDeclaration, + realmId, + resultOwnership, + sandbox, + thisParameter, + }, + }); + + return this.#buildReturnValue(evaluationResult); + } + + /** + * The script.disown command disowns the given handles. This does not + * guarantee the handled object will be garbage collected, as there can be + * other handles or strong ECMAScript references. + * + * @param {Object=} options + * @param {Array} handles + * Array of handle ids to disown. + * @param {Object} target + * The target owning the handles, which either matches the definition for + * a RealmTarget or for ContextTarget. + */ + async disown(options = {}) { + const { handles, target = {} } = options; + + lazy.assert.array( + handles, + `Expected "handles" to be an array, got ${handles}` + ); + handles.forEach(handle => { + lazy.assert.string( + handle, + `Expected "handles" to be an array of strings, got ${handle}` + ); + }); + + const { contextId, realmId, sandbox } = this.#assertTarget(target); + const context = await this.#getContextFromTarget({ contextId, realmId }); + await this.messageHandler.forwardCommand({ + moduleName: "script", + commandName: "disownHandles", + destination: { + type: lazy.WindowGlobalMessageHandler.type, + id: context.id, + }, + params: { + handles, + realmId, + sandbox, + }, + }); + } + + /** + * Evaluate a provided expression in the provided target, which is either a + * realm or a browsing context. + * + * @param {Object=} options + * @param {boolean} awaitPromise + * Determines if the command should wait for the return value of the + * expression to resolve, if this return value is a Promise. + * @param {string} expression + * The expression to evaluate. + * @param {OwnershipModel=} resultOwnership + * The ownership model to use for the results of this evaluation. Defaults + * to `OwnershipModel.None`. + * @param {Object} target + * The target for the evaluation, which either matches the definition for + * a RealmTarget or for ContextTarget. + * + * @returns {ScriptEvaluateResult} + * + * @throws {InvalidArgumentError} + * If any of the arguments does not have the expected type. + * @throws {NoSuchFrameError} + * If the target cannot be found. + */ + async evaluate(options = {}) { + const { + awaitPromise, + expression: source, + resultOwnership = lazy.OwnershipModel.None, + target = {}, + } = options; + + lazy.assert.string( + source, + `Expected "expression" to be a string, got ${source}` + ); + + lazy.assert.boolean( + awaitPromise, + `Expected "awaitPromise" to be a boolean, got ${awaitPromise}` + ); + + this.#assertResultOwnership(resultOwnership); + + const { contextId, realmId, sandbox } = this.#assertTarget(target); + const context = await this.#getContextFromTarget({ contextId, realmId }); + const evaluationResult = await this.messageHandler.forwardCommand({ + moduleName: "script", + commandName: "evaluateExpression", + destination: { + type: lazy.WindowGlobalMessageHandler.type, + id: context.id, + }, + params: { + awaitPromise, + expression: source, + realmId, + resultOwnership, + sandbox, + }, + }); + + return this.#buildReturnValue(evaluationResult); + } + + /** + * An object that holds basic information about a realm. + * + * @typedef BaseRealmInfo + * + * @property {string} id + * The realm unique identifier. + * @property {string} origin + * The serialization of an origin. + */ + + /** + * + * @typedef WindowRealmInfoProperties + * + * @property {string} context + * The browsing context id, associated with the realm. + * @property {string=} sandbox + * The name of the sandbox. If the value is null or empty + * string, the default realm will be returned. + * @property {RealmType.Window} type + * The window realm type. + */ + + /** + * An object that holds information about a window realm. + * + * @typedef {BaseRealmInfo & WindowRealmInfoProperties} WindowRealmInfo + */ + + /** + * An object that holds information about a realm. + * + * @typedef {WindowRealmInfo} RealmInfo + */ + + /** + * An object that holds a list of realms. + * + * @typedef ScriptGetRealmsResult + * + * @property {Array} realms + * List of realms. + */ + + /** + * Returns a list of all realms, optionally filtered to realms + * of a specific type, or to the realms associated with + * a specified browsing context. + * + * @param {Object=} options + * @param {string=} context + * The id of the browsing context to filter + * only realms associated with it. If not provided, return realms + * associated with all browsing contexts. + * @param {RealmType=} type + * Type of realm to filter. + * If not provided, return realms of all types. + * + * @returns {ScriptGetRealmsResult} + * + * @throws {InvalidArgumentError} + * If any of the arguments does not have the expected type. + * @throws {NoSuchFrameError} + * If the context cannot be found. + */ + async getRealms(options = {}) { + const { context: contextId = null, type = null } = options; + const destination = {}; + + if (contextId !== null) { + lazy.assert.string( + contextId, + `Expected "context" to be a string, got ${contextId}` + ); + destination.id = this.#getBrowsingContext(contextId).id; + } else { + destination.contextDescriptor = { + type: lazy.ContextDescriptorType.All, + }; + } + + if (type !== null) { + const supportedRealmTypes = Object.values(lazy.RealmType); + if (!supportedRealmTypes.includes(type)) { + throw new lazy.error.InvalidArgumentError( + `Expected "type" to be one of ${supportedRealmTypes}, got ${type}` + ); + } + + // Remove this check when other realm types are supported + if (type !== lazy.RealmType.Window) { + throw new lazy.error.UnsupportedOperationError( + `Unsupported "type": ${type}. Only "type" ${lazy.RealmType.Window} is currently supported.` + ); + } + } + + return { realms: await this.#getRealmInfos(destination) }; + } + + #assertResultOwnership(resultOwnership) { + if ( + ![lazy.OwnershipModel.None, lazy.OwnershipModel.Root].includes( + resultOwnership + ) + ) { + throw new lazy.error.InvalidArgumentError( + `Expected "resultOwnership" to be one of ${Object.values( + lazy.OwnershipModel + )}, got ${resultOwnership}` + ); + } + } + + #assertTarget(target) { + lazy.assert.object( + target, + `Expected "target" to be an object, got ${target}` + ); + + const { + context: contextId = null, + realm: realmId = null, + sandbox = null, + } = target; + + if (realmId != null && (contextId != null || sandbox != null)) { + throw new lazy.error.InvalidArgumentError( + `A context and a realm reference are mutually exclusive` + ); + } + + if (contextId != null) { + lazy.assert.string( + contextId, + `Expected "context" to be a string, got ${contextId}` + ); + + if (sandbox != null) { + lazy.assert.string( + sandbox, + `Expected "sandbox" to be a string, got ${sandbox}` + ); + } + } else if (realmId != null) { + lazy.assert.string( + realmId, + `Expected "realm" to be a string, got ${realmId}` + ); + } else { + throw new lazy.error.InvalidArgumentError(`No context or realm provided`); + } + + return { contextId, realmId, sandbox }; + } + + #buildReturnValue(evaluationResult) { + const rv = { realm: evaluationResult.realmId }; + switch (evaluationResult.evaluationStatus) { + // TODO: Compare with EvaluationStatus.Normal after Bug 1774444 is fixed. + case "normal": + rv.type = ScriptEvaluateResultType.Success; + rv.result = evaluationResult.result; + break; + // TODO: Compare with EvaluationStatus.Throw after Bug 1774444 is fixed. + case "throw": + rv.type = ScriptEvaluateResultType.Exception; + rv.exceptionDetails = evaluationResult.exceptionDetails; + break; + default: + throw new lazy.error.UnsupportedOperationError( + `Unsupported evaluation status ${evaluationResult.evaluationStatus}` + ); + } + return rv; + } + + #getBrowsingContext(contextId) { + const context = lazy.TabManager.getBrowsingContextById(contextId); + if (context === null) { + throw new lazy.error.NoSuchFrameError( + `Browsing Context with id ${contextId} not found` + ); + } + + if (!context.currentWindowGlobal) { + throw new lazy.error.NoSuchFrameError( + `No window found for BrowsingContext with id ${contextId}` + ); + } + + return context; + } + + async #getContextFromTarget({ contextId, realmId }) { + if (contextId !== null) { + return this.#getBrowsingContext(contextId); + } + + const destination = { + contextDescriptor: { + type: lazy.ContextDescriptorType.All, + }, + }; + const realms = await this.#getRealmInfos(destination); + const realm = realms.find(realm => realm.realm == realmId); + + if (realm && realm.context !== null) { + return this.#getBrowsingContext(realm.context); + } + + throw new lazy.error.NoSuchFrameError(`Realm with id ${realmId} not found`); + } + + async #getRealmInfos(destination) { + let realms = await this.messageHandler.forwardCommand({ + moduleName: "script", + commandName: "getWindowRealms", + destination: { + type: lazy.WindowGlobalMessageHandler.type, + ...destination, + }, + }); + + const isBroadcast = !!destination.contextDescriptor; + if (!isBroadcast) { + realms = [realms]; + } + + return realms + .flat() + .map(realm => { + // Resolve browsing context to a TabManager id. + realm.context = lazy.TabManager.getIdForBrowsingContext(realm.context); + return realm; + }) + .filter(realm => realm.context !== null); + } + + static get supportedEvents() { + return []; + } +} + +export const script = ScriptModule; diff --git a/remote/webdriver-bidi/modules/root/session.sys.mjs b/remote/webdriver-bidi/modules/root/session.sys.mjs new file mode 100644 index 0000000000..281b913f32 --- /dev/null +++ b/remote/webdriver-bidi/modules/root/session.sys.mjs @@ -0,0 +1,406 @@ +/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", + ContextDescriptorType: + "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + RootMessageHandler: + "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", +}); + +class SessionModule extends Module { + #browsingContextIdEventMap; + #globalEventSet; + + constructor(messageHandler) { + super(messageHandler); + + // Map with top-level browsing context id keys and values + // that are a set of event names for events + // that are enabled in the given browsing context. + // TODO: Bug 1804417. Use navigable instead of browsing context id. + this.#browsingContextIdEventMap = new Map(); + + // Set of event names which are strings of the form [moduleName].[eventName] + // for events that are enabled for all browsing contexts. + // We should only add an actual event listener on the MessageHandler the + // first time an event is subscribed to. + this.#globalEventSet = new Set(); + } + + destroy() { + this.#browsingContextIdEventMap = null; + this.#globalEventSet = null; + } + + /** + * Commands + */ + + /** + * Enable certain events either globally, or for a list of browsing contexts. + * + * @params {Object=} params + * @params {Array} events + * List of events to subscribe to. + * @params {Array=} contexts + * Optional list of top-level browsing context ids + * to subscribe the events for. + * + * @throws {InvalidArgumentError} + * If events or contexts are not valid types. + */ + async subscribe(params = {}) { + const { events, contexts: contextIds = null } = params; + + // Check input types until we run schema validation. + lazy.assert.array(events, "events: array value expected"); + events.forEach(name => { + lazy.assert.string(name, `${name}: string value expected`); + }); + + if (contextIds !== null) { + lazy.assert.array(contextIds, "contexts: array value expected"); + contextIds.forEach(contextId => { + lazy.assert.string(contextId, `${contextId}: string value expected`); + }); + } + + const listeners = this.#updateEventMap(events, contextIds, true); + + // TODO: Bug 1801284. Add subscribe priority sorting of subscribeStepEvents (step 4 to 6, and 8). + + // Subscribe to the relevant engine-internal events. + await this.messageHandler.eventsDispatcher.update(listeners); + } + + /** + * Disable certain events either globally, or for a list of browsing contexts. + * + * @params {Object=} params + * @params {Array} events + * List of events to unsubscribe from. + * @params {Array=} contexts + * Optional list of top-level browsing context ids + * to unsubscribe the events from. + * + * @throws {InvalidArgumentError} + * If events or contexts are not valid types. + */ + async unsubscribe(params = {}) { + const { events, contexts: contextIds = null } = params; + + // Check input types until we run schema validation. + lazy.assert.array(events, "events: array value expected"); + events.forEach(name => { + lazy.assert.string(name, `${name}: string value expected`); + }); + if (contextIds !== null) { + lazy.assert.array(contextIds, "contexts: array value expected"); + contextIds.forEach(contextId => { + lazy.assert.string(contextId, `${contextId}: string value expected`); + }); + } + + const listeners = this.#updateEventMap(events, contextIds, false); + + // Unsubscribe from the relevant engine-internal events. + await this.messageHandler.eventsDispatcher.update(listeners); + } + + #assertModuleSupportsEvent(moduleName, event) { + const rootModuleClass = this.#getRootModuleClass(moduleName); + if (!rootModuleClass?.supportsEvent(event)) { + throw new lazy.error.InvalidArgumentError( + `${event} is not a valid event name` + ); + } + } + + #getBrowserIdForContextId(contextId) { + const context = lazy.TabManager.getBrowsingContextById(contextId); + if (!context) { + throw new lazy.error.NoSuchFrameError( + `Browsing context with id ${contextId} not found` + ); + } + + return context.browserId; + } + + #getRootModuleClass(moduleName) { + // Modules which support event subscriptions should have a root module + // defining supported events. + const rootDestination = { type: lazy.RootMessageHandler.type }; + const moduleClasses = this.messageHandler.getAllModuleClasses( + moduleName, + rootDestination + ); + + if (!moduleClasses.length) { + throw new lazy.error.InvalidArgumentError( + `Module ${moduleName} does not exist` + ); + } + + return moduleClasses[0]; + } + + #getTopBrowsingContextId(contextId) { + const context = lazy.TabManager.getBrowsingContextById(contextId); + if (!context) { + throw new lazy.error.NoSuchFrameError( + `Browsing context with id ${contextId} not found` + ); + } + const topContext = context.top; + return lazy.TabManager.getIdForBrowsingContext(topContext); + } + + /** + * Obtain a set of events based on the given event name. + * + * Could contain a period for a specific event, + * or just the module name for all events. + * + * @param {string} event + * Name of the event to process. + * + * @returns {Set} + * A Set with the expanded events in the form of `.`. + * + * @throws {InvalidArgumentError} + * If event does not reference a valid event. + */ + #obtainEvents(event) { + const events = new Set(); + + // Check if a period is present that splits the event name into the module, + // and the actual event. Hereby only care about the first found instance. + const index = event.indexOf("."); + if (index >= 0) { + const [moduleName] = event.split("."); + this.#assertModuleSupportsEvent(moduleName, event); + events.add(event); + } else { + // Interpret the name as module, and register all its available events + const rootModuleClass = this.#getRootModuleClass(event); + const supportedEvents = rootModuleClass?.supportedEvents; + + for (const eventName of supportedEvents) { + events.add(eventName); + } + } + + return events; + } + + /** + * Obtain a list of event enabled browsing context ids. + * + * @see https://w3c.github.io/webdriver-bidi/#event-enabled-browsing-contexts + * + * @param {string} eventName + * The name of the event. + * + * @return {Set} The set of browsing context. + */ + #obtainEventEnabledBrowsingContextIds(eventName) { + const contextIds = new Set(); + for (const [ + contextId, + events, + ] of this.#browsingContextIdEventMap.entries()) { + if (events.has(eventName)) { + // Check that a browsing context still exists for a given id + const context = lazy.TabManager.getBrowsingContextById(contextId); + if (context) { + contextIds.add(contextId); + } + } + } + + return contextIds; + } + + #onMessageHandlerEvent = (name, event) => { + this.messageHandler.emitProtocolEvent(name, event); + }; + + /** + * Update global event state for top-level browsing contexts. + * + * @see https://w3c.github.io/webdriver-bidi/#update-the-event-map + * + * @param {Array} requestedEventNames + * The list of the event names to run the update for. + * @param {Array|null} browsingContextIds + * The list of the browsing context ids to update or null. + * @param {boolean} enabled + * True, if events have to be enabled. Otherwise false. + * + * @return {Array} subscriptions + * The list of information to subscribe/unsubscribe to. + * + * @throws {InvalidArgumentError} + * If failed unsubscribe from event from requestedEventNames for + * browsing context id from browsingContextIds, if present. + */ + #updateEventMap(requestedEventNames, browsingContextIds, enabled) { + const globalEventSet = new Set(this.#globalEventSet); + const eventMap = structuredClone(this.#browsingContextIdEventMap); + + const eventNames = new Set(); + + requestedEventNames.forEach(name => { + this.#obtainEvents(name).forEach(event => eventNames.add(event)); + }); + const enabledEvents = new Map(); + const subscriptions = []; + + if (browsingContextIds === null) { + // Subscribe or unsubscribe events for all browsing contexts. + if (enabled) { + // Subscribe to each event. + + // Get the list of all top level browsing context ids. + const allTopBrowsingContextIds = lazy.TabManager.allBrowserUniqueIds; + + for (const eventName of eventNames) { + if (!globalEventSet.has(eventName)) { + const alreadyEnabledContextIds = this.#obtainEventEnabledBrowsingContextIds( + eventName + ); + globalEventSet.add(eventName); + for (const contextId of alreadyEnabledContextIds) { + eventMap.get(contextId).delete(eventName); + + // Since we're going to subscribe to all top-level + // browsing context ids to not have duplicate subscriptions, + // we have to unsubscribe from already subscribed. + subscriptions.push({ + event: eventName, + contextDescriptor: { + type: lazy.ContextDescriptorType.TopBrowsingContext, + id: this.#getBrowserIdForContextId(contextId), + }, + callback: this.#onMessageHandlerEvent, + enable: false, + }); + } + + // Get a list of all top-level browsing context ids + // that are not contained in alreadyEnabledContextIds. + const newlyEnabledContextIds = allTopBrowsingContextIds.filter( + contextId => !alreadyEnabledContextIds.has(contextId) + ); + + enabledEvents.set(eventName, newlyEnabledContextIds); + + subscriptions.push({ + event: eventName, + contextDescriptor: { + type: lazy.ContextDescriptorType.All, + }, + callback: this.#onMessageHandlerEvent, + enable: true, + }); + } + } + } else { + // Unsubscribe each event which has a global subscription. + for (const eventName of eventNames) { + if (globalEventSet.has(eventName)) { + globalEventSet.delete(eventName); + + subscriptions.push({ + event: eventName, + contextDescriptor: { + type: lazy.ContextDescriptorType.All, + }, + callback: this.#onMessageHandlerEvent, + enable: false, + }); + } else { + throw new lazy.error.InvalidArgumentError( + `Failed to unsubscribe from event ${eventName}` + ); + } + } + } + } else { + // Subscribe or unsubscribe events for given list of browsing context ids. + const targets = new Map(); + for (const contextId of browsingContextIds) { + const topLevelContextId = this.#getTopBrowsingContextId(contextId); + if (!eventMap.has(topLevelContextId)) { + eventMap.set(topLevelContextId, new Set()); + } + targets.set(topLevelContextId, eventMap.get(topLevelContextId)); + } + + for (const eventName of eventNames) { + // Do nothing if we want to subscribe, + // but the event has already a global subscription. + if (enabled && this.#globalEventSet.has(eventName)) { + continue; + } + for (const [contextId, target] of targets.entries()) { + // Subscribe if an event doesn't have a subscription for a specific context id. + if (enabled && !target.has(eventName)) { + target.add(eventName); + if (!enabledEvents.has(eventName)) { + enabledEvents.set(eventName, new Set()); + } + enabledEvents.get(eventName).add(contextId); + + subscriptions.push({ + event: eventName, + contextDescriptor: { + type: lazy.ContextDescriptorType.TopBrowsingContext, + id: this.#getBrowserIdForContextId(contextId), + }, + callback: this.#onMessageHandlerEvent, + enable: true, + }); + } else if (!enabled) { + // Unsubscribe from each event for a specific context id if the event has a subscription. + if (target.has(eventName)) { + target.delete(eventName); + + subscriptions.push({ + event: eventName, + contextDescriptor: { + type: lazy.ContextDescriptorType.TopBrowsingContext, + id: this.#getBrowserIdForContextId(contextId), + }, + callback: this.#onMessageHandlerEvent, + enable: false, + }); + } else { + throw new lazy.error.InvalidArgumentError( + `Failed to unsubscribe from event ${eventName} for context ${contextId}` + ); + } + } + } + } + } + + this.#globalEventSet = globalEventSet; + this.#browsingContextIdEventMap = eventMap; + + return subscriptions; + } +} + +// To export the class as lower-case +export const session = SessionModule; -- cgit v1.2.3