object.js (23962B)
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 "use strict"; 6 7 const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); 8 const { objectSpec } = require("resource://devtools/shared/specs/object.js"); 9 10 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 11 const { assert } = DevToolsUtils; 12 13 loader.lazyRequireGetter( 14 this, 15 "propertyDescriptor", 16 "resource://devtools/server/actors/object/property-descriptor.js", 17 true 18 ); 19 loader.lazyRequireGetter( 20 this, 21 "PropertyIteratorActor", 22 "resource://devtools/server/actors/object/property-iterator.js", 23 true 24 ); 25 loader.lazyRequireGetter( 26 this, 27 "SymbolIteratorActor", 28 "resource://devtools/server/actors/object/symbol-iterator.js", 29 true 30 ); 31 loader.lazyRequireGetter( 32 this, 33 "PrivatePropertiesIteratorActor", 34 "resource://devtools/server/actors/object/private-properties-iterator.js", 35 true 36 ); 37 loader.lazyRequireGetter( 38 this, 39 "previewers", 40 "resource://devtools/server/actors/object/previewers.js" 41 ); 42 43 loader.lazyRequireGetter( 44 this, 45 ["customFormatterHeader", "customFormatterBody"], 46 "resource://devtools/server/actors/utils/custom-formatters.js", 47 true 48 ); 49 50 // This is going to be used by findSafeGetters, where we want to avoid calling getters for 51 // deprecated properties (otherwise a warning message is displayed in the console). 52 // We could do something like EagerEvaluation, where we create a new Sandbox which is then 53 // used to compare functions, but, we'd need to make new classes available in 54 // the Sandbox, and possibly do it again when a new property gets deprecated. 55 // Since this is only to be able to automatically call getters, we can simply check against 56 // a list of unsafe getters that we generate from webidls. 57 loader.lazyRequireGetter( 58 this, 59 "unsafeGettersNames", 60 "resource://devtools/server/actors/webconsole/webidl-unsafe-getters-names.js" 61 ); 62 63 // ContentDOMReference requires ChromeUtils, which isn't available in worker context. 64 const lazy = {}; 65 if (!isWorker) { 66 loader.lazyGetter( 67 lazy, 68 "ContentDOMReference", 69 () => 70 ChromeUtils.importESModule( 71 "resource://gre/modules/ContentDOMReference.sys.mjs", 72 // ContentDOMReference needs to be retrieved from the shared global 73 // since it is a shared singleton. 74 { global: "shared" } 75 ).ContentDOMReference 76 ); 77 } 78 79 const { 80 getArrayLength, 81 getPromiseState, 82 getStorageLength, 83 isArray, 84 isStorage, 85 isTypedArray, 86 createValueGrip, 87 } = require("resource://devtools/server/actors/object/utils.js"); 88 89 class ObjectActor extends Actor { 90 /** 91 * Creates an actor for the specified object. 92 * 93 * @param ThreadActor threadActor 94 * The current thread actor from where this object is running from. 95 * @param Debugger.Object obj 96 * The debuggee object. 97 * @param Object 98 * A collection of abstract methods that are implemented by the caller. 99 * ObjectActor requires the following functions to be implemented by 100 * the caller: 101 * - {Number} customFormatterObjectTagDepth: See `processObjectTag` 102 * - {Debugger.Object} customFormatterConfigDbgObj 103 * - {bool} allowSideEffect: allow side effectful operations while 104 * constructing a preview 105 */ 106 constructor( 107 threadActor, 108 obj, 109 { 110 customFormatterObjectTagDepth, 111 customFormatterConfigDbgObj, 112 allowSideEffect = true, 113 } 114 ) { 115 super(threadActor.conn, objectSpec); 116 117 assert( 118 !obj.optimizedOut, 119 "Should not create object actors for optimized out values!" 120 ); 121 122 this.obj = obj; 123 this.targetActor = threadActor.targetActor; 124 this.threadActor = threadActor; 125 this.rawObj = obj.unsafeDereference(); 126 this.safeRawObj = this.#getSafeRawObject(); 127 this.allowSideEffect = allowSideEffect; 128 129 // Cache obj.class as it can be costly when queried from previewers if this is in a hot path 130 // (e.g. logging objects within a for loops). 131 this.className = this.obj.class; 132 133 this.hooks = { 134 customFormatterObjectTagDepth, 135 customFormatterConfigDbgObj, 136 }; 137 } 138 139 addWatchpoint(property, label, watchpointType) { 140 this.threadActor.addWatchpoint(this, { property, label, watchpointType }); 141 } 142 143 removeWatchpoint(property) { 144 this.threadActor.removeWatchpoint(this, property); 145 } 146 147 removeWatchpoints() { 148 this.threadActor.removeWatchpoint(this); 149 } 150 151 createValueGrip(value, depth) { 152 if (typeof depth != "number") { 153 throw new Error("Missing 'depth' argument to createValeuGrip()"); 154 } 155 return createValueGrip( 156 this.threadActor, 157 value, 158 // Register any nested object actor in the same pool as their parent object actor 159 this.getParent(), 160 depth, 161 this.hooks 162 ); 163 } 164 165 /** 166 * Returns a grip for this actor for returning in a protocol message. 167 * 168 * @param {number} depth 169 * Current depth in the generated preview object sent to the client. 170 */ 171 form({ depth = 0 } = {}) { 172 const g = { 173 type: "object", 174 actor: this.actorID, 175 }; 176 177 const unwrapped = DevToolsUtils.unwrap(this.obj); 178 if (unwrapped === undefined) { 179 // Objects belonging to an invisible-to-debugger compartment might be proxies, 180 // so just in case they shouldn't be accessed. 181 g.class = "InvisibleToDebugger: " + this.className; 182 return g; 183 } 184 185 // Only process custom formatters if the feature is enabled. 186 if (this.threadActor?.targetActor?.customFormatters) { 187 const result = customFormatterHeader(this); 188 if (result) { 189 const { formatter, ...header } = result; 190 this._customFormatterItem = formatter; 191 192 return { 193 ...g, 194 ...header, 195 }; 196 } 197 } 198 199 if (unwrapped?.isProxy) { 200 // Proxy objects can run traps when accessed, so just create a preview with 201 // the target and the handler. 202 g.class = "Proxy"; 203 previewers.Proxy[0](this, g, depth + 1); 204 return g; 205 } 206 207 const ownPropertyLength = this._getOwnPropertyLength(); 208 209 Object.assign(g, { 210 // If the debuggee does not subsume the object's compartment, most properties won't 211 // be accessible. Cross-orgin Window and Location objects might expose some, though. 212 // Change the displayed class, but when creating the preview use the original one. 213 class: unwrapped === null ? "Restricted" : this.className, 214 ownPropertyLength: Number.isFinite(ownPropertyLength) 215 ? ownPropertyLength 216 : undefined, 217 extensible: this.obj.isExtensible(), 218 frozen: this.obj.isFrozen(), 219 sealed: this.obj.isSealed(), 220 isError: this.obj.isError, 221 }); 222 223 if (g.class == "Function") { 224 g.isClassConstructor = this.obj.isClassConstructor; 225 } 226 227 this._populateGripPreview(g, depth + 1); 228 229 if ( 230 this.safeRawObj && 231 Node.isInstance(this.safeRawObj) && 232 lazy.ContentDOMReference 233 ) { 234 // ContentDOMReference.get takes a DOM element and returns an object with 235 // its browsing context id, as well as a unique identifier. We are putting it in 236 // the grip here in order to be able to retrieve the node later, potentially from a 237 // different DevToolsServer running in the same process. 238 // If ContentDOMReference.get throws, we simply don't add the property to the grip. 239 try { 240 g.contentDomReference = lazy.ContentDOMReference.get(this.safeRawObj); 241 } catch (e) {} 242 } 243 244 return g; 245 } 246 247 customFormatterBody() { 248 return customFormatterBody(this, this._customFormatterItem); 249 } 250 251 _getOwnPropertyLength() { 252 if (isTypedArray(this.obj)) { 253 // Bug 1348761: getOwnPropertyNames is unnecessary slow on TypedArrays 254 return getArrayLength(this.obj); 255 } 256 257 if (isStorage(this.obj)) { 258 return getStorageLength(this.obj); 259 } 260 261 try { 262 return this.obj.getOwnPropertyNamesLength(); 263 } catch (err) { 264 // The above can throw when the debuggee does not subsume the object's 265 // compartment, or for some WrappedNatives like Cu.Sandbox. 266 } 267 268 return null; 269 } 270 271 #getSafeRawObject() { 272 let raw = this.rawObj; 273 274 // If Cu is not defined, we are running on a worker thread, where xrays 275 // don't exist. 276 if (raw && Cu) { 277 raw = Cu.unwaiveXrays(raw); 278 } 279 280 if (raw && !DevToolsUtils.isSafeJSObject(raw)) { 281 raw = null; 282 } 283 284 return raw; 285 } 286 287 /** 288 * Populate the `preview` property on `grip` given its type. 289 * 290 * @param {object} grip 291 * Object onto which preview data attribute should be added. 292 * @param {number} depth 293 * Current depth in the generated preview object sent to the client. 294 */ 295 _populateGripPreview(grip, depth) { 296 for (const previewer of previewers[this.className] || previewers.Object) { 297 try { 298 const previewerResult = previewer(this, grip, depth); 299 if (previewerResult) { 300 return; 301 } 302 } catch (e) { 303 const msg = 304 "ObjectActor.prototype._populateGripPreview previewer function"; 305 DevToolsUtils.reportException(msg, e); 306 } 307 } 308 } 309 310 /** 311 * Returns an object exposing the internal Promise state. 312 */ 313 promiseState() { 314 const { state, value, reason } = getPromiseState(this.obj); 315 const promiseState = { state }; 316 317 if (state == "fulfilled") { 318 promiseState.value = this.createValueGrip(value, 0); 319 } else if (state == "rejected") { 320 promiseState.reason = this.createValueGrip(reason, 0); 321 } 322 323 promiseState.creationTimestamp = Date.now() - this.obj.promiseLifetime; 324 325 // Only add the timeToSettle property if the Promise isn't pending. 326 if (state !== "pending") { 327 promiseState.timeToSettle = this.obj.promiseTimeToResolution; 328 } 329 330 return { promiseState }; 331 } 332 333 /** 334 * Creates an actor to iterate over an object property names and values. 335 * See PropertyIteratorActor constructor for more info about options param. 336 * 337 * @param options object 338 */ 339 enumProperties(options) { 340 return new PropertyIteratorActor(this, options, this.conn); 341 } 342 343 /** 344 * Creates an actor to iterate over entries of a Map/Set-like object. 345 */ 346 enumEntries() { 347 return new PropertyIteratorActor(this, { enumEntries: true }, this.conn); 348 } 349 350 /** 351 * Creates an actor to iterate over an object symbols properties. 352 */ 353 enumSymbols() { 354 return new SymbolIteratorActor(this, this.conn); 355 } 356 357 /** 358 * Creates an actor to iterate over an object private properties. 359 */ 360 enumPrivateProperties() { 361 return new PrivatePropertiesIteratorActor(this, this.conn); 362 } 363 364 /** 365 * Handle a protocol request to provide the prototype and own properties of 366 * the object. 367 * 368 * @returns {object} An object containing the data of this.obj, of the following form: 369 * - {Object} prototype: The descriptor of this.obj's prototype. 370 * - {Object} ownProperties: an object where the keys are the names of the 371 * this.obj's ownProperties, and the values the descriptors of 372 * the properties. 373 * - {Array} ownSymbols: An array containing all descriptors of this.obj's 374 * ownSymbols. Here we have an array, and not an object like for 375 * ownProperties, because we can have multiple symbols with the same 376 * name in this.obj, e.g. `{[Symbol()]: "a", [Symbol()]: "b"}`. 377 * - {Object} safeGetterValues: an object that maps this.obj's property names 378 * with safe getters descriptors. 379 */ 380 prototypeAndProperties() { 381 let objProto = null; 382 let names = []; 383 let symbols = []; 384 if (DevToolsUtils.isSafeDebuggerObject(this.obj)) { 385 try { 386 objProto = this.obj.proto; 387 names = this.obj.getOwnPropertyNames(); 388 symbols = this.obj.getOwnPropertySymbols(); 389 } catch (err) { 390 // The above can throw when the debuggee does not subsume the object's 391 // compartment, or for some WrappedNatives like Cu.Sandbox. 392 } 393 } 394 395 const ownProperties = Object.create(null); 396 const ownSymbols = []; 397 398 for (const name of names) { 399 ownProperties[name] = propertyDescriptor(this, name, 0); 400 } 401 402 for (const sym of symbols) { 403 ownSymbols.push({ 404 name: sym.toString(), 405 descriptor: propertyDescriptor(this, sym, 0), 406 }); 407 } 408 409 return { 410 prototype: this.createValueGrip(objProto, 0), 411 ownProperties, 412 ownSymbols, 413 safeGetterValues: this._findSafeGetterValues(names, 0), 414 }; 415 } 416 417 /** 418 * Find the safe getter values for the current Debugger.Object, |this.obj|. 419 * 420 * @private 421 * @param array ownProperties 422 * The array that holds the list of known ownProperties names for 423 * |this.obj|. 424 * @param {number} depth 425 * Current depth in the generated preview object sent to the client. 426 * @param number [limit=Infinity] 427 * Optional limit of getter values to find. 428 * @return object 429 * An object that maps property names to safe getter descriptors as 430 * defined by the remote debugging protocol. 431 */ 432 _findSafeGetterValues(ownProperties, depth, limit = Infinity) { 433 const safeGetterValues = Object.create(null); 434 let obj = this.obj; 435 let level = 0, 436 currentGetterValuesCount = 0; 437 438 // Do not search safe getters in unsafe objects. 439 if (!DevToolsUtils.isSafeDebuggerObject(obj)) { 440 return safeGetterValues; 441 } 442 443 // Most objects don't have any safe getters but inherit some from their 444 // prototype. Avoid calling getOwnPropertyNames on objects that may have 445 // many properties like Array, strings or js objects. That to avoid 446 // freezing firefox when doing so. 447 if ( 448 this.className == "Object" || 449 this.className == "String" || 450 isArray(this.obj) 451 ) { 452 obj = obj.proto; 453 level++; 454 } 455 456 while (obj && DevToolsUtils.isSafeDebuggerObject(obj)) { 457 for (const name of this._findSafeGetters(obj)) { 458 // Avoid overwriting properties from prototypes closer to this.obj. Also 459 // avoid providing safeGetterValues from prototypes if property |name| 460 // is already defined as an own property. 461 if ( 462 name in safeGetterValues || 463 (obj != this.obj && ownProperties.includes(name)) 464 ) { 465 continue; 466 } 467 468 // Ignore __proto__ on Object.prototye. 469 if (!obj.proto && name == "__proto__") { 470 continue; 471 } 472 473 const desc = safeGetOwnPropertyDescriptor(obj, name); 474 if (!desc?.get) { 475 // If no getter matches the name, the cache is stale and should be cleaned up. 476 obj._safeGetters = null; 477 continue; 478 } 479 480 const getterValue = this._evaluateGetter(desc.get); 481 if (getterValue === this._evaluateGetterNoResult) { 482 continue; 483 } 484 485 // Treat an already-rejected Promise as we would a thrown exception 486 // by not including it as a safe getter value (see Bug 1477765). 487 if (isRejectedPromise(getterValue)) { 488 // Until we have a good way to handle Promise rejections through the 489 // debugger API (Bug 1478076), call `catch` when it's safe to do so. 490 const raw = getterValue.unsafeDereference(); 491 if (DevToolsUtils.isSafeJSObject(raw)) { 492 raw.catch(e => e); 493 } 494 continue; 495 } 496 497 // WebIDL attributes specified with the LenientThis extended attribute 498 // return undefined and should be ignored. 499 safeGetterValues[name] = { 500 getterValue: this.createValueGrip(getterValue, depth), 501 getterPrototypeLevel: level, 502 enumerable: desc.enumerable, 503 writable: level == 0 ? desc.writable : true, 504 }; 505 506 ++currentGetterValuesCount; 507 if (currentGetterValuesCount == limit) { 508 return safeGetterValues; 509 } 510 } 511 512 obj = obj.proto; 513 level++; 514 } 515 516 return safeGetterValues; 517 } 518 519 _evaluateGetterNoResult = Symbol(); 520 521 /** 522 * Evaluate the getter function |desc.get|. 523 * 524 * @param {object} getter 525 */ 526 _evaluateGetter(getter) { 527 const result = getter.call(this.obj); 528 if (!result || "throw" in result) { 529 return this._evaluateGetterNoResult; 530 } 531 532 let getterValue = this._evaluateGetterNoResult; 533 if ("return" in result) { 534 getterValue = result.return; 535 } else if ("yield" in result) { 536 getterValue = result.yield; 537 } 538 539 return getterValue; 540 } 541 542 /** 543 * Find the safe getters for a given Debugger.Object. Safe getters are native 544 * getters which are safe to execute. 545 * 546 * @private 547 * @param Debugger.Object object 548 * The Debugger.Object where you want to find safe getters. 549 * @return Set 550 * A Set of names of safe getters. This result is cached for each 551 * Debugger.Object. 552 */ 553 _findSafeGetters(object) { 554 if (object._safeGetters) { 555 return object._safeGetters; 556 } 557 558 const getters = new Set(); 559 560 if (!DevToolsUtils.isSafeDebuggerObject(object)) { 561 object._safeGetters = getters; 562 return getters; 563 } 564 565 let names = []; 566 try { 567 names = object.getOwnPropertyNames(); 568 } catch (ex) { 569 // Calling getOwnPropertyNames() on some wrapped native prototypes is not 570 // allowed: "cannot modify properties of a WrappedNative". See bug 952093. 571 } 572 573 for (const name of names) { 574 let desc = null; 575 try { 576 desc = object.getOwnPropertyDescriptor(name); 577 } catch (e) { 578 // Calling getOwnPropertyDescriptor on wrapped native prototypes is not 579 // allowed (bug 560072). 580 } 581 if (!desc || desc.value !== undefined || !("get" in desc)) { 582 continue; 583 } 584 585 if ( 586 DevToolsUtils.hasSafeGetter(desc) && 587 !unsafeGettersNames.includes(name) 588 ) { 589 getters.add(name); 590 } 591 } 592 593 object._safeGetters = getters; 594 return getters; 595 } 596 597 /** 598 * Handle a protocol request to provide the prototype of the object. 599 */ 600 prototype() { 601 let objProto = null; 602 if (DevToolsUtils.isSafeDebuggerObject(this.obj)) { 603 objProto = this.obj.proto; 604 } 605 return { prototype: this.createValueGrip(objProto, 0) }; 606 } 607 608 /** 609 * Handle a protocol request to provide the property descriptor of the 610 * object's specified property. 611 * 612 * @param name string 613 * The property we want the description of. 614 */ 615 property(name) { 616 if (!name) { 617 return this.throwError( 618 "missingParameter", 619 "no property name was specified" 620 ); 621 } 622 623 return { descriptor: propertyDescriptor(this, name, 0) }; 624 } 625 626 /** 627 * Handle a protocol request to provide the value of the object's 628 * specified property. 629 * 630 * Note: Since this will evaluate getters, it can trigger execution of 631 * content code and may cause side effects. This endpoint should only be used 632 * when you are confident that the side-effects will be safe, or the user 633 * is expecting the effects. 634 * 635 * @param {string} name 636 * The property we want the value of. 637 * @param {string|null} receiverId 638 * The actorId of the receiver to be used if the property is a getter. 639 * If null or invalid, the receiver will be the referent. 640 */ 641 propertyValue(name, receiverId) { 642 if (!name) { 643 return this.throwError( 644 "missingParameter", 645 "no property name was specified" 646 ); 647 } 648 649 let receiver; 650 if (receiverId) { 651 const receiverActor = this.conn.getActor(receiverId); 652 if (receiverActor) { 653 receiver = receiverActor.obj; 654 } 655 } 656 657 const value = receiver 658 ? this.obj.getProperty(name, receiver) 659 : this.obj.getProperty(name); 660 661 return { value: this._buildCompletion(value) }; 662 } 663 664 /** 665 * Handle a protocol request to evaluate a function and provide the value of 666 * the result. 667 * 668 * Note: Since this will evaluate the function, it can trigger execution of 669 * content code and may cause side effects. This endpoint should only be used 670 * when you are confident that the side-effects will be safe, or the user 671 * is expecting the effects. 672 * 673 * @param {any} context 674 * The 'this' value to call the function with. 675 * @param {Array<any>} args 676 * The array of un-decoded actor objects, or primitives. 677 */ 678 apply(context, args) { 679 if (!this.obj.callable) { 680 return this.throwError("notCallable", "debugee object is not callable"); 681 } 682 683 const debugeeContext = this._getValueFromGrip(context); 684 const debugeeArgs = args && args.map(this._getValueFromGrip, this); 685 686 const value = this.obj.apply(debugeeContext, debugeeArgs); 687 688 return { value: this._buildCompletion(value) }; 689 } 690 691 _getValueFromGrip(grip) { 692 if (typeof grip !== "object" || !grip) { 693 return grip; 694 } 695 696 if (typeof grip.actor !== "string") { 697 return this.throwError( 698 "invalidGrip", 699 "grip argument did not include actor ID" 700 ); 701 } 702 703 const actor = this.conn.getActor(grip.actor); 704 705 if (!actor) { 706 return this.throwError( 707 "unknownActor", 708 "grip actor did not match a known object" 709 ); 710 } 711 712 return actor.obj; 713 } 714 715 /** 716 * Converts a Debugger API completion value record into an equivalent 717 * object grip for use by the API. 718 * 719 * See https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/ 720 * for more specifics on the expected behavior. 721 */ 722 _buildCompletion(value) { 723 let completionGrip = null; 724 725 // .apply result will be falsy if the script being executed is terminated 726 // via the "slow script" dialog. 727 if (value) { 728 completionGrip = {}; 729 if ("return" in value) { 730 completionGrip.return = this.createValueGrip(value.return, 0); 731 } 732 if ("throw" in value) { 733 completionGrip.throw = this.createValueGrip(value.throw, 0); 734 } 735 } 736 737 return completionGrip; 738 } 739 740 /** 741 * Handle a protocol request to get the target and handler internal slots of a proxy. 742 */ 743 proxySlots() { 744 // There could be transparent security wrappers, unwrap to check if it's a proxy. 745 // However, retrieve proxyTarget and proxyHandler from `this.obj` to avoid exposing 746 // the unwrapped target and handler. 747 const unwrapped = DevToolsUtils.unwrap(this.obj); 748 if (!unwrapped || !unwrapped.isProxy) { 749 return this.throwError( 750 "objectNotProxy", 751 "'proxySlots' request is only valid for grips with a 'Proxy' class." 752 ); 753 } 754 return { 755 proxyTarget: this.createValueGrip(this.obj.proxyTarget, 0), 756 proxyHandler: this.createValueGrip(this.obj.proxyHandler, 0), 757 }; 758 } 759 760 /** 761 * Release the actor, when it isn't needed anymore. 762 * Protocol.js uses this release method to call the destroy method. 763 */ 764 release() { 765 if (this.hooks) { 766 this.hooks.customFormatterConfigDbgObj = null; 767 } 768 this._customFormatterItem = null; 769 this.obj = null; 770 this.rawObj = null; 771 this.safeRawObj = null; 772 this.threadActor = null; 773 } 774 } 775 776 exports.ObjectActor = ObjectActor; 777 778 function safeGetOwnPropertyDescriptor(obj, name) { 779 let desc = null; 780 try { 781 desc = obj.getOwnPropertyDescriptor(name); 782 } catch (ex) { 783 // The above can throw if the cache becomes stale. 784 } 785 return desc; 786 } 787 788 /** 789 * Check if the value is rejected promise 790 * 791 * @param {object} getterValue 792 * @returns {boolean} true if the value is rejected promise, false otherwise. 793 */ 794 function isRejectedPromise(getterValue) { 795 return ( 796 getterValue && 797 getterValue.class == "Promise" && 798 getterValue.promiseState == "rejected" 799 ); 800 }