diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/server/actors/webconsole/commands/manager.js | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/actors/webconsole/commands/manager.js')
-rw-r--r-- | devtools/server/actors/webconsole/commands/manager.js | 577 |
1 files changed, 577 insertions, 0 deletions
diff --git a/devtools/server/actors/webconsole/commands/manager.js b/devtools/server/actors/webconsole/commands/manager.js new file mode 100644 index 0000000000..3fa274a0f5 --- /dev/null +++ b/devtools/server/actors/webconsole/commands/manager.js @@ -0,0 +1,577 @@ +/* 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/. */ + +"use strict"; + +loader.lazyRequireGetter( + this, + ["isCommand"], + "resource://devtools/server/actors/webconsole/commands/parser.js", + true +); + +/** + * WebConsole commands manager. + * + * Defines a set of functions / variables ("commands") that are available from + * the Web Console but not from the web page. + * + */ +const WebConsoleCommandsManager = { + _registeredCommands: new Map(), + + /** + * Register a new command. + * @param {string} name The command name (exemple: "$") + * @param {(function|object)} command The command to register. + * It can be a function so the command is a function (like "$()"), + * or it can also be a property descriptor to describe a getter(like + * "$0"). + * + * The command function or the command getter are passed a owner object as + * their first parameter (see the example below). + * + * @example + * + * WebConsoleCommandsManager.register("$", function JSTH_$(owner, selector) + * { + * return owner.window.document.querySelector(selector); + * }); + * + * WebConsoleCommandsManager.register("$0", { + * get: function(owner) { + * return owner.makeDebuggeeValue(owner.selectedNode); + * } + * }); + */ + register(name, command) { + if ( + typeof command != "function" && + !(typeof command == "object" && typeof command.get == "function") + ) { + throw new Error( + "Invalid web console command. It can only be a function, or an object with a function as 'get' attribute" + ); + } + this._registeredCommands.set(name, command); + }, + + /** + * Return the name of all registered commands. + * + * @return {array} List of all command names. + */ + getAllCommandNames() { + return [...this._registeredCommands.keys()]; + }, + + /** + * There is two types of "commands" here. + * + * - Functions or variables exposed in the scope of the evaluated string from the WebConsole input. + * Example: $(), $0, copy(), clear(),... + * - "True commands", which can also be ran from the WebConsole input with ":" prefix. + * Example: this list of commands. + * Note that some "true commands" are not exposed as function (see getColonOnlyCommandNames). + * + * The following list distinguish these "true commands" from the first category. + * It especially avoid any JavaScript evaluation when the frontend tries to execute + * a string starting with ':' character. + */ + getAllColonCommandNames() { + return ["block", "help", "history", "screenshot", "unblock"]; + }, + + /** + * Some commands are not exposed in the scope of the evaluated string, + * and can only be used via `:command-name`. + */ + getColonOnlyCommandNames() { + return ["screenshot"]; + }, + + /** + * Map of all command objects keyed by command name. + * Commands object are the objects passed to register() method. + * + * @return {Map<string -> command>} + */ + getAllCommands() { + return this._registeredCommands; + }, + + /** + * Is the command name possibly overriding a symbol which + * already exists in the paused frame, or global into which + * we are about to execute into? + */ + _isCommandNameAlreadyInScope(name, frame, dbgGlobal) { + // Fallback on global scope when Debugger.Frame doesn't come along an Environment. + if (frame && frame.environment) { + return !!frame.environment.find(name); + } + return !!dbgGlobal.getOwnPropertyDescriptor(name); + }, + + /** + * Create an object with the API we expose to the Web Console during + * JavaScript evaluation. + * This object inherits properties and methods from the Web Console actor. + * + * @param object consoleActor + * The related web console actor evaluating some code. + * @param object debuggerGlobal + * A Debugger.Object that wraps a content global. This is used for the + * Web Console Commands. + * @param object frame (optional) + * The frame where the string was evaluated. + * @param string evalInput + * String to evaluate. + * @param string selectedNodeActorID + * The Node actor ID of the currently selected DOM Element, if any is selected. + * + * @return object + * Object with two properties: + * - 'bindings', the object with all commands set as attribute on this object. + * - 'getHelperResult', a live getter returning the additional data the last command + * which executed want to convey to the frontend. + * (The return value of commands isn't returned to the client but it only + * returned to the code ran from console evaluation) + */ + getWebConsoleCommands( + consoleActor, + debuggerGlobal, + frame, + evalInput, + selectedNodeActorID + ) { + const bindings = Object.create(null); + + const owner = { + window: consoleActor.evalGlobal, + makeDebuggeeValue: debuggerGlobal.makeDebuggeeValue.bind(debuggerGlobal), + createValueGrip: consoleActor.createValueGrip.bind(consoleActor), + preprocessDebuggerObject: + consoleActor.preprocessDebuggerObject.bind(consoleActor), + helperResult: null, + consoleActor, + evalInput, + }; + if (selectedNodeActorID) { + const actor = consoleActor.conn.getActor(selectedNodeActorID); + if (actor) { + owner.selectedNode = actor.rawNode; + } + } + + const evalGlobal = consoleActor.evalGlobal; + function maybeExport(obj, name) { + if (typeof obj[name] != "function") { + return; + } + + // By default, chrome-implemented functions that are exposed to content + // refuse to accept arguments that are cross-origin for the caller. This + // is generally the safe thing, but causes problems for certain console + // helpers like cd(), where we users sometimes want to pass a cross-origin + // window. To circumvent this restriction, we use exportFunction along + // with a special option designed for this purpose. See bug 1051224. + obj[name] = Cu.exportFunction(obj[name], evalGlobal, { + allowCrossOriginArguments: true, + }); + } + + // Not supporting extra commands in workers yet. This should be possible to + // add one by one as long as they don't require jsm, Cu, etc. + const commands = isWorker ? [] : this.getAllCommands(); + + // Is it a command evaluation, starting with ':' prefix? + const isCmd = isCommand(evalInput); + + const colonOnlyCommandNames = this.getColonOnlyCommandNames(); + for (const [name, command] of commands) { + // When we are running command via `:` prefix, no user code is being ran and only the command executes, + // so always expose the commands as the command will try to call its JavaScript method (see getEvalInput). + // Otherwise, when we run user code, we want to avoid overriding existing symbols with commands. + // Also ignore commands which can only be run with the `:` prefix. + if ( + !isCmd && + (this._isCommandNameAlreadyInScope(name, frame, debuggerGlobal) || + colonOnlyCommandNames.includes(name)) + ) { + continue; + } + + const descriptor = { + // We force the enumerability and the configurability (so the + // WebConsoleActor can reconfigure the property). + enumerable: true, + configurable: true, + }; + + if (typeof command === "function") { + // Function commands + descriptor.value = command.bind(undefined, owner); + maybeExport(descriptor, "value"); + // Make sure the helpers can be used during eval. + descriptor.value = debuggerGlobal.makeDebuggeeValue(descriptor.value); + } else if (typeof command?.get === "function") { + // Getter commands + descriptor.get = command.get.bind(undefined, owner); + maybeExport(descriptor, "get"); + } + Object.defineProperty(bindings, name, descriptor); + } + + return { + // Use a method as commands will update owner.helperResult later + getHelperResult() { + return owner.helperResult; + }, + bindings, + }; + }, +}; + +exports.WebConsoleCommandsManager = WebConsoleCommandsManager; + +/* + * Built-in commands. + * + * A list of helper functions used by Firebug can be found here: + * http://getfirebug.com/wiki/index.php/Command_Line_API + */ + +/** + * Find a node by ID. + * + * @param string id + * The ID of the element you want. + * @return Node or null + * The result of calling document.querySelector(selector). + */ +WebConsoleCommandsManager.register("$", function (owner, selector) { + try { + return owner.window.document.querySelector(selector); + } catch (err) { + // Throw an error like `err` but that belongs to `owner.window`. + throw new owner.window.DOMException(err.message, err.name); + } +}); + +/** + * Find the nodes matching a CSS selector. + * + * @param string selector + * A string that is passed to window.document.querySelectorAll. + * @return NodeList + * Returns the result of document.querySelectorAll(selector). + */ +WebConsoleCommandsManager.register("$$", function (owner, selector) { + let nodes; + try { + nodes = owner.window.document.querySelectorAll(selector); + } catch (err) { + // Throw an error like `err` but that belongs to `owner.window`. + throw new owner.window.DOMException(err.message, err.name); + } + + // Calling owner.window.Array.from() doesn't work without accessing the + // wrappedJSObject, so just loop through the results instead. + const result = new owner.window.Array(); + for (let i = 0; i < nodes.length; i++) { + result.push(nodes[i]); + } + return result; +}); + +/** + * Returns the result of the last console input evaluation + * + * @return object|undefined + * Returns last console evaluation or undefined + */ +WebConsoleCommandsManager.register("$_", { + get(owner) { + return owner.consoleActor.getLastConsoleInputEvaluation(); + }, +}); + +/** + * Runs an xPath query and returns all matched nodes. + * + * @param string xPath + * xPath search query to execute. + * @param [optional] Node context + * Context to run the xPath query on. Uses window.document if not set. + * @param [optional] string|number resultType + Specify the result type. Default value XPathResult.ANY_TYPE + * @return array of Node + */ +WebConsoleCommandsManager.register( + "$x", + function ( + owner, + xPath, + context, + resultType = owner.window.XPathResult.ANY_TYPE + ) { + const nodes = new owner.window.Array(); + // Not waiving Xrays, since we want the original Document.evaluate function, + // instead of anything that's been redefined. + const doc = owner.window.document; + context = context || doc; + switch (resultType) { + case "number": + resultType = owner.window.XPathResult.NUMBER_TYPE; + break; + + case "string": + resultType = owner.window.XPathResult.STRING_TYPE; + break; + + case "bool": + resultType = owner.window.XPathResult.BOOLEAN_TYPE; + break; + + case "node": + resultType = owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE; + break; + + case "nodes": + resultType = owner.window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE; + break; + } + const results = doc.evaluate(xPath, context, null, resultType, null); + if (results.resultType === owner.window.XPathResult.NUMBER_TYPE) { + return results.numberValue; + } + if (results.resultType === owner.window.XPathResult.STRING_TYPE) { + return results.stringValue; + } + if (results.resultType === owner.window.XPathResult.BOOLEAN_TYPE) { + return results.booleanValue; + } + if ( + results.resultType === owner.window.XPathResult.ANY_UNORDERED_NODE_TYPE || + results.resultType === owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE + ) { + return results.singleNodeValue; + } + if ( + results.resultType === + owner.window.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE || + results.resultType === owner.window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE + ) { + for (let i = 0; i < results.snapshotLength; i++) { + nodes.push(results.snapshotItem(i)); + } + return nodes; + } + + let node; + while ((node = results.iterateNext())) { + nodes.push(node); + } + + return nodes; + } +); + +/** + * Returns the currently selected object in the highlighter. + * + * @return Object representing the current selection in the + * Inspector, or null if no selection exists. + */ +WebConsoleCommandsManager.register("$0", { + get(owner) { + return owner.makeDebuggeeValue(owner.selectedNode); + }, +}); + +/** + * Clears the output of the WebConsole. + */ +WebConsoleCommandsManager.register("clear", function (owner) { + owner.helperResult = { + type: "clearOutput", + }; +}); + +/** + * Clears the input history of the WebConsole. + */ +WebConsoleCommandsManager.register("clearHistory", function (owner) { + owner.helperResult = { + type: "clearHistory", + }; +}); + +/** + * Returns the result of Object.keys(object). + * + * @param object object + * Object to return the property names from. + * @return array of strings + */ +WebConsoleCommandsManager.register("keys", function (owner, object) { + // Need to waive Xrays so we can iterate functions and accessor properties + return Cu.cloneInto(Object.keys(Cu.waiveXrays(object)), owner.window); +}); + +/** + * Returns the values of all properties on object. + * + * @param object object + * Object to display the values from. + * @return array of string + */ +WebConsoleCommandsManager.register("values", function (owner, object) { + const values = []; + // Need to waive Xrays so we can iterate functions and accessor properties + const waived = Cu.waiveXrays(object); + const names = Object.getOwnPropertyNames(waived); + + for (const name of names) { + values.push(waived[name]); + } + + return Cu.cloneInto(values, owner.window); +}); + +/** + * Opens a help window in MDN. + */ +WebConsoleCommandsManager.register("help", function (owner) { + owner.helperResult = { type: "help" }; +}); + +/** + * Inspects the passed object. This is done by opening the PropertyPanel. + * + * @param object object + * Object to inspect. + */ +WebConsoleCommandsManager.register( + "inspect", + function (owner, object, forceExpandInConsole = false) { + const dbgObj = owner.preprocessDebuggerObject( + owner.makeDebuggeeValue(object) + ); + + const grip = owner.createValueGrip(dbgObj); + owner.helperResult = { + type: "inspectObject", + input: owner.evalInput, + object: grip, + forceExpandInConsole, + }; + } +); + +/** + * Copy the String representation of a value to the clipboard. + * + * @param any value + * A value you want to copy as a string. + * @return void + */ +WebConsoleCommandsManager.register("copy", function (owner, value) { + let payload; + try { + if (Element.isInstance(value)) { + payload = value.outerHTML; + } else if (typeof value == "string") { + payload = value; + } else { + payload = JSON.stringify(value, null, " "); + } + } catch (ex) { + owner.helperResult = { + type: "error", + message: "webconsole.error.commands.copyError", + messageArgs: [ex.toString()], + }; + return; + } + owner.helperResult = { + type: "copyValueToClipboard", + value: payload, + }; +}); + +/** + * Take a screenshot of a page. + * + * @param object args + * The arguments to be passed to the screenshot + * @return void + */ +WebConsoleCommandsManager.register("screenshot", function (owner, args = {}) { + owner.helperResult = { + type: "screenshotOutput", + args, + }; +}); + +/** + * Shows a history of commands and expressions previously executed within the command line. + * + * @param object args + * The arguments to be passed to the history + * @return void + */ +WebConsoleCommandsManager.register("history", function (owner, args = {}) { + owner.helperResult = { + type: "historyOutput", + args, + }; +}); + +/** + * Block specific resource from loading + * + * @param object args + * an object with key "url", i.e. a filter + * + * @return void + */ +WebConsoleCommandsManager.register("block", function (owner, args = {}) { + if (!args.url) { + owner.helperResult = { + type: "error", + message: "webconsole.messages.commands.blockArgMissing", + }; + return; + } + + owner.helperResult = { + type: "blockURL", + args, + }; +}); + +/* + * Unblock a blocked a resource + * + * @param object filter + * an object with key "url", i.e. a filter + * + * @return void + */ +WebConsoleCommandsManager.register("unblock", function (owner, args = {}) { + if (!args.url) { + owner.helperResult = { + type: "error", + message: "webconsole.messages.commands.blockArgMissing", + }; + return; + } + + owner.helperResult = { + type: "unblockURL", + args, + }; +}); |