diff options
Diffstat (limited to 'remote/webdriver-bidi/modules/root/script.sys.mjs')
-rw-r--r-- | remote/webdriver-bidi/modules/root/script.sys.mjs | 747 |
1 files changed, 747 insertions, 0 deletions
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..b5ab7fa30d --- /dev/null +++ b/remote/webdriver-bidi/modules/root/script.sys.mjs @@ -0,0 +1,747 @@ +/* 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", + generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", + OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", + RealmType: "chrome://remote/content/shared/Realm.sys.mjs", + setDefaultAndAssertSerializationOptions: + "chrome://remote/content/webdriver-bidi/RemoteValue.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 { + #preloadScriptMap; + + constructor(messageHandler) { + super(messageHandler); + + // Map in which the keys are UUIDs, and the values are structs + // with an item named expression, which is a string, + // and an item named sandbox which is a string or null. + this.#preloadScriptMap = new Map(); + } + + destroy() { + this.#preloadScriptMap = null; + } + + /** + * Used as return value for script.addPreloadScript command. + * + * @typedef AddPreloadScriptResult + * + * @property {string} script + * The unique id associated with added preload script. + */ + + /** + * @typedef ChannelProperties + * + * @property {string} channel + * The channel id. + * @property {SerializationOptions=} serializationOptions + * An object which holds the information of how the result of evaluation + * in case of ECMAScript objects should be serialized. + * @property {OwnershipModel=} ownership + * The ownership model to use for the results of this evaluation. Defaults + * to `OwnershipModel.None`. + */ + + /** + * Represents a channel used to send custom messages from preload script + * to clients. + * + * @typedef ChannelValue + * + * @property {'channel'} type + * @property {ChannelProperties} value + */ + + /** + * Adds a preload script, which runs on creation of a new Window, + * before any author-defined script have run. + * + * @param {object=} options + * @param {Array<ChannelValue>=} options.arguments + * The arguments to pass to the function call. + * @param {string} options.functionDeclaration + * The expression to evaluate. + * @param {string=} options.sandbox + * The name of the sandbox. If the value is null or empty + * string, the default realm will be used. + * + * @returns {AddPreloadScriptResult} + * + * @throws {InvalidArgumentError} + * If any of the arguments does not have the expected type. + */ + async addPreloadScript(options = {}) { + const { + arguments: commandArguments = [], + functionDeclaration, + sandbox = null, + } = options; + + lazy.assert.string( + functionDeclaration, + `Expected "functionDeclaration" to be a string, got ${functionDeclaration}` + ); + + if (sandbox != null) { + lazy.assert.string( + sandbox, + `Expected "sandbox" to be a string, got ${sandbox}` + ); + } + + lazy.assert.array( + commandArguments, + `Expected "arguments" to be an array, got ${commandArguments}` + ); + lazy.assert.that( + commandArguments => + commandArguments.every(({ type, value }) => { + if (type === "channel") { + this.#assertChannelArgument(value); + return true; + } + return false; + }), + `One of the arguments has an unsupported type, only type "channel" is supported` + )(commandArguments); + + const script = lazy.generateUUID(); + const preloadScript = { + arguments: commandArguments, + functionDeclaration, + sandbox, + }; + + this.#preloadScriptMap.set(script, preloadScript); + + await this.messageHandler.addSessionDataItem({ + category: "preload-script", + moduleName: "script", + values: [ + { + ...preloadScript, + script, + }, + ], + contextDescriptor: { + type: lazy.ContextDescriptorType.All, + }, + }); + + return { script }; + } + + /** + * 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<StackFrame>} 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<RemoteValue>=} options.arguments + * The arguments to pass to the function call. + * @param {boolean} options.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} options.functionDeclaration + * The expression to evaluate. + * @param {OwnershipModel=} options.resultOwnership + * The ownership model to use for the results of this evaluation. Defaults + * to `OwnershipModel.None`. + * @param {SerializationOptions=} options.serializationOptions + * An object which holds the information of how the result of evaluation + * in case of ECMAScript objects should be serialized. + * @param {object} options.target + * The target for the evaluation, which either matches the definition for + * a RealmTarget or for ContextTarget. + * @param {RemoteValue=} options.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, + serializationOptions, + 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}` + ); + commandArguments.forEach(({ type, value }) => { + if (type === "channel") { + this.#assertChannelArgument(value); + } + }); + } + + const { contextId, realmId, sandbox } = this.#assertTarget(target); + const context = await this.#getContextFromTarget({ contextId, realmId }); + const serializationOptionsWithDefaults = + lazy.setDefaultAndAssertSerializationOptions(serializationOptions); + 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, + serializationOptions: serializationOptionsWithDefaults, + 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<string>} options.handles + * Array of handle ids to disown. + * @param {object} options.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} options.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} options.expression + * The expression to evaluate. + * @param {OwnershipModel=} options.resultOwnership + * The ownership model to use for the results of this evaluation. Defaults + * to `OwnershipModel.None`. + * @param {SerializationOptions=} options.serializationOptions + * An object which holds the information of how the result of evaluation + * in case of ECMAScript objects should be serialized. + * @param {object} options.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, + serializationOptions, + 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 serializationOptionsWithDefaults = + lazy.setDefaultAndAssertSerializationOptions(serializationOptions); + 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, + serializationOptions: serializationOptionsWithDefaults, + }, + }); + + 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. + */ + + /* eslint-disable jsdoc/valid-types */ + /** + * An object that holds information about a window realm. + * + * @typedef {BaseRealmInfo & WindowRealmInfoProperties} WindowRealmInfo + */ + /* eslint-enable jsdoc/valid-types */ + + /** + * An object that holds information about a realm. + * + * @typedef {WindowRealmInfo} RealmInfo + */ + + /** + * An object that holds a list of realms. + * + * @typedef ScriptGetRealmsResult + * + * @property {Array<RealmInfo>} 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=} options.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=} options.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) }; + } + + /** + * Removes a preload script. + * + * @param {object=} options + * @param {string} options.script + * The unique id associated with a preload script. + * + * @throws {InvalidArgumentError} + * If any of the arguments does not have the expected type. + * @throws {NoSuchScriptError} + * If the script cannot be found. + */ + async removePreloadScript(options = {}) { + const { script } = options; + + lazy.assert.string( + script, + `Expected "script" to be a string, got ${script}` + ); + + if (!this.#preloadScriptMap.has(script)) { + throw new lazy.error.NoSuchScriptError( + `Preload script with id ${script} not found` + ); + } + + const preloadScript = this.#preloadScriptMap.get(script); + + await this.messageHandler.removeSessionDataItem({ + category: "preload-script", + moduleName: "script", + values: [ + { + ...preloadScript, + script, + }, + ], + contextDescriptor: { + type: lazy.ContextDescriptorType.All, + }, + }); + + this.#preloadScriptMap.delete(script); + } + + #assertChannelArgument(value) { + lazy.assert.object(value); + const { + channel, + ownership = lazy.OwnershipModel.None, + serializationOptions, + } = value; + lazy.assert.string(channel); + lazy.setDefaultAndAssertSerializationOptions(serializationOptions); + lazy.assert.that( + ownership => + [lazy.OwnershipModel.None, lazy.OwnershipModel.Root].includes( + ownership + ), + `Expected "ownership" to be one of ${Object.values( + lazy.OwnershipModel + )}, got ${ownership}` + )(ownership); + + return true; + } + + #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 ["script.message"]; + } +} + +export const script = ScriptModule; |