/* 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"; const { Cu } = require("chrome"); // Note that this is only used in WebConsoleCommands, see $0 and screenshot. if (!isWorker) { loader.lazyRequireGetter( this, "captureScreenshot", "devtools/server/actors/utils/capture-screenshot", true ); } const CONSOLE_WORKER_IDS = (exports.CONSOLE_WORKER_IDS = [ "SharedWorker", "ServiceWorker", "Worker", ]); var WebConsoleUtils = { /** * Given a message, return one of CONSOLE_WORKER_IDS if it matches * one of those. * * @return string */ getWorkerType: function(message) { const id = message ? message.innerID : null; return CONSOLE_WORKER_IDS[CONSOLE_WORKER_IDS.indexOf(id)] || null; }, /** * Clone an object. * * @param object object * The object you want cloned. * @param boolean recursive * Tells if you want to dig deeper into the object, to clone * recursively. * @param function [filter] * Optional, filter function, called for every property. Three * arguments are passed: key, value and object. Return true if the * property should be added to the cloned object. Return false to skip * the property. * @return object * The cloned object. */ cloneObject: function(object, recursive, filter) { if (typeof object != "object") { return object; } let temp; if (Array.isArray(object)) { temp = []; object.forEach(function(value, index) { if (!filter || filter(index, value, object)) { temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value); } }); } else { temp = {}; for (const key in object) { const value = object[key]; if ( object.hasOwnProperty(key) && (!filter || filter(key, value, object)) ) { temp[key] = recursive ? WebConsoleUtils.cloneObject(value) : value; } } } return temp; }, /** * Gets the ID of the inner window of this DOM window. * * @param nsIDOMWindow window * @return integer|null * Inner ID for the given window, null if we can't access it. */ getInnerWindowId: function(window) { // Might throw with SecurityError: Permission denied to access property // "windowGlobalChild" on cross-origin object. try { return window.windowGlobalChild.innerWindowId; } catch (e) { return null; } }, /** * Recursively gather a list of inner window ids given a * top level window. * * @param nsIDOMWindow window * @return Array * list of inner window ids. */ getInnerWindowIDsForFrames: function(window) { const innerWindowID = this.getInnerWindowId(window); if (innerWindowID === null) { return []; } let ids = [innerWindowID]; if (window.frames) { for (let i = 0; i < window.frames.length; i++) { const frame = window.frames[i]; ids = ids.concat(this.getInnerWindowIDsForFrames(frame)); } } return ids; }, /** * Create a grip for the given value. If the value is an object, * an object wrapper will be created. * * @param mixed value * The value you want to create a grip for, before sending it to the * client. * @param function objectWrapper * If the value is an object then the objectWrapper function is * invoked to give us an object grip. See this.getObjectGrip(). * @return mixed * The value grip. */ createValueGrip: function(value, objectWrapper) { switch (typeof value) { case "boolean": return value; case "string": return objectWrapper(value); case "number": if (value === Infinity) { return { type: "Infinity" }; } else if (value === -Infinity) { return { type: "-Infinity" }; } else if (Number.isNaN(value)) { return { type: "NaN" }; } else if (!value && 1 / value === -Infinity) { return { type: "-0" }; } return value; case "undefined": return { type: "undefined" }; case "object": if (value === null) { return { type: "null" }; } // Fall through. case "function": return objectWrapper(value); default: console.error( "Failed to provide a grip for value of " + typeof value + ": " + value ); return null; } }, /** * Remove any frames in a stack that are above a debugger-triggered evaluation * and will correspond with devtools server code, which we never want to show * to the user. * * @param array stack * An array of frames, with the topmost first, and each of which has a * 'filename' property. * @return array * An array of stack frames with any devtools server frames removed. * The original array is not modified. */ removeFramesAboveDebuggerEval(stack) { const debuggerEvalFilename = "debugger eval code"; // Remove any frames for server code above the last debugger eval frame. const evalIndex = stack.findIndex(({ filename }, idx, arr) => { const nextFrame = arr[idx + 1]; return ( filename == debuggerEvalFilename && (!nextFrame || nextFrame.filename !== debuggerEvalFilename) ); }); if (evalIndex != -1) { return stack.slice(0, evalIndex + 1); } // In some cases (e.g. evaluated expression with SyntaxError), we might not have a // "debugger eval code" frame but still have internal ones. If that's the case, we // return null as the end user shouldn't see those frames. if ( stack.some( ({ filename }) => filename && filename.startsWith("resource://devtools/") ) ) { return null; } return stack; }, }; exports.WebConsoleUtils = WebConsoleUtils; /** * WebConsole commands manager. * * Defines a set of functions /variables ("commands") that are available from * the Web Console but not from the web page. * */ var WebConsoleCommands = { _registeredCommands: new Map(), _originalCommands: new Map(), /** * @private * Reserved for built-in commands. To register a command from the code of an * add-on, see WebConsoleCommands.register instead. * * @see WebConsoleCommands.register */ _registerOriginal: function(name, command) { this.register(name, command); this._originalCommands.set(name, this.getCommand(name)); }, /** * 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 / value (like * "$0"). * * The command function or the command getter are passed a owner object as * their first parameter (see the example below). * * Note that setters don't work currently and "enumerable" and "configurable" * are forced to true. * * @example * * WebConsoleCommands.register("$", function JSTH_$(owner, selector) * { * return owner.window.document.querySelector(selector); * }); * * WebConsoleCommands.register("$0", { * get: function(owner) { * return owner.makeDebuggeeValue(owner.selectedNode); * } * }); */ register: function(name, command) { this._registeredCommands.set(name, command); }, /** * Unregister a command. * * If the command being unregister overrode a built-in command, * the latter is restored. * * @param {string} name The name of the command */ unregister: function(name) { this._registeredCommands.delete(name); if (this._originalCommands.has(name)) { this.register(name, this._originalCommands.get(name)); } }, /** * Returns a command by its name. * * @param {string} name The name of the command. * * @return {(function|object)} The command. */ getCommand: function(name) { return this._registeredCommands.get(name); }, /** * Returns true if a command is registered with the given name. * * @param {string} name The name of the command. * * @return {boolean} True if the command is registered. */ hasCommand: function(name) { return this._registeredCommands.has(name); }, }; exports.WebConsoleCommands = WebConsoleCommands; /* * 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). */ WebConsoleCommands._registerOriginal("$", 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). */ WebConsoleCommands._registerOriginal("$$", 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 */ WebConsoleCommands._registerOriginal("$_", { get: function(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 */ WebConsoleCommands._registerOriginal("$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. */ WebConsoleCommands._registerOriginal("$0", { get: function(owner) { return owner.makeDebuggeeValue(owner.selectedNode); }, }); /** * Clears the output of the WebConsole. */ WebConsoleCommands._registerOriginal("clear", function(owner) { owner.helperResult = { type: "clearOutput", }; }); /** * Clears the input history of the WebConsole. */ WebConsoleCommands._registerOriginal("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 */ WebConsoleCommands._registerOriginal("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 */ WebConsoleCommands._registerOriginal("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. */ WebConsoleCommands._registerOriginal("help", function(owner) { owner.helperResult = { type: "help" }; }); /** * Inspects the passed object. This is done by opening the PropertyPanel. * * @param object object * Object to inspect. */ WebConsoleCommands._registerOriginal("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 */ WebConsoleCommands._registerOriginal("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) { payload = "/* " + ex + " */"; } 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 */ WebConsoleCommands._registerOriginal("screenshot", function(owner, args = {}) { owner.helperResult = (async () => { // creates data for saving the screenshot // help is handled on the client side const value = await captureScreenshot(args, owner.window.document); return { type: "screenshotOutput", value, // pass args through to the client, so that the client can take care of copying // and saving the screenshot data on the client machine instead of on the // remote machine args, }; })(); }); /** * Block specific resource from loading * * @param object args * an object with key "url", i.e. a filter * * @return void */ WebConsoleCommands._registerOriginal("block", function(owner, args = {}) { if (!args.url) { owner.helperResult = { type: "error", message: "webconsole.messages.commands.blockArgMissing", }; return; } owner.helperResult = (async () => { await owner.consoleActor.blockRequest(args); return { type: "blockURL", args, }; })(); }); /* * Unblock a blocked a resource * * @param object filter * an object with key "url", i.e. a filter * * @return void */ WebConsoleCommands._registerOriginal("unblock", function(owner, args = {}) { if (!args.url) { owner.helperResult = { type: "error", message: "webconsole.messages.commands.blockArgMissing", }; return; } owner.helperResult = (async () => { await owner.consoleActor.unblockRequest(args); return { type: "unblockURL", args, }; })(); }); /** * (Internal only) Add the bindings to |owner.sandbox|. * This is intended to be used by the WebConsole actor only. * * @param object owner * The owning object. */ function addWebConsoleCommands(owner) { // 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 ? [] : WebConsoleCommands._registeredCommands; if (!owner) { throw new Error("The owner is required"); } for (const [name, command] of commands) { if (typeof command === "function") { owner.sandbox[name] = command.bind(undefined, owner); } else if (typeof command === "object") { const clone = Object.assign({}, command, { // We force the enumerability and the configurability (so the // WebConsoleActor can reconfigure the property). enumerable: true, configurable: true, }); if (typeof command.get === "function") { clone.get = command.get.bind(undefined, owner); } if (typeof command.set === "function") { clone.set = command.set.bind(undefined, owner); } Object.defineProperty(owner.sandbox, name, clone); } } } exports.addWebConsoleCommands = addWebConsoleCommands;