webconsole.js (53865B)
1 /* This Source 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 /* global clearConsoleEvents */ 6 7 "use strict"; 8 9 const { Actor } = require("resource://devtools/shared/protocol.js"); 10 const { 11 webconsoleSpec, 12 } = require("resource://devtools/shared/specs/webconsole.js"); 13 14 const { ThreadActor } = require("resource://devtools/server/actors/thread.js"); 15 const { 16 LongStringActor, 17 } = require("resource://devtools/server/actors/string.js"); 18 const { 19 createValueGrip, 20 isArray, 21 stringIsLong, 22 } = require("resource://devtools/server/actors/object/utils.js"); 23 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 24 const ErrorDocs = require("resource://devtools/server/actors/errordocs.js"); 25 const Targets = require("resource://devtools/server/actors/targets/index.js"); 26 27 loader.lazyRequireGetter( 28 this, 29 "evalWithDebugger", 30 "resource://devtools/server/actors/webconsole/eval-with-debugger.js", 31 true 32 ); 33 loader.lazyRequireGetter( 34 this, 35 "ConsoleFileActivityListener", 36 "resource://devtools/server/actors/webconsole/listeners/console-file-activity.js", 37 true 38 ); 39 loader.lazyRequireGetter( 40 this, 41 "jsPropertyProvider", 42 "resource://devtools/shared/webconsole/js-property-provider.js", 43 true 44 ); 45 loader.lazyRequireGetter( 46 this, 47 ["isCommand"], 48 "resource://devtools/server/actors/webconsole/commands/parser.js", 49 true 50 ); 51 loader.lazyRequireGetter( 52 this, 53 ["CONSOLE_WORKER_IDS", "WebConsoleUtils"], 54 "resource://devtools/server/actors/webconsole/utils.js", 55 true 56 ); 57 loader.lazyRequireGetter( 58 this, 59 ["WebConsoleCommandsManager"], 60 "resource://devtools/server/actors/webconsole/commands/manager.js", 61 true 62 ); 63 loader.lazyRequireGetter( 64 this, 65 "EventEmitter", 66 "resource://devtools/shared/event-emitter.js" 67 ); 68 loader.lazyRequireGetter( 69 this, 70 "MESSAGE_CATEGORY", 71 "resource://devtools/shared/constants.js", 72 true 73 ); 74 75 // Generated by /devtools/shared/webconsole/GenerateReservedWordsJS.py 76 loader.lazyRequireGetter( 77 this, 78 "RESERVED_JS_KEYWORDS", 79 "resource://devtools/shared/webconsole/reserved-js-words.js" 80 ); 81 82 // Overwrite implemented listeners for workers so that we don't attempt 83 // to load an unsupported module. 84 if (isWorker) { 85 loader.lazyRequireGetter( 86 this, 87 ["ConsoleAPIListener", "ConsoleServiceListener"], 88 "resource://devtools/server/actors/webconsole/worker-listeners.js", 89 true 90 ); 91 } else { 92 loader.lazyRequireGetter( 93 this, 94 "ConsoleAPIListener", 95 "resource://devtools/server/actors/webconsole/listeners/console-api.js", 96 true 97 ); 98 loader.lazyRequireGetter( 99 this, 100 "ConsoleServiceListener", 101 "resource://devtools/server/actors/webconsole/listeners/console-service.js", 102 true 103 ); 104 loader.lazyRequireGetter( 105 this, 106 "ConsoleReflowListener", 107 "resource://devtools/server/actors/webconsole/listeners/console-reflow.js", 108 true 109 ); 110 loader.lazyRequireGetter( 111 this, 112 "DocumentEventsListener", 113 "resource://devtools/server/actors/webconsole/listeners/document-events.js", 114 true 115 ); 116 } 117 loader.lazyRequireGetter( 118 this, 119 "ObjectUtils", 120 "resource://devtools/server/actors/object/utils.js" 121 ); 122 123 function isObject(value) { 124 return Object(value) === value; 125 } 126 127 /** 128 * The WebConsoleActor implements capabilities needed for the Web Console 129 * feature. 130 * 131 * @class 132 * @param object connection 133 * The connection to the client, DevToolsServerConnection. 134 * @param object [targetActor] 135 * Optional, the parent actor. 136 */ 137 class WebConsoleActor extends Actor { 138 constructor(connection, targetActor) { 139 super(connection, webconsoleSpec); 140 141 this.targetActor = targetActor; 142 143 this.dbg = this.targetActor.dbg; 144 145 this._gripDepth = 0; 146 this._evalCounter = 0; 147 this._listeners = new Set(); 148 this._lastConsoleInputEvaluation = undefined; 149 150 this._onWillNavigate = this._onWillNavigate.bind(this); 151 this._onChangedToplevelDocument = 152 this._onChangedToplevelDocument.bind(this); 153 this.onConsoleServiceMessage = this.onConsoleServiceMessage.bind(this); 154 this.onConsoleAPICall = this.onConsoleAPICall.bind(this); 155 this.onDocumentEvent = this.onDocumentEvent.bind(this); 156 157 this.targetActor.on( 158 "changed-toplevel-document", 159 this._onChangedToplevelDocument 160 ); 161 } 162 163 /** 164 * Debugger instance. 165 * 166 * @see jsdebugger.sys.mjs 167 */ 168 dbg = null; 169 170 /** 171 * This is used by the ObjectActor to keep track of the depth of grip() calls. 172 * 173 * @private 174 * @type number 175 */ 176 _gripDepth = null; 177 178 /** 179 * Holds a set of all currently registered listeners. 180 * 181 * @private 182 * @type Set 183 */ 184 _listeners = null; 185 186 /** 187 * The global we work with (this can be a Window, a Worker global or even a Sandbox 188 * for processes and addons). 189 * 190 * @type nsIDOMWindow, WorkerGlobalScope or Sandbox 191 */ 192 get global() { 193 if (this.targetActor.isRootActor) { 194 return this._getWindowForBrowserConsole(); 195 } 196 return this.targetActor.targetGlobal; 197 } 198 199 /** 200 * Get a window to use for the browser console. 201 * 202 * (note that is is also used for browser toolbox and webextension 203 * i.e. all targets flagged with isRootActor=true) 204 * 205 * @private 206 * @return nsIDOMWindow 207 * The window to use, or null if no window could be found. 208 */ 209 _getWindowForBrowserConsole() { 210 // Check if our last used chrome window is still live. 211 let window = this._lastChromeWindow && this._lastChromeWindow.get(); 212 // If not, look for a new one. 213 // In case of WebExtension reload of the background page, the last 214 // chrome window might be a dead wrapper, from which we can't check for window.closed. 215 if (!window || Cu.isDeadWrapper(window) || window.closed) { 216 window = this.targetActor.window; 217 if (!window) { 218 // Try to find the Browser Console window to use instead. 219 window = Services.wm.getMostRecentWindow("devtools:webconsole"); 220 // We prefer the normal chrome window over the console window, 221 // so we'll look for those windows in order to replace our reference. 222 const onChromeWindowOpened = () => { 223 // We'll look for this window when someone next requests window() 224 Services.obs.removeObserver(onChromeWindowOpened, "domwindowopened"); 225 this._lastChromeWindow = null; 226 }; 227 Services.obs.addObserver(onChromeWindowOpened, "domwindowopened"); 228 } 229 230 this._handleNewWindow(window); 231 } 232 233 return window; 234 } 235 236 /** 237 * Store a newly found window on the actor to be used in the future. 238 * 239 * @private 240 * @param nsIDOMWindow window 241 * The window to store on the actor (can be null). 242 */ 243 _handleNewWindow(window) { 244 if (window) { 245 if (this._hadChromeWindow) { 246 Services.console.logStringMessage("Webconsole context has changed"); 247 } 248 this._lastChromeWindow = Cu.getWeakReference(window); 249 this._hadChromeWindow = true; 250 } else { 251 this._lastChromeWindow = null; 252 } 253 } 254 255 /** 256 * Whether we've been using a window before. 257 * 258 * @private 259 * @type boolean 260 */ 261 _hadChromeWindow = false; 262 263 /** 264 * A weak reference to the last chrome window we used to work with. 265 * 266 * @private 267 * @type nsIWeakReference 268 */ 269 _lastChromeWindow = null; 270 271 // The evalGlobal is used at the scope for JS evaluation. 272 _evalGlobal = null; 273 get evalGlobal() { 274 return this._evalGlobal || this.global; 275 } 276 277 set evalGlobal(global) { 278 this._evalGlobal = global; 279 280 if (!this._progressListenerActive) { 281 this.targetActor.on("will-navigate", this._onWillNavigate); 282 this._progressListenerActive = true; 283 } 284 } 285 286 /** 287 * Flag used to track if we are listening for events from the progress 288 * listener of the target actor. We use the progress listener to clear 289 * this.evalGlobal on page navigation. 290 * 291 * @private 292 * @type boolean 293 */ 294 _progressListenerActive = false; 295 296 /** 297 * The ConsoleServiceListener instance. 298 * 299 * @type object 300 */ 301 consoleServiceListener = null; 302 303 /** 304 * The ConsoleAPIListener instance. 305 */ 306 consoleAPIListener = null; 307 308 /** 309 * The ConsoleFileActivityListener instance. 310 */ 311 consoleFileActivityListener = null; 312 313 /** 314 * The ConsoleReflowListener instance. 315 */ 316 consoleReflowListener = null; 317 318 grip() { 319 return { actor: this.actorID }; 320 } 321 322 _findProtoChain = ThreadActor.prototype._findProtoChain; 323 _removeFromProtoChain = ThreadActor.prototype._removeFromProtoChain; 324 325 /** 326 * Destroy the current WebConsoleActor instance. 327 */ 328 destroy() { 329 this.stopListeners(); 330 super.destroy(); 331 332 this.targetActor.off( 333 "changed-toplevel-document", 334 this._onChangedToplevelDocument 335 ); 336 337 this._lastConsoleInputEvaluation = null; 338 this._evalGlobal = null; 339 this.dbg = null; 340 } 341 342 /** 343 * Create a grip for the given value. 344 * 345 * @param mixed value 346 * @param object objectActorAttributes 347 * See createValueGrip in devtools/server/actors/object/utils.js 348 * @return object 349 */ 350 createValueGrip(value, objectActorAttributes = {}) { 351 return createValueGrip( 352 this.targetActor.threadActor, 353 value, 354 this.targetActor.objectsPool, 355 0, 356 objectActorAttributes 357 ); 358 } 359 360 /** 361 * Make a debuggee value for the given value. 362 * 363 * @param mixed value 364 * The value you want to get a debuggee value for. 365 * @param boolean useObjectGlobal 366 * If |true| the object global is determined and added as a debuggee, 367 * otherwise |this.global| is used when makeDebuggeeValue() is invoked. 368 * @return object 369 * Debuggee value for |value|. 370 */ 371 makeDebuggeeValue(value, useObjectGlobal) { 372 if (useObjectGlobal && isObject(value)) { 373 try { 374 const global = Cu.getGlobalForObject(value); 375 const dbgGlobal = this.dbg.makeGlobalObjectReference(global); 376 return dbgGlobal.makeDebuggeeValue(value); 377 } catch (ex) { 378 // The above can throw an exception if value is not an actual object 379 // or 'Object in compartment marked as invisible to Debugger' 380 } 381 } 382 const dbgGlobal = this.dbg.makeGlobalObjectReference(this.global); 383 return dbgGlobal.makeDebuggeeValue(value); 384 } 385 386 /** 387 * Create a grip for the given string. 388 * 389 * @param string string 390 * The string you want to create the grip for. 391 * @param object pool 392 * A Pool where the new actor instance is added. 393 * @return object 394 * A LongStringActor object that wraps the given string. 395 */ 396 longStringGrip(string, pool) { 397 const actor = new LongStringActor(this.conn, string); 398 pool.manage(actor); 399 return actor.form(); 400 } 401 402 /** 403 * Create a long string grip if needed for the given string. 404 * 405 * @private 406 * @param string string 407 * The string you want to create a long string grip for. 408 * @return string|object 409 * A string is returned if |string| is not a long string. 410 * A LongStringActor grip is returned if |string| is a long string. 411 */ 412 _createStringGrip(string) { 413 if (string && stringIsLong(string)) { 414 return this.longStringGrip(string, this); 415 } 416 return string; 417 } 418 419 /** 420 * Returns the latest web console input evaluation. 421 * This is undefined if no evaluations have been completed. 422 * 423 * @return object 424 */ 425 getLastConsoleInputEvaluation() { 426 return this._lastConsoleInputEvaluation; 427 } 428 429 /** 430 * Preprocess a debugger object (e.g. return the `boundTargetFunction` 431 * debugger object if the given debugger object is a bound function). 432 * 433 * This method is called by both the `inspect` binding implemented 434 * for the webconsole and the one implemented for the devtools API 435 * `browser.devtools.inspectedWindow.eval`. 436 */ 437 preprocessDebuggerObject(dbgObj) { 438 // Returns the bound target function on a bound function. 439 if (dbgObj?.isBoundFunction && dbgObj?.boundTargetFunction) { 440 return dbgObj.boundTargetFunction; 441 } 442 443 return dbgObj; 444 } 445 446 /** 447 * This helper is used by the WebExtensionInspectedWindowActor to 448 * inspect an object in the developer toolbox. 449 * 450 * NOTE: shared parts related to preprocess the debugger object (between 451 * this function and the `inspect` webconsole command defined in 452 * "devtools/server/actor/webconsole/utils.js") should be added to 453 * the webconsole actors' `preprocessDebuggerObject` method. 454 */ 455 inspectObject(dbgObj, inspectFromAnnotation) { 456 dbgObj = this.preprocessDebuggerObject(dbgObj); 457 this.emit("inspectObject", { 458 objectActor: this.createValueGrip(dbgObj), 459 inspectFromAnnotation, 460 }); 461 } 462 463 // Request handlers for known packet types. 464 465 /** 466 * Handler for the "startListeners" request. 467 * 468 * @param array listeners 469 * An array of events to start sent by the Web Console client. 470 * @return object 471 * The response object which holds the startedListeners array. 472 */ 473 // eslint-disable-next-line complexity 474 async startListeners(listeners) { 475 const startedListeners = []; 476 const global = !this.targetActor.isRootActor ? this.global : null; 477 const isTargetActorContentProcess = 478 this.targetActor.targetType === Targets.TYPES.PROCESS; 479 480 for (const event of listeners) { 481 switch (event) { 482 case "PageError": 483 // Workers don't support this message type yet 484 if (isWorker) { 485 break; 486 } 487 if (!this.consoleServiceListener) { 488 this.consoleServiceListener = new ConsoleServiceListener( 489 global, 490 this.onConsoleServiceMessage, 491 { 492 matchExactWindow: this.targetActor.ignoreSubFrames, 493 } 494 ); 495 this.consoleServiceListener.init(); 496 } 497 startedListeners.push(event); 498 break; 499 case "ConsoleAPI": 500 if (!this.consoleAPIListener) { 501 // Create the consoleAPIListener 502 // (and apply the filtering options defined in the parent actor). 503 this.consoleAPIListener = new ConsoleAPIListener( 504 global, 505 this.onConsoleAPICall, 506 { 507 matchExactWindow: this.targetActor.ignoreSubFrames, 508 } 509 ); 510 this.consoleAPIListener.init(); 511 } 512 startedListeners.push(event); 513 break; 514 case "NetworkActivity": { 515 // Workers don't support this message type 516 if (isWorker) { 517 break; 518 } 519 // Bug 1807650 removed this in favor of the new Watcher/Resources APIs 520 const errorMessage = 521 "NetworkActivity is no longer supported. " + 522 "Instead use Watcher actor's watchResources and listen to NETWORK_EVENT resource"; 523 dump(errorMessage + "\n"); 524 throw new Error(errorMessage); 525 } 526 case "FileActivity": 527 // Workers don't support this message type 528 if (isWorker) { 529 break; 530 } 531 if (this.global instanceof Ci.nsIDOMWindow) { 532 if (!this.consoleFileActivityListener) { 533 this.consoleFileActivityListener = 534 new ConsoleFileActivityListener(this.global, this); 535 } 536 this.consoleFileActivityListener.startMonitor(); 537 startedListeners.push(event); 538 } 539 break; 540 case "ReflowActivity": 541 // Workers don't support this message type 542 if (isWorker) { 543 break; 544 } 545 if (!this.consoleReflowListener) { 546 this.consoleReflowListener = new ConsoleReflowListener( 547 this.global, 548 this 549 ); 550 } 551 startedListeners.push(event); 552 break; 553 case "DocumentEvents": 554 // Workers don't support this message type 555 if (isWorker || isTargetActorContentProcess) { 556 break; 557 } 558 if (!this.documentEventsListener) { 559 this.documentEventsListener = new DocumentEventsListener( 560 this.targetActor 561 ); 562 563 this.documentEventsListener.on("dom-loading", data => 564 this.onDocumentEvent("dom-loading", data) 565 ); 566 this.documentEventsListener.on("dom-interactive", data => 567 this.onDocumentEvent("dom-interactive", data) 568 ); 569 this.documentEventsListener.on("dom-complete", data => 570 this.onDocumentEvent("dom-complete", data) 571 ); 572 573 this.documentEventsListener.listen(); 574 } 575 startedListeners.push(event); 576 break; 577 } 578 } 579 580 // Update the live list of running listeners 581 startedListeners.forEach(this._listeners.add, this._listeners); 582 583 return { 584 startedListeners, 585 }; 586 } 587 588 /** 589 * Handler for the "stopListeners" request. 590 * 591 * @param array listeners 592 * An array of events to stop sent by the Web Console client. 593 * @return object 594 * The response packet to send to the client: holds the 595 * stoppedListeners array. 596 */ 597 stopListeners(listeners) { 598 const stoppedListeners = []; 599 600 // If no specific listeners are requested to be detached, we stop all 601 // listeners. 602 const eventsToDetach = listeners || [ 603 "PageError", 604 "ConsoleAPI", 605 "FileActivity", 606 "ReflowActivity", 607 "DocumentEvents", 608 ]; 609 610 for (const event of eventsToDetach) { 611 switch (event) { 612 case "PageError": 613 if (this.consoleServiceListener) { 614 this.consoleServiceListener.destroy(); 615 this.consoleServiceListener = null; 616 } 617 stoppedListeners.push(event); 618 break; 619 case "ConsoleAPI": 620 if (this.consoleAPIListener) { 621 this.consoleAPIListener.destroy(); 622 this.consoleAPIListener = null; 623 } 624 stoppedListeners.push(event); 625 break; 626 case "FileActivity": 627 if (this.consoleFileActivityListener) { 628 this.consoleFileActivityListener.stopMonitor(); 629 this.consoleFileActivityListener = null; 630 } 631 stoppedListeners.push(event); 632 break; 633 case "ReflowActivity": 634 if (this.consoleReflowListener) { 635 this.consoleReflowListener.destroy(); 636 this.consoleReflowListener = null; 637 } 638 stoppedListeners.push(event); 639 break; 640 case "DocumentEvents": 641 if (this.documentEventsListener) { 642 this.documentEventsListener.destroy(); 643 this.documentEventsListener = null; 644 } 645 stoppedListeners.push(event); 646 break; 647 } 648 } 649 650 // Update the live list of running listeners 651 stoppedListeners.forEach(this._listeners.delete, this._listeners); 652 653 return { stoppedListeners }; 654 } 655 656 /** 657 * Handler for the "getCachedMessages" request. This method sends the cached 658 * error messages and the window.console API calls to the client. 659 * 660 * @param array messageTypes 661 * An array of message types sent by the Web Console client. 662 * @return object 663 * The response packet to send to the client: it holds the cached 664 * messages array. 665 */ 666 getCachedMessages(messageTypes) { 667 if (!messageTypes) { 668 return { 669 error: "missingParameter", 670 message: "The messageTypes parameter is missing.", 671 }; 672 } 673 674 const messages = []; 675 676 const consoleServiceCachedMessages = 677 messageTypes.includes("PageError") || messageTypes.includes("LogMessage") 678 ? this.consoleServiceListener?.getCachedMessages( 679 !this.targetActor.isRootActor 680 ) 681 : null; 682 683 for (const type of messageTypes) { 684 switch (type) { 685 case "ConsoleAPI": { 686 if (!this.consoleAPIListener) { 687 break; 688 } 689 690 // this.global might not be a window (can be a worker global or a Sandbox), 691 // and in such case performance isn't defined 692 const winStartTime = 693 this.global?.performance?.timing?.navigationStart; 694 695 const cache = this.consoleAPIListener.getCachedMessages( 696 !this.targetActor.isRootActor 697 ); 698 cache.forEach(cachedMessage => { 699 // Filter out messages that came from a ServiceWorker but happened 700 // before the page was requested. 701 if ( 702 cachedMessage.innerID === "ServiceWorker" && 703 winStartTime > cachedMessage.timeStamp 704 ) { 705 return; 706 } 707 708 messages.push({ 709 message: this.prepareConsoleMessageForRemote(cachedMessage), 710 type: "consoleAPICall", 711 }); 712 }); 713 break; 714 } 715 716 case "PageError": { 717 if (!consoleServiceCachedMessages) { 718 break; 719 } 720 721 for (const cachedMessage of consoleServiceCachedMessages) { 722 if (!(cachedMessage instanceof Ci.nsIScriptError)) { 723 continue; 724 } 725 726 messages.push({ 727 pageError: this.preparePageErrorForRemote(cachedMessage), 728 type: "pageError", 729 }); 730 } 731 break; 732 } 733 734 case "LogMessage": { 735 if (!consoleServiceCachedMessages) { 736 break; 737 } 738 739 for (const cachedMessage of consoleServiceCachedMessages) { 740 if (cachedMessage instanceof Ci.nsIScriptError) { 741 continue; 742 } 743 744 messages.push({ 745 message: this._createStringGrip(cachedMessage.message), 746 timeStamp: cachedMessage.microSecondTimeStamp / 1000, 747 type: "logMessage", 748 }); 749 } 750 break; 751 } 752 } 753 } 754 755 return { 756 messages, 757 }; 758 } 759 760 /** 761 * Handler for the "evaluateJSAsync" request. This method evaluates a given 762 * JavaScript string with an associated `resultID`. 763 * 764 * The result will be returned later as an unsolicited `evaluationResult`, 765 * that can be associated back to this request via the `resultID` field. 766 * 767 * @param object request 768 * The JSON request object received from the Web Console client. 769 * @return object 770 * The response packet to send to with the unique id in the 771 * `resultID` field. 772 */ 773 async evaluateJSAsync(request) { 774 const startTime = ChromeUtils.dateNow(); 775 // Use a timestamp instead of a UUID as this code is used by workers, which 776 // don't have access to the UUID XPCOM component. 777 // Also use a counter in order to prevent mixing up response when calling 778 // at the exact same time. 779 const resultID = startTime + "-" + this._evalCounter++; 780 781 // Execute the evaluation in the next event loop in order to immediately 782 // reply with the resultID. 783 // 784 // The console input should be evaluated with micro task level != 0, 785 // so that microtask checkpoint isn't performed while evaluating it. 786 DevToolsUtils.executeSoonWithMicroTask(async () => { 787 try { 788 // Execute the script that may pause. 789 let response = await this.evaluateJS(request); 790 // Wait for any potential returned Promise. 791 response = await this._maybeWaitForResponseResult(response); 792 793 // Set the timestamp only now, so any messages logged in the expression (e.g. console.log) 794 // can be appended before the result message (unlike the evaluation result, other 795 // console resources are throttled before being handled by the webconsole client, 796 // which might cause some ordering issue). 797 // Use ChromeUtils.dateNow() as it gives us a higher precision than Date.now(). 798 response.timestamp = ChromeUtils.dateNow(); 799 // Finally, emit an unsolicited evaluationResult packet with the evaluation result. 800 this.emit("evaluationResult", { 801 type: "evaluationResult", 802 resultID, 803 startTime, 804 ...response, 805 }); 806 } catch (e) { 807 const message = `Encountered error while waiting for Helper Result: ${e}\n${e.stack}`; 808 DevToolsUtils.reportException("evaluateJSAsync", Error(message)); 809 } 810 }); 811 return { resultID }; 812 } 813 814 /** 815 * In order to support async evaluations (e.g. top-level await, …), 816 * we have to be able to handle promises. This method handles waiting for the promise, 817 * and then returns the result. 818 * 819 * @private 820 * @param object response 821 * The response packet to send to with the unique id in the 822 * `resultID` field, and potentially a promise in the `helperResult` or in the 823 * `awaitResult` field. 824 * 825 * @return object 826 * The updated response object. 827 */ 828 async _maybeWaitForResponseResult(response) { 829 if (!response?.awaitResult) { 830 return response; 831 } 832 833 let result; 834 try { 835 result = await response.awaitResult; 836 837 // `createValueGrip` expect a debuggee value, while here we have the raw object. 838 // We need to call `makeDebuggeeValue` on it to make it work. 839 const dbgResult = this.makeDebuggeeValue(result); 840 response.result = this.createValueGrip(dbgResult); 841 } catch (e) { 842 // The promise was rejected. We let the engine handle this as it will report a 843 // `uncaught exception` error. 844 response.topLevelAwaitRejected = true; 845 } 846 847 // Remove the promise from the response object. 848 delete response.awaitResult; 849 850 return response; 851 } 852 853 /** 854 * Handler for the "evaluateJS" request. This method evaluates the given 855 * JavaScript string and sends back the result. 856 * 857 * @param object request 858 * The JSON request object received from the Web Console client. 859 * @return object 860 * The evaluation response packet. 861 */ 862 evaluateJS(request) { 863 const input = request.text; 864 865 const evalOptions = { 866 frameActor: request.frameActor, 867 url: request.url, 868 innerWindowID: request.innerWindowID, 869 selectedNodeActor: request.selectedNodeActor, 870 selectedObjectActor: request.selectedObjectActor, 871 eager: request.eager, 872 bindings: request.bindings, 873 lineNumber: request.lineNumber, 874 // This flag is set to true in most cases as we consider most evaluations as internal and: 875 // * prevent any breakpoint from being triggerred when evaluating the JS input 876 // * prevent spawning Debugger.Source for the evaluated JS and showing it in Debugger UI 877 // This is only set to false when evaluating the console input. 878 disableBreaks: !!request.disableBreaks, 879 // Optional flag, to be set to true when Console Commands should override local symbols with 880 // the same name. Like if the page defines `$`, the evaluated string will use the `$` implemented 881 // by the console command instead of the page's function. 882 preferConsoleCommandsOverLocalSymbols: 883 !!request.preferConsoleCommandsOverLocalSymbols, 884 }; 885 886 const { mapped } = request; 887 888 // Set a flag on the thread actor which indicates an evaluation is being 889 // done for the client. This is used to disable all types of breakpoints for all sources 890 // via `disabledBreaks`. When this flag is used, `reportExceptionsWhenBreaksAreDisabled` 891 // allows to still pause on exceptions. 892 this.targetActor.threadActor.insideClientEvaluation = evalOptions; 893 894 let evalInfo; 895 try { 896 evalInfo = evalWithDebugger(input, evalOptions, this); 897 } finally { 898 this.targetActor.threadActor.insideClientEvaluation = null; 899 } 900 901 return new Promise((resolve, reject) => { 902 // Queue up a task to run in the next tick so any microtask created by the evaluated 903 // expression has the time to be run. 904 // e.g. in : 905 // ``` 906 // const promiseThenCb = result => "result: " + result; 907 // new Promise(res => res("hello")).then(promiseThenCb) 908 // ``` 909 // we want`promiseThenCb` to have run before handling the result. 910 DevToolsUtils.executeSoon(() => { 911 try { 912 const result = this.prepareEvaluationResult( 913 evalInfo, 914 input, 915 request.eager, 916 mapped, 917 request.evalInTracer 918 ); 919 resolve(result); 920 } catch (err) { 921 reject(err); 922 } 923 }); 924 }); 925 } 926 927 // eslint-disable-next-line complexity 928 prepareEvaluationResult(evalInfo, input, eager, mapped, evalInTracer) { 929 const evalResult = evalInfo.result; 930 const helperResult = evalInfo.helperResult; 931 932 let result, 933 errorDocURL, 934 errorMessage, 935 errorNotes = null, 936 errorGrip = null, 937 frame = null, 938 awaitResult, 939 errorMessageName, 940 exceptionStack; 941 if (evalResult) { 942 if ("return" in evalResult) { 943 result = evalResult.return; 944 if ( 945 mapped?.await && 946 result && 947 result.class === "Promise" && 948 typeof result.unsafeDereference === "function" 949 ) { 950 awaitResult = result.unsafeDereference(); 951 } 952 } else if ("yield" in evalResult) { 953 result = evalResult.yield; 954 } else if ("throw" in evalResult) { 955 const error = evalResult.throw; 956 const allowSideEffect = !eager; 957 errorGrip = this.createValueGrip(error, { allowSideEffect }); 958 959 exceptionStack = this.prepareStackForRemote(evalResult.stack); 960 961 if (exceptionStack) { 962 exceptionStack = 963 WebConsoleUtils.removeFramesAboveDebuggerEval(exceptionStack); 964 965 // Set the frame based on the topmost stack frame for the exception. 966 if (exceptionStack && exceptionStack.length) { 967 const { 968 filename: source, 969 sourceId, 970 lineNumber: line, 971 columnNumber: column, 972 } = exceptionStack[0]; 973 frame = { source, sourceId, line, column }; 974 } 975 } 976 977 errorMessage = String(error); 978 if (allowSideEffect && typeof error === "object" && error !== null) { 979 try { 980 errorMessage = DevToolsUtils.callPropertyOnObject( 981 error, 982 "toString" 983 ); 984 } catch (e) { 985 // If the debuggee is not allowed to access the "toString" property 986 // of the error object, calling this property from the debuggee's 987 // compartment will fail. The debugger should show the error object 988 // as it is seen by the debuggee, so this behavior is correct. 989 // 990 // Unfortunately, we have at least one test that assumes calling the 991 // "toString" property of an error object will succeed if the 992 // debugger is allowed to access it, regardless of whether the 993 // debuggee is allowed to access it or not. 994 // 995 // To accomodate these tests, if calling the "toString" property 996 // from the debuggee compartment fails, we rewrap the error object 997 // in the debugger's compartment, and then call the "toString" 998 // property from there. 999 if (typeof error.unsafeDereference === "function") { 1000 const rawError = error.unsafeDereference(); 1001 errorMessage = rawError ? rawError.toString() : ""; 1002 } 1003 } 1004 } 1005 1006 // It is possible that we won't have permission to unwrap an 1007 // object and retrieve its errorMessageName. 1008 try { 1009 errorDocURL = ErrorDocs.GetURL(error); 1010 errorMessageName = error.errorMessageName; 1011 } catch (ex) { 1012 // ignored 1013 } 1014 1015 try { 1016 const line = error.errorLineNumber; 1017 const column = error.errorColumnNumber; 1018 1019 if ( 1020 !frame && 1021 typeof line === "number" && 1022 typeof column === "number" 1023 ) { 1024 // Set frame only if we have line/column numbers. 1025 frame = { 1026 source: "debugger eval code", 1027 line, 1028 column, 1029 }; 1030 } 1031 } catch (ex) { 1032 // ignored 1033 } 1034 1035 try { 1036 const notes = error.errorNotes; 1037 if (notes?.length) { 1038 errorNotes = []; 1039 for (const note of notes) { 1040 errorNotes.push({ 1041 messageBody: this._createStringGrip(note.message), 1042 frame: { 1043 source: note.fileName, 1044 line: note.lineNumber, 1045 column: note.columnNumber, 1046 }, 1047 }); 1048 } 1049 } 1050 } catch (ex) { 1051 // ignored 1052 } 1053 } 1054 } 1055 // If a value is encountered that the devtools server doesn't support yet, 1056 // the console should remain functional. 1057 let resultGrip; 1058 if (!awaitResult) { 1059 try { 1060 const objectActor = 1061 this.targetActor.threadActor.getThreadLifetimeObject(result); 1062 if (evalInTracer) { 1063 const tracerActor = this.targetActor.getTargetScopedActor("tracer"); 1064 resultGrip = tracerActor.createValueGrip(result); 1065 } else if (objectActor) { 1066 resultGrip = this.targetActor.threadActor.createValueGrip(result); 1067 } else { 1068 resultGrip = this.createValueGrip(result); 1069 } 1070 } catch (e) { 1071 errorMessage = e; 1072 } 1073 } 1074 1075 // Don't update _lastConsoleInputEvaluation in eager evaluation, as it would interfere 1076 // with the $_ command. 1077 if (!eager) { 1078 if (!awaitResult) { 1079 this._lastConsoleInputEvaluation = result; 1080 } else { 1081 // If we evaluated a top-level await expression, we want to assign its result to the 1082 // _lastConsoleInputEvaluation only when the promise resolves, and only if it 1083 // resolves. If the promise rejects, we don't re-assign _lastConsoleInputEvaluation, 1084 // it will keep its previous value. 1085 1086 const p = awaitResult.then(res => { 1087 this._lastConsoleInputEvaluation = this.makeDebuggeeValue(res); 1088 }); 1089 1090 // If the top level await was already rejected (e.g. `await Promise.reject("bleh")`), 1091 // catch the resulting promise of awaitResult.then. 1092 // If we don't do that, the new Promise will also be rejected, and since it's 1093 // unhandled, it will generate an error. 1094 // We don't want to do that for pending promise (e.g. `await new Promise((res, rej) => setTimeout(rej,250))`), 1095 // as the the Promise rejection will be considered as handled, and the "Uncaught (in promise)" 1096 // message wouldn't be emitted. 1097 const { state } = ObjectUtils.getPromiseState(evalResult.return); 1098 if (state === "rejected") { 1099 p.catch(() => {}); 1100 } 1101 } 1102 } 1103 1104 return { 1105 input, 1106 result: resultGrip, 1107 awaitResult, 1108 exception: errorGrip, 1109 exceptionMessage: this._createStringGrip(errorMessage), 1110 exceptionDocURL: errorDocURL, 1111 exceptionStack, 1112 hasException: errorGrip !== null, 1113 errorMessageName, 1114 frame, 1115 helperResult, 1116 notes: errorNotes, 1117 }; 1118 } 1119 1120 /** 1121 * The Autocomplete request handler. 1122 * 1123 * @param string text 1124 * The request message - what input to autocomplete. 1125 * @param number cursor 1126 * The cursor position at the moment of starting autocomplete. 1127 * @param string frameActor 1128 * The frameactor id of the current paused frame. 1129 * @param string selectedNodeActor 1130 * The actor id of the currently selected node. 1131 * @param array authorizedEvaluations 1132 * Array of the properties access which can be executed by the engine. 1133 * @return object 1134 * The response message - matched properties. 1135 */ 1136 autocomplete( 1137 text, 1138 cursor, 1139 frameActorId, 1140 selectedNodeActor, 1141 authorizedEvaluations, 1142 expressionVars = [] 1143 ) { 1144 let dbgObject = null; 1145 let environment = null; 1146 let matches = []; 1147 let matchProp; 1148 let isElementAccess; 1149 1150 const reqText = text.substr(0, cursor); 1151 1152 if (isCommand(reqText)) { 1153 matchProp = reqText; 1154 matches = WebConsoleCommandsManager.getAllColonCommandNames() 1155 .filter(c => `:${c}`.startsWith(reqText)) 1156 .map(c => `:${c}`); 1157 } else { 1158 // This is the case of the paused debugger 1159 if (frameActorId) { 1160 const frameActor = this.conn.getActor(frameActorId); 1161 try { 1162 // Need to try/catch since accessing frame.environment 1163 // can throw "Debugger.Frame is not live" 1164 const frame = frameActor.frame; 1165 environment = frame.environment; 1166 } catch (e) { 1167 DevToolsUtils.reportException( 1168 "autocomplete", 1169 Error("The frame actor was not found: " + frameActorId) 1170 ); 1171 } 1172 } else { 1173 dbgObject = this.dbg.addDebuggee(this.evalGlobal); 1174 } 1175 1176 const result = jsPropertyProvider({ 1177 dbgObject, 1178 environment, 1179 frameActorId, 1180 inputValue: text, 1181 cursor, 1182 webconsoleActor: this, 1183 selectedNodeActor, 1184 authorizedEvaluations, 1185 expressionVars, 1186 }); 1187 1188 if (result === null) { 1189 return { 1190 matches: null, 1191 }; 1192 } 1193 1194 if (result && result.isUnsafeGetter === true) { 1195 return { 1196 isUnsafeGetter: true, 1197 getterPath: result.getterPath, 1198 }; 1199 } 1200 1201 matches = result.matches || new Set(); 1202 matchProp = result.matchProp || ""; 1203 isElementAccess = result.isElementAccess; 1204 1205 // We consider '$' as alphanumeric because it is used in the names of some 1206 // helper functions; we also consider whitespace as alphanum since it should not 1207 // be seen as break in the evaled string. 1208 const lastNonAlphaIsDot = /[.][a-zA-Z0-9$\s]*$/.test(reqText); 1209 1210 // We only return commands and keywords when we are not dealing with a property or 1211 // element access. 1212 if (matchProp && !lastNonAlphaIsDot && !isElementAccess) { 1213 const colonOnlyCommands = 1214 WebConsoleCommandsManager.getColonOnlyCommandNames(); 1215 for (const name of WebConsoleCommandsManager.getAllCommandNames()) { 1216 // Filter out commands like `screenshot` as it is inaccessible without the `:` prefix 1217 if ( 1218 !colonOnlyCommands.includes(name) && 1219 name.startsWith(result.matchProp) 1220 ) { 1221 matches.add(name); 1222 } 1223 } 1224 1225 for (const keyword of RESERVED_JS_KEYWORDS) { 1226 if (keyword.startsWith(result.matchProp)) { 1227 matches.add(keyword); 1228 } 1229 } 1230 } 1231 1232 // Sort the results in order to display lowercased item first (e.g. we want to 1233 // display `document` then `Document` as we loosely match the user input if the 1234 // first letter was lowercase). 1235 const firstMeaningfulCharIndex = isElementAccess ? 1 : 0; 1236 matches = Array.from(matches).sort((a, b) => { 1237 const aFirstMeaningfulChar = a[firstMeaningfulCharIndex]; 1238 const bFirstMeaningfulChar = b[firstMeaningfulCharIndex]; 1239 const lA = 1240 aFirstMeaningfulChar.toLocaleLowerCase() === aFirstMeaningfulChar; 1241 const lB = 1242 bFirstMeaningfulChar.toLocaleLowerCase() === bFirstMeaningfulChar; 1243 if (lA === lB) { 1244 if (a === matchProp) { 1245 return -1; 1246 } 1247 if (b === matchProp) { 1248 return 1; 1249 } 1250 return a.localeCompare(b); 1251 } 1252 return lA ? -1 : 1; 1253 }); 1254 } 1255 1256 return { 1257 matches, 1258 matchProp, 1259 isElementAccess: isElementAccess === true, 1260 }; 1261 } 1262 1263 /** 1264 * The "clearMessagesCacheAsync" request handler. 1265 */ 1266 clearMessagesCacheAsync() { 1267 if (isWorker) { 1268 // Defined on WorkerScope 1269 clearConsoleEvents(); 1270 return; 1271 } 1272 1273 const windowId = !this.targetActor.isRootActor 1274 ? WebConsoleUtils.getInnerWindowId(this.global) 1275 : null; 1276 1277 const ConsoleAPIStorage = Cc[ 1278 "@mozilla.org/consoleAPI-storage;1" 1279 ].getService(Ci.nsIConsoleAPIStorage); 1280 ConsoleAPIStorage.clearEvents(windowId); 1281 1282 CONSOLE_WORKER_IDS.forEach(id => { 1283 ConsoleAPIStorage.clearEvents(id); 1284 }); 1285 1286 if (this.targetActor.isRootActor || !this.global) { 1287 // If were dealing with the root actor (e.g. the browser console), we want 1288 // to remove all cached messages, not only the ones specific to a window. 1289 Services.console.reset(); 1290 } else if (this.targetActor.ignoreSubFrames) { 1291 Services.console.resetWindow(windowId); 1292 } else { 1293 WebConsoleUtils.getInnerWindowIDsForFrames(this.global).forEach(id => 1294 Services.console.resetWindow(id) 1295 ); 1296 } 1297 } 1298 1299 // End of request handlers. 1300 1301 // Event handlers for various listeners. 1302 1303 /** 1304 * Handler for messages received from the ConsoleServiceListener. This method 1305 * sends the nsIConsoleMessage to the remote Web Console client. 1306 * 1307 * @param nsIConsoleMessage message 1308 * The message we need to send to the client. 1309 */ 1310 onConsoleServiceMessage(message) { 1311 if (message instanceof Ci.nsIScriptError) { 1312 this.emit("pageError", { 1313 pageError: this.preparePageErrorForRemote(message), 1314 }); 1315 } else { 1316 this.emit("logMessage", { 1317 message: this._createStringGrip(message.message), 1318 timeStamp: message.microSecondTimeStamp / 1000, 1319 }); 1320 } 1321 } 1322 1323 getActorIdForInternalSourceId(id) { 1324 const actor = 1325 this.targetActor.sourcesManager.getSourceActorByInternalSourceId(id); 1326 return actor ? actor.actorID : null; 1327 } 1328 1329 /** 1330 * Prepare a SavedFrame stack to be sent to the client. 1331 * 1332 * @param SavedFrame errorStack 1333 * Stack for an error we need to send to the client. 1334 * @return object 1335 * The object you can send to the remote client. 1336 */ 1337 prepareStackForRemote(errorStack) { 1338 // Convert stack objects to the JSON attributes expected by client code 1339 // Bug 1348885: If the global from which this error came from has been 1340 // nuked, stack is going to be a dead wrapper. 1341 if (!errorStack || (Cu && Cu.isDeadWrapper(errorStack))) { 1342 return null; 1343 } 1344 const stack = []; 1345 let s = errorStack; 1346 while (s) { 1347 stack.push({ 1348 filename: s.source, 1349 sourceId: this.getActorIdForInternalSourceId(s.sourceId), 1350 lineNumber: s.line, 1351 columnNumber: s.column, 1352 functionName: s.functionDisplayName, 1353 asyncCause: s.asyncCause ? s.asyncCause : undefined, 1354 }); 1355 s = s.parent || s.asyncParent; 1356 } 1357 return stack; 1358 } 1359 1360 /** 1361 * Prepare an nsIScriptError to be sent to the client. 1362 * 1363 * @param nsIScriptError pageError 1364 * The page error we need to send to the client. 1365 * @return object 1366 * The object you can send to the remote client. 1367 */ 1368 preparePageErrorForRemote(pageError) { 1369 const stack = this.prepareStackForRemote(pageError.stack); 1370 let notesArray = null; 1371 const notes = pageError.notes; 1372 if (notes?.length) { 1373 notesArray = []; 1374 for (let i = 0, len = notes.length; i < len; i++) { 1375 const note = notes.queryElementAt(i, Ci.nsIScriptErrorNote); 1376 notesArray.push({ 1377 messageBody: this._createStringGrip(note.errorMessage), 1378 frame: { 1379 source: note.sourceName, 1380 sourceId: this.getActorIdForInternalSourceId(note.sourceId), 1381 line: note.lineNumber, 1382 column: note.columnNumber, 1383 }, 1384 }); 1385 } 1386 } 1387 1388 // If there is no location information in the error but we have a stack, 1389 // fill in the location with the first frame on the stack. 1390 let { sourceName, sourceId, lineNumber, columnNumber } = pageError; 1391 if (!sourceName && !sourceId && !lineNumber && !columnNumber && stack) { 1392 sourceName = stack[0].filename; 1393 sourceId = stack[0].sourceId; 1394 lineNumber = stack[0].lineNumber; 1395 columnNumber = stack[0].columnNumber; 1396 } 1397 1398 const isCSSMessage = pageError.category === MESSAGE_CATEGORY.CSS_PARSER; 1399 1400 const result = { 1401 errorMessage: this._createStringGrip(pageError.errorMessage), 1402 errorMessageName: isCSSMessage ? undefined : pageError.errorMessageName, 1403 exceptionDocURL: ErrorDocs.GetURL(pageError), 1404 sourceName, 1405 sourceId: this.getActorIdForInternalSourceId(sourceId), 1406 lineNumber, 1407 columnNumber, 1408 category: pageError.category, 1409 innerWindowID: pageError.innerWindowID, 1410 timeStamp: pageError.microSecondTimeStamp / 1000, 1411 warning: !!(pageError.flags & pageError.warningFlag), 1412 error: !(pageError.flags & (pageError.warningFlag | pageError.infoFlag)), 1413 info: !!(pageError.flags & pageError.infoFlag), 1414 private: pageError.isFromPrivateWindow, 1415 stacktrace: stack, 1416 notes: notesArray, 1417 chromeContext: pageError.isFromChromeContext, 1418 isPromiseRejection: isCSSMessage 1419 ? undefined 1420 : pageError.isPromiseRejection, 1421 isForwardedFromContentProcess: pageError.isForwardedFromContentProcess, 1422 cssSelectors: isCSSMessage ? pageError.cssSelectors : undefined, 1423 }; 1424 1425 // If the pageError does have an exception object, we want to return the grip for it, 1426 // but only if we do manage to get the grip, as we're checking the property on the 1427 // client to render things differently. 1428 if (pageError.hasException) { 1429 try { 1430 const obj = this.makeDebuggeeValue(pageError.exception, true); 1431 if (obj?.class !== "DeadObject") { 1432 result.exception = this.createValueGrip(obj); 1433 result.hasException = true; 1434 } 1435 } catch (e) {} 1436 } 1437 1438 return result; 1439 } 1440 1441 /** 1442 * Handler for window.console API calls received from the ConsoleAPIListener. 1443 * This method sends the object to the remote Web Console client. 1444 * 1445 * @see ConsoleAPIListener 1446 * @param object message 1447 * The console API call we need to send to the remote client. 1448 * @param object extraProperties 1449 * an object whose properties will be folded in the packet that is emitted. 1450 */ 1451 onConsoleAPICall(message, extraProperties = {}) { 1452 this.emit("consoleAPICall", { 1453 message: this.prepareConsoleMessageForRemote(message), 1454 ...extraProperties, 1455 }); 1456 } 1457 1458 /** 1459 * Handler for the DocumentEventsListener. 1460 * 1461 * @see DocumentEventsListener 1462 * @param {string} name 1463 * The document event name that either of followings. 1464 * - dom-loading 1465 * - dom-interactive 1466 * - dom-complete 1467 * @param {number} time 1468 * The time that the event is fired. 1469 * @param {boolean} hasNativeConsoleAPI 1470 * Tells if the window.console object is native or overwritten by script in the page. 1471 * Only passed when `name` is "dom-complete" (see devtools/server/actors/webconsole/listeners/document-events.js). 1472 */ 1473 onDocumentEvent(name, { time, hasNativeConsoleAPI }) { 1474 this.emit("documentEvent", { 1475 name, 1476 time, 1477 hasNativeConsoleAPI, 1478 }); 1479 } 1480 1481 /** 1482 * Handler for file activity. This method sends the file request information 1483 * to the remote Web Console client. 1484 * 1485 * @see ConsoleFileActivityListener 1486 * @param string fileURI 1487 * The requested file URI. 1488 */ 1489 onFileActivity(fileURI) { 1490 this.emit("fileActivity", { 1491 uri: fileURI, 1492 }); 1493 } 1494 1495 // End of event handlers for various listeners. 1496 1497 /** 1498 * Prepare a message from the console API to be sent to the remote Web Console 1499 * instance. 1500 * 1501 * @param object message 1502 * The original message received from the console storage listener. 1503 * @param boolean aUseObjectGlobal 1504 * If |true| the object global is determined and added as a debuggee, 1505 * otherwise |this.global| is used when makeDebuggeeValue() is invoked. 1506 * @return object 1507 * The object that can be sent to the remote client. 1508 */ 1509 prepareConsoleMessageForRemote(message, useObjectGlobal = true) { 1510 const result = { 1511 arguments: message.arguments 1512 ? message.arguments.map(obj => { 1513 const dbgObj = this.makeDebuggeeValue(obj, useObjectGlobal); 1514 return this.createValueGrip(dbgObj); 1515 }) 1516 : [], 1517 chromeContext: message.chromeContext, 1518 columnNumber: message.columnNumber, 1519 filename: message.filename, 1520 level: message.level, 1521 lineNumber: message.lineNumber, 1522 // messages emitted from Console.sys.mjs don't have a microSecondTimeStamp property 1523 timeStamp: message.microSecondTimeStamp 1524 ? message.microSecondTimeStamp / 1000 1525 : message.timeStamp, 1526 sourceId: this.getActorIdForInternalSourceId(message.sourceId), 1527 category: message.category || "webdev", 1528 innerWindowID: message.innerID, 1529 }; 1530 1531 // It only make sense to include the following properties in the message when they have 1532 // a meaningful value. Otherwise we simply don't include them so we save cycles in JSActor communication. 1533 if (message.counter) { 1534 result.counter = message.counter; 1535 } 1536 if (message.private) { 1537 result.private = message.private; 1538 } 1539 if (message.prefix) { 1540 result.prefix = message.prefix; 1541 } 1542 1543 if (message.stacktrace) { 1544 result.stacktrace = message.stacktrace.map(frame => { 1545 return { 1546 ...frame, 1547 sourceId: this.getActorIdForInternalSourceId(frame.sourceId), 1548 }; 1549 }); 1550 } 1551 1552 if (message.styles && message.styles.length) { 1553 result.styles = message.styles.map(string => { 1554 return this.createValueGrip(string); 1555 }); 1556 } 1557 1558 if (message.timer) { 1559 result.timer = message.timer; 1560 } 1561 1562 if (message.level === "table") { 1563 const tableItems = this._getConsoleTableMessageItems(result); 1564 if (tableItems) { 1565 result.arguments[0].ownProperties = tableItems; 1566 result.arguments[0].preview = null; 1567 } 1568 1569 // Only return the 2 first params. 1570 result.arguments = result.arguments.slice(0, 2); 1571 } 1572 1573 return result; 1574 } 1575 1576 /** 1577 * Return the properties needed to display the appropriate table for a given 1578 * console.table call. 1579 * This function does a little more than creating an ObjectActor for the first 1580 * parameter of the message. When layout out the console table in the output, we want 1581 * to be able to look into sub-properties so the table can have a different layout ( 1582 * for arrays of arrays, objects with objects properties, arrays of objects, …). 1583 * So here we need to retrieve the properties of the first parameter, and also all the 1584 * sub-properties we might need. 1585 * 1586 * @param {object} result: The console.table message. 1587 * @returns {object} An object containing the properties of the first argument of the 1588 * console.table call. 1589 */ 1590 _getConsoleTableMessageItems(result) { 1591 if ( 1592 !result || 1593 !Array.isArray(result.arguments) || 1594 !result.arguments.length 1595 ) { 1596 return null; 1597 } 1598 1599 const [tableItemGrip] = result.arguments; 1600 const dataType = tableItemGrip.class; 1601 const needEntries = ["Map", "WeakMap", "Set", "WeakSet"].includes(dataType); 1602 const ignoreNonIndexedProperties = isArray(tableItemGrip); 1603 1604 const tableItemActor = this.targetActor.objectsPool.getActorByID( 1605 tableItemGrip.actor 1606 ); 1607 if (!tableItemActor) { 1608 return null; 1609 } 1610 1611 // Retrieve the properties (or entries for Set/Map) of the console table first arg. 1612 const iterator = needEntries 1613 ? tableItemActor.enumEntries() 1614 : tableItemActor.enumProperties({ 1615 ignoreNonIndexedProperties, 1616 }); 1617 const { ownProperties } = iterator.all(); 1618 1619 // The iterator returns a descriptor for each property, wherein the value could be 1620 // in one of those sub-property. 1621 const descriptorKeys = ["safeGetterValues", "getterValue", "value"]; 1622 1623 Object.values(ownProperties).forEach(desc => { 1624 if (typeof desc !== "undefined") { 1625 descriptorKeys.forEach(key => { 1626 if (desc && desc.hasOwnProperty(key)) { 1627 const grip = desc[key]; 1628 1629 // We need to load sub-properties as well to render the table in a nice way. 1630 const actor = 1631 grip && this.targetActor.objectsPool.getActorByID(grip.actor); 1632 if (actor && typeof actor.enumProperties === "function") { 1633 const res = actor 1634 .enumProperties({ 1635 ignoreNonIndexedProperties: isArray(grip), 1636 }) 1637 .all(); 1638 if (res?.ownProperties) { 1639 desc[key].ownProperties = res.ownProperties; 1640 } 1641 } 1642 } 1643 }); 1644 } 1645 }); 1646 1647 return ownProperties; 1648 } 1649 1650 /** 1651 * The "will-navigate" progress listener. This is used to clear the current 1652 * eval scope. 1653 */ 1654 _onWillNavigate({ isTopLevel }) { 1655 if (isTopLevel) { 1656 this._evalGlobal = null; 1657 this.targetActor.off("will-navigate", this._onWillNavigate); 1658 this._progressListenerActive = false; 1659 } 1660 } 1661 1662 /** 1663 * This listener is called when we switch to another frame, 1664 * mostly to unregister previous listeners and start listening on the new document. 1665 */ 1666 _onChangedToplevelDocument() { 1667 // Convert the Set to an Array 1668 const listeners = [...this._listeners]; 1669 1670 // Unregister existing listener on the previous document 1671 // (pass a copy of the array as it will shift from it) 1672 this.stopListeners(listeners.slice()); 1673 1674 // This method is called after this.global is changed, 1675 // so we register new listener on this new global 1676 this.startListeners(listeners); 1677 1678 // Also reset the cached top level chrome window being targeted 1679 this._lastChromeWindow = null; 1680 } 1681 } 1682 1683 exports.WebConsoleActor = WebConsoleActor;