manager.js (30665B)
1 /* This Smurce Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 loader.lazyRequireGetter( 8 this, 9 ["getCommandAndArgs"], 10 "resource://devtools/server/actors/webconsole/commands/parser.js", 11 true 12 ); 13 14 loader.lazyGetter(this, "l10n", () => { 15 return new Localization( 16 [ 17 "devtools/shared/webconsole-commands.ftl", 18 "devtools/server/actors/webconsole/commands/experimental-commands.ftl", 19 ], 20 true 21 ); 22 }); 23 24 const lazy = {}; 25 ChromeUtils.defineESModuleGetters( 26 lazy, 27 { 28 JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs", 29 }, 30 { global: "contextual" } 31 ); 32 33 const USAGE_STRING_MAPPING = { 34 block: "webconsole-commands-usage-block", 35 trace: "webconsole-commands-usage-trace3", 36 unblock: "webconsole-commands-usage-unblock", 37 }; 38 39 /** 40 * WebConsole commands manager. 41 * 42 * Defines a set of functions / variables ("commands") that are available from 43 * the Web Console but not from the web page. 44 * 45 */ 46 const WebConsoleCommandsManager = { 47 // Flag used by eager evaluation in order to allow the execution of commands 48 // which are side effect free and disallow all the others. 49 SIDE_EFFECT_FREE: Symbol("SIDE_EFFECT_FREE"), 50 51 // Map of command name to command function or property descriptor (see register method) 52 _registeredCommands: new Map(), 53 // Map of command name to optional array of accepted argument names 54 _validArguments: new Map(), 55 // Set of command names that are side effect free 56 _sideEffectFreeCommands: new Set(), 57 58 /** 59 * Register a new command. 60 * 61 * @param {object} options 62 * @param {string} options.name 63 * The command name (exemple: "$", "screenshot",...)) 64 * @param {boolean} isSideEffectFree 65 * Tells if the command is free of any side effect to know 66 * if it can run in eager console evaluation. 67 * @param {function|object} options.command 68 * The command to register. 69 * It can be: 70 * - a function for the command like "$()" or ":screenshot" 71 * which triggers some code. 72 * - a property descriptor for getters like "$0", 73 * which only returns a value. 74 * @param {Array<string>} options.validArguments 75 * Optional list of valid arguments. 76 * If passed, we will assert that passed arguments are all valid on execution. 77 * 78 * The command function or the command getter are passed a: 79 * - "owner" object as their first parameter (see the example below). 80 * See _createOwnerObject for definition. 81 * - "args" object with all parameters when this is ran as a ":my-command" command. 82 * See getCommandAndArgs for definition. 83 * 84 * Note that if you want to support `--help` argument, you need to provide a usage string in: 85 * devtools/shared/locales/en-US/webconsole-commands.properties 86 * 87 * @example 88 * 89 * WebConsoleCommandsManager.register("$", function (owner, selector) 90 * { 91 * return owner.window.document.querySelector(selector); 92 * }, 93 * ["my-argument"]); 94 * 95 * WebConsoleCommandsManager.register("$0", { 96 * get: function(owner) { 97 * return owner.makeDebuggeeValue(owner.selectedNode); 98 * } 99 * }); 100 */ 101 register({ name, isSideEffectFree, command, validArguments }) { 102 if ( 103 typeof command != "function" && 104 !(typeof command == "object" && typeof command.get == "function") 105 ) { 106 throw new Error( 107 "Invalid web console command. It can only be a function, or an object with a function as 'get' attribute" 108 ); 109 } 110 if (typeof isSideEffectFree !== "boolean") { 111 throw new Error( 112 "Invalid web console command. 'isSideEffectFree' attribute should be set and be a boolean" 113 ); 114 } 115 this._registeredCommands.set(name, command); 116 if (validArguments) { 117 this._validArguments.set(name, validArguments); 118 } 119 if (isSideEffectFree) { 120 this._sideEffectFreeCommands.add(name); 121 } 122 }, 123 124 /** 125 * Return the name of all registered commands. 126 * 127 * @return {Array} List of all command names. 128 */ 129 getAllCommandNames() { 130 return [...this._registeredCommands.keys()]; 131 }, 132 133 /** 134 * There is two types of "commands" here. 135 * 136 * - Functions or variables exposed in the scope of the evaluated string from the WebConsole input. 137 * Example: $(), $0, copy(), clear(),... 138 * - "True commands", which can also be ran from the WebConsole input with ":" prefix. 139 * Example: this list of commands. 140 * Note that some "true commands" are not exposed as function (see getColonOnlyCommandNames). 141 * 142 * The following list distinguish these "true commands" from the first category. 143 * It especially avoid any JavaScript evaluation when the frontend tries to execute 144 * a string starting with ':' character. 145 */ 146 getAllColonCommandNames() { 147 return ["block", "help", "history", "screenshot", "unblock", "trace"]; 148 }, 149 150 /** 151 * Some commands are not exposed in the scope of the evaluated string, 152 * and can only be used via `:command-name`. 153 */ 154 getColonOnlyCommandNames() { 155 return ["screenshot", "trace"]; 156 }, 157 158 /** 159 * Map of all command objects keyed by command name. 160 * Commands object are the objects passed to register() method. 161 * 162 * @return {Map<string -> command>} 163 */ 164 getAllCommands() { 165 return this._registeredCommands; 166 }, 167 168 /** 169 * Is the command name possibly overriding a symbol which 170 * already exists in the paused frame or the global into which 171 * we are about to execute into? 172 */ 173 _isCommandNameAlreadyInScope(name, frame, dbgGlobal) { 174 if (frame && frame.environment) { 175 return !!frame.environment.find(name); 176 } 177 178 // Fallback on global scope when Debugger.Frame doesn't come along an 179 // Environment, or is not a frame. 180 181 try { 182 // This can throw in Browser Toolbox tests 183 const globalEnv = dbgGlobal.asEnvironment(); 184 if (globalEnv) { 185 return !!dbgGlobal.asEnvironment().find(name); 186 } 187 } catch {} 188 189 return !!dbgGlobal.getOwnPropertyDescriptor(name); 190 }, 191 192 _createOwnerObject( 193 consoleActor, 194 debuggerGlobal, 195 evalInput, 196 selectedNodeActorID 197 ) { 198 const owner = { 199 window: consoleActor.evalGlobal, 200 makeDebuggeeValue: debuggerGlobal.makeDebuggeeValue.bind(debuggerGlobal), 201 createValueGrip: consoleActor.createValueGrip.bind(consoleActor), 202 preprocessDebuggerObject: 203 consoleActor.preprocessDebuggerObject.bind(consoleActor), 204 helperResult: null, 205 consoleActor, 206 evalInput, 207 }; 208 if (selectedNodeActorID) { 209 const actor = consoleActor.conn.getActor(selectedNodeActorID); 210 if (actor) { 211 owner.selectedNode = actor.rawNode; 212 } 213 } 214 return owner; 215 }, 216 217 _getCommandsForCurrentEnvironment() { 218 // Not supporting extra commands in workers yet. This should be possible to 219 // add one by one as long as they don't require jsm/mjs, Cu, etc. 220 return isWorker ? new Map() : this.getAllCommands(); 221 }, 222 223 /** 224 * Create an object with the API we expose to the Web Console during 225 * JavaScript evaluation. 226 * This object inherits properties and methods from the Web Console actor. 227 * 228 * @param object consoleActor 229 * The related web console actor evaluating some code. 230 * @param object debuggerGlobal 231 * A Debugger.Object that wraps a content global. This is used for the 232 * Web Console Commands. 233 * @param object frame (optional) 234 * The frame where the string was evaluated. 235 * @param string evalInput 236 * String to evaluate. 237 * @param string selectedNodeActorID 238 * The Node actor ID of the currently selected DOM Element, if any is selected. 239 * @param bool preferConsoleCommandsOverLocalSymbols 240 * If true, define all bindings even if there's conflicting existing 241 * symbols. This is for the case evaluating non-user code in frame 242 * environment. 243 * 244 * @return object 245 * Object with two properties: 246 * - 'bindings', the object with all commands set as attribute on this object. 247 * - 'getHelperResult', a live getter returning the additional data the last command 248 * which executed want to convey to the frontend. 249 * (The return value of commands isn't returned to the client but it only 250 * returned to the code ran from console evaluation) 251 */ 252 getWebConsoleCommands( 253 consoleActor, 254 debuggerGlobal, 255 frame, 256 evalInput, 257 selectedNodeActorID, 258 preferConsoleCommandsOverLocalSymbols 259 ) { 260 const bindings = Object.create(null); 261 262 const owner = this._createOwnerObject( 263 consoleActor, 264 debuggerGlobal, 265 evalInput, 266 selectedNodeActorID 267 ); 268 269 const evalGlobal = consoleActor.evalGlobal; 270 function maybeExport(obj, name) { 271 if (typeof obj[name] != "function") { 272 return; 273 } 274 275 // By default, chrome-implemented functions that are exposed to content 276 // refuse to accept arguments that are cross-origin for the caller. This 277 // is generally the safe thing, but causes problems for certain console 278 // helpers like cd(), where we users sometimes want to pass a cross-origin 279 // window. To circumvent this restriction, we use exportFunction along 280 // with a special option designed for this purpose. See bug 1051224. 281 obj[name] = Cu.exportFunction(obj[name], evalGlobal, { 282 allowCrossOriginArguments: true, 283 }); 284 } 285 286 const commands = this._getCommandsForCurrentEnvironment(); 287 288 const colonOnlyCommandNames = this.getColonOnlyCommandNames(); 289 for (const [name, command] of commands) { 290 // When we run user code in frame, we want to avoid overriding existing 291 // symbols with commands. 292 // 293 // When we run user code in global scope, all bindings are automatically 294 // shadowed, except for "help" function which is checked by getEvalInput. 295 // 296 // When preferConsoleCommandsOverLocalSymbols is true, ignore symbols in 297 // the current scope and always use commands ones. 298 if ( 299 !preferConsoleCommandsOverLocalSymbols && 300 (frame || name === "help") && 301 this._isCommandNameAlreadyInScope(name, frame, debuggerGlobal) 302 ) { 303 continue; 304 } 305 // Also ignore commands which can only be run with the `:` prefix. 306 if (colonOnlyCommandNames.includes(name)) { 307 continue; 308 } 309 310 const descriptor = { 311 // We force the enumerability and the configurability (so the 312 // WebConsoleActor can reconfigure the property). 313 enumerable: true, 314 configurable: true, 315 }; 316 317 if (typeof command === "function") { 318 // Function commands 319 descriptor.value = command.bind(undefined, owner); 320 maybeExport(descriptor, "value"); 321 322 // Unfortunately evalWithBindings will access all bindings values, 323 // which would trigger a debuggee native call because bindings's property 324 // is using Cu.exportFunction. 325 // Put a magic symbol attribute on them in order to carefully accept 326 // all bindings as being side effect safe by default. 327 if (this._sideEffectFreeCommands.has(name)) { 328 descriptor.value.isSideEffectFree = this.SIDE_EFFECT_FREE; 329 } 330 331 // Make sure the helpers can be used during eval. 332 descriptor.value = debuggerGlobal.makeDebuggeeValue(descriptor.value); 333 } else if (typeof command?.get === "function") { 334 // Getter commands 335 descriptor.get = command.get.bind(undefined, owner); 336 maybeExport(descriptor, "get"); 337 338 // See comment in previous block. 339 if (this._sideEffectFreeCommands.has(name)) { 340 descriptor.get.isSideEffectFree = this.SIDE_EFFECT_FREE; 341 } 342 } 343 Object.defineProperty(bindings, name, descriptor); 344 } 345 346 return { 347 // Use a method as commands will update owner.helperResult later 348 getHelperResult() { 349 return owner.helperResult; 350 }, 351 bindings, 352 }; 353 }, 354 355 /** 356 * Create a function for given ':command'-style command. 357 * 358 * @param object consoleActor 359 * The related web console actor evaluating some code. 360 * @param object debuggerGlobal 361 * A Debugger.Object that wraps a content global. This is used for the 362 * Web Console Commands. 363 * @param string selectedNodeActorID 364 * The Node actor ID of the currently selected DOM Element, if any is selected. 365 * @param string evalInput 366 * String to evaluate. 367 * 368 * @return object 369 * Object with two properties: 370 * - 'commandFunc', a function corresponds to the 'commandName' 371 * - 'getHelperResult', a live getter returning the data the command 372 * which executed want to convey to the frontend. 373 */ 374 executeCommand(consoleActor, debuggerGlobal, selectedNodeActorID, evalInput) { 375 const { command, args } = getCommandAndArgs(evalInput); 376 const commands = this._getCommandsForCurrentEnvironment(); 377 if (!commands.has(command)) { 378 throw new Error(`Unsupported command '${command}'`); 379 } 380 381 if (args.help || args.usage) { 382 const l10nKey = USAGE_STRING_MAPPING[command]; 383 if (l10nKey) { 384 const message = l10n.formatValueSync(l10nKey); 385 if (message && message !== l10nKey) { 386 return { 387 result: null, 388 helperResult: { 389 type: "usage", 390 message, 391 }, 392 }; 393 } 394 } 395 } 396 397 const validArguments = this._validArguments.get(command); 398 if (validArguments) { 399 for (const key of Object.keys(args)) { 400 if (!validArguments.includes(key)) { 401 throw new Error( 402 `:${command} command doesn't support '${key}' argument.` 403 ); 404 } 405 } 406 } 407 408 const owner = this._createOwnerObject( 409 consoleActor, 410 debuggerGlobal, 411 evalInput, 412 selectedNodeActorID 413 ); 414 415 const commandFunction = commands.get(command); 416 417 // This is where we run the command passed to register method 418 const result = commandFunction(owner, args); 419 420 return { 421 result, 422 423 // commandFunction may mutate owner.helperResult which is used 424 // to convey additional data to the frontend. 425 helperResult: owner.helperResult, 426 }; 427 }, 428 }; 429 430 exports.WebConsoleCommandsManager = WebConsoleCommandsManager; 431 432 /* 433 * Built-in commands. 434 * 435 * A list of helper functions used by Firebug can be found here: 436 * http://getfirebug.com/wiki/index.php/Command_Line_API 437 */ 438 439 /** 440 * Find the first node matching a CSS selector. 441 * 442 * @param string selector 443 * A string that is passed to window.document.querySelector 444 * @param [optional] Node element 445 * An optional Node to replace window.document 446 * @return Node or null 447 * The result of calling document.querySelectorAll(selector). 448 */ 449 WebConsoleCommandsManager.register({ 450 name: "$", 451 isSideEffectFree: true, 452 command(owner, selector, element) { 453 try { 454 if ( 455 element && 456 element.querySelector && 457 (element.nodeType == Node.ELEMENT_NODE || 458 element.nodeType == Node.DOCUMENT_NODE || 459 element.nodeType == Node.DOCUMENT_FRAGMENT_NODE) 460 ) { 461 return element.querySelector(selector); 462 } 463 return owner.window.document.querySelector(selector); 464 } catch (err) { 465 // Throw an error like `err` but that belongs to `owner.window`. 466 throw new owner.window.DOMException(err.message, err.name); 467 } 468 }, 469 }); 470 471 /** 472 * Find the nodes matching a CSS selector. 473 * 474 * @param string selector 475 * A string that is passed to window.document.querySelectorAll. 476 * @param [optional] Node element 477 * An optional root Node, defaults to window.document 478 * @return array of Node 479 * The result of calling document.querySelector(selector) in an array. 480 */ 481 WebConsoleCommandsManager.register({ 482 name: "$$", 483 isSideEffectFree: true, 484 command(owner, selector, element) { 485 let scope = owner.window.document; 486 try { 487 if ( 488 element && 489 element.querySelectorAll && 490 (element.nodeType == Node.ELEMENT_NODE || 491 element.nodeType == Node.DOCUMENT_NODE || 492 element.nodeType == Node.DOCUMENT_FRAGMENT_NODE) 493 ) { 494 scope = element; 495 } 496 const nodes = scope.querySelectorAll(selector); 497 const result = new owner.window.Array(); 498 // Calling owner.window.Array.from() doesn't work without accessing the 499 // wrappedJSObject, so just loop through the results instead. 500 for (let i = 0; i < nodes.length; i++) { 501 result.push(nodes[i]); 502 } 503 return result; 504 } catch (err) { 505 // Throw an error like `err` but that belongs to `owner.window`. 506 throw new owner.window.DOMException(err.message, err.name); 507 } 508 }, 509 }); 510 511 /** 512 * Find the nodes matching a CSS selector, including those inside shadow DOM 513 * 514 * @param string selector 515 * A string that is passed to all `querySelectorAll` calls performed by this command. 516 * @param [optional] Node element 517 * An optional root Node, defaults to window.document 518 * @return array of Node 519 * An array containing the nodes returned by calling `querySelectorAll(selector)` 520 * on `element` and on all shadow hosts under element (recursively). 521 */ 522 WebConsoleCommandsManager.register({ 523 name: "$$$", 524 isSideEffectFree: true, 525 command(owner, selector, element) { 526 let scope = owner.window.document; 527 try { 528 if ( 529 element?.querySelectorAll && 530 (element.nodeType == Node.ELEMENT_NODE || 531 element.nodeType == Node.DOCUMENT_NODE || 532 element.nodeType == Node.DOCUMENT_FRAGMENT_NODE) 533 ) { 534 scope = element; 535 } 536 537 const result = new owner.window.Array(); 538 539 const collectElements = root => { 540 const nodes = root.querySelectorAll(selector); 541 // Calling owner.window.Array.from() doesn't work without accessing the 542 // wrappedJSObject, so just loop through the results instead. 543 for (let i = 0, len = nodes.length; i < len; i++) { 544 // If we have a native anonymous element, it's seen as a cross-origin object 545 // and can't be added to result. We could waive `result` to avoid this exception, 546 // but those nodes would show up as `Restricted` (See Bug 2006913), so it's not 547 // really useful. If we'd have a proper rendering for those, ideally we'd use 548 // the Inspector Walker filter to see if a node should be skipped or not 549 // and we could add the node here. 550 if (nodes[i].isNativeAnonymous) { 551 continue; 552 } 553 554 result.push(nodes[i]); 555 } 556 557 // If the scope is a host, run the query inside its shadow DOM 558 if (root.openOrClosedShadowRoot) { 559 collectElements(root.openOrClosedShadowRoot); 560 } 561 562 // Finally, run the query for all hosts in scope 563 const all = root.querySelectorAll("*"); 564 for (let i = 0, len = all.length; i < len; i++) { 565 const el = all[i]; 566 if (el.openOrClosedShadowRoot) { 567 collectElements(el.openOrClosedShadowRoot); 568 } 569 } 570 }; 571 572 collectElements(scope); 573 574 return result; 575 } catch (err) { 576 // Throw an error like `err` but that belongs to `owner.window`. 577 throw new owner.window.DOMException(err.message, err.name); 578 } 579 }, 580 }); 581 582 /** 583 * Returns the result of the last console input evaluation 584 * 585 * @return object|undefined 586 * Returns last console evaluation or undefined 587 */ 588 WebConsoleCommandsManager.register({ 589 name: "$_", 590 isSideEffectFree: true, 591 command: { 592 get(owner) { 593 return owner.consoleActor.getLastConsoleInputEvaluation(); 594 }, 595 }, 596 }); 597 598 /** 599 * Runs an xPath query and returns all matched nodes. 600 * 601 * @param string xPath 602 * xPath search query to execute. 603 * @param [optional] Node context 604 * Context to run the xPath query on. Uses window.document if not set. 605 * @param [optional] string|number resultType 606 Specify the result type. Default value XPathResult.ANY_TYPE 607 * @return array of Node 608 */ 609 WebConsoleCommandsManager.register({ 610 name: "$x", 611 isSideEffectFree: true, 612 command( 613 owner, 614 xPath, 615 context, 616 resultType = owner.window.XPathResult.ANY_TYPE 617 ) { 618 const nodes = new owner.window.Array(); 619 // Not waiving Xrays, since we want the original Document.evaluate function, 620 // instead of anything that's been redefined. 621 const doc = owner.window.document; 622 context = context || doc; 623 switch (resultType) { 624 case "number": 625 resultType = owner.window.XPathResult.NUMBER_TYPE; 626 break; 627 628 case "string": 629 resultType = owner.window.XPathResult.STRING_TYPE; 630 break; 631 632 case "bool": 633 resultType = owner.window.XPathResult.BOOLEAN_TYPE; 634 break; 635 636 case "node": 637 resultType = owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE; 638 break; 639 640 case "nodes": 641 resultType = owner.window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE; 642 break; 643 } 644 const results = doc.evaluate(xPath, context, null, resultType, null); 645 if (results.resultType === owner.window.XPathResult.NUMBER_TYPE) { 646 return results.numberValue; 647 } 648 if (results.resultType === owner.window.XPathResult.STRING_TYPE) { 649 return results.stringValue; 650 } 651 if (results.resultType === owner.window.XPathResult.BOOLEAN_TYPE) { 652 return results.booleanValue; 653 } 654 if ( 655 results.resultType === owner.window.XPathResult.ANY_UNORDERED_NODE_TYPE || 656 results.resultType === owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE 657 ) { 658 return results.singleNodeValue; 659 } 660 if ( 661 results.resultType === 662 owner.window.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE || 663 results.resultType === owner.window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE 664 ) { 665 for (let i = 0; i < results.snapshotLength; i++) { 666 nodes.push(results.snapshotItem(i)); 667 } 668 return nodes; 669 } 670 671 let node; 672 while ((node = results.iterateNext())) { 673 nodes.push(node); 674 } 675 676 return nodes; 677 }, 678 }); 679 680 /** 681 * Returns the currently selected object in the highlighter. 682 * 683 * @return Object representing the current selection in the 684 * Inspector, or null if no selection exists. 685 */ 686 WebConsoleCommandsManager.register({ 687 name: "$0", 688 isSideEffectFree: true, 689 command: { 690 get(owner) { 691 return owner.makeDebuggeeValue(owner.selectedNode); 692 }, 693 }, 694 }); 695 696 /** 697 * Clears the output of the WebConsole. 698 */ 699 WebConsoleCommandsManager.register({ 700 name: "clear", 701 isSideEffectFree: false, 702 command(owner) { 703 owner.helperResult = { 704 type: "clearOutput", 705 }; 706 }, 707 }); 708 709 /** 710 * Clears the input history of the WebConsole. 711 */ 712 WebConsoleCommandsManager.register({ 713 name: "clearHistory", 714 isSideEffectFree: false, 715 command(owner) { 716 owner.helperResult = { 717 type: "clearHistory", 718 }; 719 }, 720 }); 721 722 /** 723 * Returns the result of Object.keys(object). 724 * 725 * @param object object 726 * Object to return the property names from. 727 * @return array of strings 728 */ 729 WebConsoleCommandsManager.register({ 730 name: "keys", 731 isSideEffectFree: true, 732 command(owner, object) { 733 // Need to waive Xrays so we can iterate functions and accessor properties 734 return Cu.cloneInto(Object.keys(Cu.waiveXrays(object)), owner.window); 735 }, 736 }); 737 738 /** 739 * Returns the values of all properties on object. 740 * 741 * @param object object 742 * Object to display the values from. 743 * @return array of string 744 */ 745 WebConsoleCommandsManager.register({ 746 name: "values", 747 isSideEffectFree: true, 748 command(owner, object) { 749 const values = []; 750 // Need to waive Xrays so we can iterate functions and accessor properties 751 const waived = Cu.waiveXrays(object); 752 const names = Object.getOwnPropertyNames(waived); 753 754 for (const name of names) { 755 values.push(waived[name]); 756 } 757 758 return Cu.cloneInto(values, owner.window); 759 }, 760 }); 761 762 /** 763 * Opens a help window in MDN. 764 */ 765 WebConsoleCommandsManager.register({ 766 name: "help", 767 isSideEffectFree: false, 768 command(owner) { 769 owner.helperResult = { type: "help" }; 770 }, 771 }); 772 773 /** 774 * Inspects the passed object. This is done by opening the PropertyPanel. 775 * 776 * @param object object 777 * Object to inspect. 778 */ 779 WebConsoleCommandsManager.register({ 780 name: "inspect", 781 isSideEffectFree: false, 782 command(owner, object, forceExpandInConsole = false) { 783 const dbgObj = owner.preprocessDebuggerObject( 784 owner.makeDebuggeeValue(object) 785 ); 786 787 const grip = owner.createValueGrip(dbgObj); 788 owner.helperResult = { 789 type: "inspectObject", 790 input: owner.evalInput, 791 object: grip, 792 forceExpandInConsole, 793 }; 794 }, 795 }); 796 797 /** 798 * Copy the String representation of a value to the clipboard. 799 * 800 * @param any value 801 * A value you want to copy as a string. 802 * @return void 803 */ 804 WebConsoleCommandsManager.register({ 805 name: "copy", 806 isSideEffectFree: false, 807 command(owner, value) { 808 let payload; 809 try { 810 if (Element.isInstance(value)) { 811 payload = value.outerHTML; 812 } else if (typeof value == "string") { 813 payload = value; 814 } else { 815 // Need to waive Xrays so we can iterate accessor properties. 816 // If Cu is not defined, we are running on a worker thread, where xrays don't exist. 817 if (value && Cu) { 818 value = Cu.waiveXrays(value); 819 } 820 payload = JSON.stringify(value, null, " "); 821 } 822 } catch (ex) { 823 owner.helperResult = { 824 type: "error", 825 message: "webconsole.error.commands.copyError", 826 messageArgs: [ex.toString()], 827 }; 828 return; 829 } 830 owner.helperResult = { 831 type: "copyValueToClipboard", 832 value: payload, 833 }; 834 }, 835 }); 836 837 /** 838 * Take a screenshot of a page. 839 * 840 * @param object args 841 * The arguments to be passed to the screenshot 842 * @return void 843 */ 844 WebConsoleCommandsManager.register({ 845 name: "screenshot", 846 isSideEffectFree: false, 847 command(owner, args = {}) { 848 owner.helperResult = { 849 type: "screenshotOutput", 850 args, 851 }; 852 }, 853 }); 854 855 /** 856 * Shows a history of commands and expressions previously executed within the command line. 857 * 858 * @param object args 859 * The arguments to be passed to the history 860 * @return void 861 */ 862 WebConsoleCommandsManager.register({ 863 name: "history", 864 isSideEffectFree: false, 865 command(owner, args = {}) { 866 owner.helperResult = { 867 type: "historyOutput", 868 args, 869 }; 870 }, 871 }); 872 873 /** 874 * Block specific resource from loading 875 * 876 * @param object args 877 * an object with key "url", i.e. a filter 878 * 879 * @return void 880 */ 881 WebConsoleCommandsManager.register({ 882 name: "block", 883 isSideEffectFree: false, 884 command(owner, args = {}) { 885 // Note that this command is implemented in the frontend, from actions's input.js 886 // We only forward the command arguments back to the client. 887 if (!args.url) { 888 owner.helperResult = { 889 type: "error", 890 message: "webconsole.messages.commands.blockArgMissing", 891 }; 892 return; 893 } 894 895 owner.helperResult = { 896 type: "blockURL", 897 args, 898 }; 899 }, 900 validArguments: ["url"], 901 }); 902 903 /** 904 * Unblock a blocked a resource 905 * 906 * @param object filter 907 * an object with key "url", i.e. a filter 908 * 909 * @return void 910 */ 911 WebConsoleCommandsManager.register({ 912 name: "unblock", 913 isSideEffectFree: false, 914 command(owner, args = {}) { 915 // Note that this command is implemented in the frontend, from actions's input.js 916 // We only forward the command arguments back to the client. 917 if (!args.url) { 918 owner.helperResult = { 919 type: "error", 920 message: "webconsole.messages.commands.blockArgMissing", 921 }; 922 return; 923 } 924 925 owner.helperResult = { 926 type: "unblockURL", 927 args, 928 }; 929 }, 930 validArguments: ["url"], 931 }); 932 933 /** 934 * Toggle JavaScript tracing 935 * 936 * @param object args 937 * An object with various configuration only valid when starting the tracing. 938 * 939 * @return void 940 */ 941 WebConsoleCommandsManager.register({ 942 name: "trace", 943 isSideEffectFree: false, 944 command(owner, args) { 945 // Disable :trace command on worker until this feature is enabled by default 946 if (isWorker) { 947 throw new Error(":trace command isn't supported in workers"); 948 } 949 950 if (!owner.consoleActor.targetActor.isTracerFeatureEnabled) { 951 throw new Error( 952 ":trace requires 'devtools.debugger.features.javascript-tracing' preference to be true" 953 ); 954 } 955 const tracerActor = 956 owner.consoleActor.targetActor.getTargetScopedActor("tracer"); 957 const logMethod = args.logMethod || "console"; 958 let traceDOMMutations = null; 959 if ("dom-mutations" in args) { 960 // When no value is passed, track all types of mutations 961 if (args["dom-mutations"] === true) { 962 traceDOMMutations = ["add", "attributes", "remove"]; 963 } else if (typeof args["dom-mutations"] == "string") { 964 // Otherwise consider the value as coma seperated list and remove any white space. 965 traceDOMMutations = args["dom-mutations"].split(",").map(e => e.trim()); 966 const acceptedValues = Object.values(lazy.JSTracer.DOM_MUTATIONS); 967 if (!traceDOMMutations.every(e => acceptedValues.includes(e))) { 968 throw new Error( 969 `:trace --dom-mutations only accept a list of strings whose values can be: ${acceptedValues}` 970 ); 971 } 972 } else { 973 throw new Error( 974 ":trace --dom-mutations accept only no arguments, or a list mutation type strings (add,attributes,remove)" 975 ); 976 } 977 } 978 // Note that toggleTracing does some sanity checks and will throw meaningful error 979 // when the arguments are wrong. 980 const enabled = tracerActor.toggleTracing({ 981 logMethod, 982 prefix: args.prefix || null, 983 traceFunctionReturn: !!args.returns, 984 traceValues: !!args.values, 985 traceOnNextInteraction: args["on-next-interaction"] || null, 986 traceDOMMutations, 987 maxDepth: args["max-depth"] || null, 988 maxRecords: args["max-records"] || null, 989 }); 990 991 owner.helperResult = { 992 type: "traceOutput", 993 enabled, 994 logMethod, 995 }; 996 }, 997 validArguments: [ 998 "logMethod", 999 "max-depth", 1000 "max-records", 1001 "on-next-interaction", 1002 "dom-mutations", 1003 "prefix", 1004 "returns", 1005 "values", 1006 ], 1007 });