property-iterator.js (22113B)
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.js"); 8 const { 9 propertyIteratorSpec, 10 } = require("resource://devtools/shared/specs/property-iterator.js"); 11 12 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 13 loader.lazyRequireGetter( 14 this, 15 "ObjectUtils", 16 "resource://devtools/server/actors/object/utils.js" 17 ); 18 loader.lazyRequireGetter( 19 this, 20 "propertyDescriptor", 21 "resource://devtools/server/actors/object/property-descriptor.js", 22 true 23 ); 24 25 /** 26 * Creates an actor to iterate over an object's property names and values. 27 * 28 * @param objectActor ObjectActor 29 * The object actor. 30 * @param options Object 31 * A dictionary object with various boolean attributes: 32 * - enumEntries Boolean 33 * If true, enumerates the entries of a Map or Set object 34 * instead of enumerating properties. 35 * - ignoreIndexedProperties Boolean 36 * If true, filters out Array items. 37 * e.g. properties names between `0` and `object.length`. 38 * - ignoreNonIndexedProperties Boolean 39 * If true, filters out items that aren't array items 40 * e.g. properties names that are not a number between `0` 41 * and `object.length`. 42 * - sort Boolean 43 * If true, the iterator will sort the properties by name 44 * before dispatching them. 45 * - query String 46 * If non-empty, will filter the properties by names and values 47 * containing this query string. The match is not case-sensitive. 48 * Regarding value filtering it just compare to the stringification 49 * of the property value. 50 */ 51 class PropertyIteratorActor extends Actor { 52 constructor(objectActor, options, conn) { 53 super(conn, propertyIteratorSpec); 54 if (!DevToolsUtils.isSafeDebuggerObject(objectActor.obj)) { 55 this.iterator = { 56 size: 0, 57 propertyName: _index => undefined, 58 propertyDescription: _index => undefined, 59 }; 60 } else if (options.enumEntries) { 61 const cls = objectActor.className; 62 if (cls == "Map") { 63 this.iterator = enumMapEntries(objectActor, 0); 64 } else if (cls == "WeakMap") { 65 this.iterator = enumWeakMapEntries(objectActor, 0); 66 } else if (cls == "Set") { 67 this.iterator = enumSetEntries(objectActor, 0); 68 } else if (cls == "WeakSet") { 69 this.iterator = enumWeakSetEntries(objectActor, 0); 70 } else if (cls == "Storage") { 71 this.iterator = enumStorageEntries(objectActor, 0); 72 } else if (cls == "URLSearchParams") { 73 this.iterator = enumURLSearchParamsEntries(objectActor, 0); 74 } else if (cls == "Headers") { 75 this.iterator = enumHeadersEntries(objectActor, 0); 76 } else if (cls == "HighlightRegistry") { 77 this.iterator = enumHighlightRegistryEntries(objectActor, 0); 78 } else if (cls == "FormData") { 79 this.iterator = enumFormDataEntries(objectActor, 0); 80 } else if (cls == "MIDIInputMap") { 81 this.iterator = enumMidiInputMapEntries(objectActor, 0); 82 } else if (cls == "MIDIOutputMap") { 83 this.iterator = enumMidiOutputMapEntries(objectActor, 0); 84 } else if (cls == "CustomStateSet") { 85 this.iterator = enumCustomStateSetEntries(objectActor, 0); 86 } else { 87 throw new Error( 88 "Unsupported class to enumerate entries from: " + cls 89 ); 90 } 91 } else if ( 92 ObjectUtils.isArray(objectActor.obj) && 93 options.ignoreNonIndexedProperties && 94 !options.query 95 ) { 96 this.iterator = enumArrayProperties(objectActor, options, 0); 97 } else { 98 this.iterator = enumObjectProperties(objectActor, options, 0); 99 } 100 } 101 102 form() { 103 return { 104 type: this.typeName, 105 actor: this.actorID, 106 count: this.iterator.size, 107 }; 108 } 109 110 names({ indexes }) { 111 const list = []; 112 for (const idx of indexes) { 113 list.push(this.iterator.propertyName(idx)); 114 } 115 return indexes; 116 } 117 118 slice({ start, count }) { 119 const ownProperties = Object.create(null); 120 for (let i = start, m = start + count; i < m; i++) { 121 const name = this.iterator.propertyName(i); 122 ownProperties[name] = this.iterator.propertyDescription(i); 123 } 124 125 return { 126 ownProperties, 127 }; 128 } 129 130 all() { 131 return this.slice({ start: 0, count: this.iterator.size }); 132 } 133 } 134 135 function waiveXrays(obj) { 136 return isWorker ? obj : Cu.waiveXrays(obj); 137 } 138 139 function unwaiveXrays(obj) { 140 return isWorker ? obj : Cu.unwaiveXrays(obj); 141 } 142 143 /** 144 * Helper function to create a grip from a Map/Set entry 145 */ 146 function gripFromEntry(objectActor, entry, depth) { 147 entry = unwaiveXrays(entry); 148 return objectActor.createValueGrip( 149 ObjectUtils.makeDebuggeeValueIfNeeded(objectActor.obj, entry), 150 depth 151 ); 152 } 153 154 function enumArrayProperties(objectActor, options, depth) { 155 return { 156 size: ObjectUtils.getArrayLength(objectActor.obj), 157 propertyName(index) { 158 return index; 159 }, 160 propertyDescription(index) { 161 return propertyDescriptor(objectActor, index, depth); 162 }, 163 }; 164 } 165 166 function enumObjectProperties(objectActor, options, depth) { 167 let names = []; 168 try { 169 names = objectActor.obj.getOwnPropertyNames(); 170 } catch (ex) { 171 // Calling getOwnPropertyNames() on some wrapped native prototypes is not 172 // allowed: "cannot modify properties of a WrappedNative". See bug 952093. 173 } 174 175 if (options.ignoreNonIndexedProperties || options.ignoreIndexedProperties) { 176 const length = DevToolsUtils.getProperty(objectActor.obj, "length"); 177 let sliceIndex; 178 179 const isLengthTrustworthy = 180 isUint32(length) && 181 (!length || ObjectUtils.isArrayIndex(names[length - 1])) && 182 !ObjectUtils.isArrayIndex(names[length]); 183 184 if (!isLengthTrustworthy) { 185 // The length property may not reflect what the object looks like, let's find 186 // where indexed properties end. 187 188 if (!ObjectUtils.isArrayIndex(names[0])) { 189 // If the first item is not a number, this means there is no indexed properties 190 // in this object. 191 sliceIndex = 0; 192 } else { 193 sliceIndex = names.length; 194 while (sliceIndex > 0) { 195 if (ObjectUtils.isArrayIndex(names[sliceIndex - 1])) { 196 break; 197 } 198 sliceIndex--; 199 } 200 } 201 } else { 202 sliceIndex = length; 203 } 204 205 // It appears that getOwnPropertyNames always returns indexed properties 206 // first, so we can safely slice `names` for/against indexed properties. 207 // We do such clever operation to optimize very large array inspection. 208 if (options.ignoreIndexedProperties) { 209 // Keep items after `sliceIndex` index 210 names = names.slice(sliceIndex); 211 } else if (options.ignoreNonIndexedProperties) { 212 // Keep `sliceIndex` first items 213 names.length = sliceIndex; 214 } 215 } 216 217 const safeGetterValues = objectActor._findSafeGetterValues(names, depth); 218 const safeGetterNames = Object.keys(safeGetterValues); 219 // Merge the safe getter values into the existing properties list. 220 for (const name of safeGetterNames) { 221 if (!names.includes(name)) { 222 names.push(name); 223 } 224 } 225 226 if (options.query) { 227 let { query } = options; 228 query = query.toLowerCase(); 229 names = names.filter(name => { 230 // Filter on attribute names 231 if (name.toLowerCase().includes(query)) { 232 return true; 233 } 234 // and then on attribute values 235 let desc; 236 try { 237 desc = objectActor.obj.getOwnPropertyDescriptor(name); 238 } catch (e) { 239 // Calling getOwnPropertyDescriptor on wrapped native prototypes is not 240 // allowed (bug 560072). 241 } 242 if (desc?.value && String(desc.value).includes(query)) { 243 return true; 244 } 245 return false; 246 }); 247 } 248 249 if (options.sort) { 250 names.sort(); 251 } 252 253 return { 254 size: names.length, 255 propertyName(index) { 256 return names[index]; 257 }, 258 propertyDescription(index) { 259 const name = names[index]; 260 let desc = propertyDescriptor(objectActor, name, depth); 261 if (!desc) { 262 desc = safeGetterValues[name]; 263 } else if (name in safeGetterValues) { 264 // Merge the safe getter values into the existing properties list. 265 const { getterValue, getterPrototypeLevel } = safeGetterValues[name]; 266 desc.getterValue = getterValue; 267 desc.getterPrototypeLevel = getterPrototypeLevel; 268 } 269 return desc; 270 }, 271 }; 272 } 273 274 function getMapEntries(objectActor) { 275 const { obj, rawObj } = objectActor; 276 // Iterating over a Map via .entries goes through various intermediate 277 // objects - an Iterator object, then a 2-element Array object, then the 278 // actual values we care about. We don't have Xrays to Iterator objects, 279 // so we get Opaque wrappers for them. And even though we have Xrays to 280 // Arrays, the semantics often deny access to the entires based on the 281 // nature of the values. So we need waive Xrays for the iterator object 282 // and the tupes, and then re-apply them on the underlying values until 283 // we fix bug 1023984. 284 // 285 // Even then though, we might want to continue waiving Xrays here for the 286 // same reason we do so for Arrays above - this filtering behavior is likely 287 // to be more confusing than beneficial in the case of Object previews. 288 const iterator = obj.makeDebuggeeValue( 289 waiveXrays(Map.prototype.keys.call(rawObj)) 290 ); 291 return [...DevToolsUtils.makeDebuggeeIterator(iterator)].map(k => { 292 const key = waiveXrays(ObjectUtils.unwrapDebuggeeValue(k)); 293 const value = Map.prototype.get.call(rawObj, key); 294 return [key, value]; 295 }); 296 } 297 298 function enumMapEntries(objectActor, depth) { 299 const entries = getMapEntries(objectActor); 300 301 return { 302 *[Symbol.iterator]() { 303 for (const [key, value] of entries) { 304 yield [key, value].map(val => gripFromEntry(objectActor, val, depth)); 305 } 306 }, 307 size: entries.length, 308 propertyName(index) { 309 return index; 310 }, 311 propertyDescription(index) { 312 const [key, val] = entries[index]; 313 return { 314 enumerable: true, 315 value: { 316 type: "mapEntry", 317 preview: { 318 key: gripFromEntry(objectActor, key, depth), 319 value: gripFromEntry(objectActor, val, depth), 320 }, 321 }, 322 }; 323 }, 324 }; 325 } 326 327 function enumStorageEntries(objectActor, depth) { 328 // Iterating over local / sessionStorage entries goes through various 329 // intermediate objects - an Iterator object, then a 2-element Array object, 330 // then the actual values we care about. We don't have Xrays to Iterator 331 // objects, so we get Opaque wrappers for them. 332 const { rawObj } = objectActor; 333 const keys = []; 334 for (let i = 0; i < rawObj.length; i++) { 335 keys.push(rawObj.key(i)); 336 } 337 return { 338 *[Symbol.iterator]() { 339 for (const key of keys) { 340 const value = rawObj.getItem(key); 341 yield [key, value].map(val => gripFromEntry(objectActor, val, depth)); 342 } 343 }, 344 size: keys.length, 345 propertyName(index) { 346 return index; 347 }, 348 propertyDescription(index) { 349 const key = keys[index]; 350 const val = rawObj.getItem(key); 351 return { 352 enumerable: true, 353 value: { 354 type: "storageEntry", 355 preview: { 356 key: gripFromEntry(objectActor, key, depth), 357 value: gripFromEntry(objectActor, val, depth), 358 }, 359 }, 360 }; 361 }, 362 }; 363 } 364 365 function enumURLSearchParamsEntries(objectActor, depth) { 366 const entries = [...waiveXrays(URLSearchParams.prototype.entries.call(objectActor.rawObj))]; 367 368 return { 369 *[Symbol.iterator]() { 370 for (const [key, value] of entries) { 371 yield [key, value]; 372 } 373 }, 374 size: entries.length, 375 propertyName(index) { 376 // UrlSearchParams entries can have the same key multiple times (e.g. `?a=1&a=2`), 377 // so let's return the index as a name to be able to display them properly in the client. 378 return index; 379 }, 380 propertyDescription(index) { 381 const [key, value] = entries[index]; 382 383 return { 384 enumerable: true, 385 value: { 386 type: "urlSearchParamsEntry", 387 preview: { 388 key: gripFromEntry(objectActor, key, depth), 389 value: gripFromEntry(objectActor, value, depth), 390 }, 391 }, 392 }; 393 }, 394 }; 395 } 396 397 function enumFormDataEntries(objectActor, depth) { 398 const entries = [...waiveXrays(FormData.prototype.entries.call(objectActor.rawObj))]; 399 400 return { 401 *[Symbol.iterator]() { 402 for (const [key, value] of entries) { 403 yield [key, value]; 404 } 405 }, 406 size: entries.length, 407 propertyName(index) { 408 return index; 409 }, 410 propertyDescription(index) { 411 const [key, value] = entries[index]; 412 413 return { 414 enumerable: true, 415 value: { 416 type: "formDataEntry", 417 preview: { 418 key: gripFromEntry(objectActor, key, depth), 419 value: gripFromEntry(objectActor, value, depth), 420 }, 421 }, 422 }; 423 }, 424 }; 425 } 426 427 function enumHeadersEntries(objectActor, depth) { 428 const entries = [...waiveXrays(Headers.prototype.entries.call(objectActor.rawObj))]; 429 430 return { 431 *[Symbol.iterator]() { 432 for (const [key, value] of entries) { 433 yield [key, value]; 434 } 435 }, 436 size: entries.length, 437 propertyName(index) { 438 return entries[index][0]; 439 }, 440 propertyDescription(index) { 441 return { 442 enumerable: true, 443 value: gripFromEntry(objectActor, entries[index][1], depth), 444 }; 445 }, 446 }; 447 } 448 449 function enumHighlightRegistryEntries(objectActor, depth) { 450 const entriesFuncDbgObj = objectActor.obj.getProperty("entries").return; 451 const entriesDbgObj = entriesFuncDbgObj ? entriesFuncDbgObj.call(objectActor.obj).return : null; 452 const entries = entriesDbgObj 453 ? [...waiveXrays( entriesDbgObj.unsafeDereference())] 454 : []; 455 456 return { 457 *[Symbol.iterator]() { 458 for (const [key, value] of entries) { 459 yield [key, gripFromEntry(objectActor, value, depth)]; 460 } 461 }, 462 size: entries.length, 463 propertyName(index) { 464 return index; 465 }, 466 propertyDescription(index) { 467 const [key, value] = entries[index]; 468 return { 469 enumerable: true, 470 value: { 471 type: "highlightRegistryEntry", 472 preview: { 473 key, 474 value: gripFromEntry(objectActor, value, depth), 475 }, 476 }, 477 }; 478 }, 479 }; 480 } 481 482 function enumMidiInputMapEntries(objectActor, depth) { 483 // We need to waive `rawObj` as we can't get the iterator from the Xray for MapLike (See Bug 1173651). 484 // We also need to waive Xrays on the result of the call to `entries` as we don't have 485 // Xrays to Iterator objects (see Bug 1023984) 486 const entries = Array.from( 487 waiveXrays(MIDIInputMap.prototype.entries.call(waiveXrays(objectActor.rawObj))) 488 ); 489 490 return { 491 *[Symbol.iterator]() { 492 for (const [key, value] of entries) { 493 yield [key, gripFromEntry(objectActor, value, depth)]; 494 } 495 }, 496 size: entries.length, 497 propertyName(index) { 498 return entries[index][0]; 499 }, 500 propertyDescription(index) { 501 return { 502 enumerable: true, 503 value: gripFromEntry(objectActor, entries[index][1], depth), 504 }; 505 }, 506 }; 507 } 508 509 function enumMidiOutputMapEntries(objectActor, depth) { 510 // We need to waive `rawObj` as we can't get the iterator from the Xray for MapLike (See Bug 1173651). 511 // We also need to waive Xrays on the result of the call to `entries` as we don't have 512 // Xrays to Iterator objects (see Bug 1023984) 513 const entries = Array.from( 514 waiveXrays(MIDIOutputMap.prototype.entries.call(waiveXrays(objectActor.rawObj))) 515 ); 516 517 return { 518 *[Symbol.iterator]() { 519 for (const [key, value] of entries) { 520 yield [key, gripFromEntry(objectActor, value, depth)]; 521 } 522 }, 523 size: entries.length, 524 propertyName(index) { 525 return entries[index][0]; 526 }, 527 propertyDescription(index) { 528 return { 529 enumerable: true, 530 value: gripFromEntry(objectActor, entries[index][1], depth), 531 }; 532 }, 533 }; 534 } 535 536 function getWeakMapEntries(rawObj) { 537 // We currently lack XrayWrappers for WeakMap, so when we iterate over 538 // the values, the temporary iterator objects get created in the target 539 // compartment. However, we _do_ have Xrays to Object now, so we end up 540 // Xraying those temporary objects, and filtering access to |it.value| 541 // based on whether or not it's Xrayable and/or callable, which breaks 542 // the for/of iteration. 543 // 544 // This code is designed to handle untrusted objects, so we can safely 545 // waive Xrays on the iterable, and relying on the Debugger machinery to 546 // make sure we handle the resulting objects carefully. 547 const keys = waiveXrays(ChromeUtils.nondeterministicGetWeakMapKeys(rawObj)); 548 549 return keys.map(k => [k, WeakMap.prototype.get.call(rawObj, k)]); 550 } 551 552 function enumWeakMapEntries(objectActor, depth) { 553 const entries = getWeakMapEntries(objectActor.rawObj); 554 555 return { 556 *[Symbol.iterator]() { 557 for (let i = 0; i < entries.length; i++) { 558 yield entries[i].map(val => gripFromEntry(objectActor, val, depth)); 559 } 560 }, 561 size: entries.length, 562 propertyName(index) { 563 return index; 564 }, 565 propertyDescription(index) { 566 const [key, val] = entries[index]; 567 return { 568 enumerable: true, 569 value: { 570 type: "mapEntry", 571 preview: { 572 key: gripFromEntry(objectActor, key, depth), 573 value: gripFromEntry(objectActor, val, depth), 574 }, 575 }, 576 }; 577 }, 578 }; 579 } 580 581 function getSetValues(objectActor) { 582 // We currently lack XrayWrappers for Set, so when we iterate over 583 // the values, the temporary iterator objects get created in the target 584 // compartment. However, we _do_ have Xrays to Object now, so we end up 585 // Xraying those temporary objects, and filtering access to |it.value| 586 // based on whether or not it's Xrayable and/or callable, which breaks 587 // the for/of iteration. 588 // 589 // This code is designed to handle untrusted objects, so we can safely 590 // waive Xrays on the iterable, and relying on the Debugger machinery to 591 // make sure we handle the resulting objects carefully. 592 const iterator = objectActor.obj.makeDebuggeeValue( 593 waiveXrays(Set.prototype.values.call(objectActor.rawObj)) 594 ); 595 return [...DevToolsUtils.makeDebuggeeIterator(iterator)]; 596 } 597 598 function enumSetEntries(objectActor, depth) { 599 const values = getSetValues(objectActor).map(v => 600 waiveXrays(ObjectUtils.unwrapDebuggeeValue(v)) 601 ); 602 603 return { 604 *[Symbol.iterator]() { 605 for (const item of values) { 606 yield gripFromEntry(objectActor, item, depth); 607 } 608 }, 609 size: values.length, 610 propertyName(index) { 611 return index; 612 }, 613 propertyDescription(index) { 614 const val = values[index]; 615 return { 616 enumerable: true, 617 value: gripFromEntry(objectActor, val, depth), 618 }; 619 }, 620 }; 621 } 622 623 function getWeakSetEntries(rawObj) { 624 // We currently lack XrayWrappers for WeakSet, so when we iterate over 625 // the values, the temporary iterator objects get created in the target 626 // compartment. However, we _do_ have Xrays to Object now, so we end up 627 // Xraying those temporary objects, and filtering access to |it.value| 628 // based on whether or not it's Xrayable and/or callable, which breaks 629 // the for/of iteration. 630 // 631 // This code is designed to handle untrusted objects, so we can safely 632 // waive Xrays on the iterable, and relying on the Debugger machinery to 633 // make sure we handle the resulting objects carefully. 634 return waiveXrays(ChromeUtils.nondeterministicGetWeakSetKeys(rawObj)); 635 } 636 637 function enumWeakSetEntries(objectActor, depth) { 638 const keys = getWeakSetEntries(objectActor.rawObj); 639 640 return { 641 *[Symbol.iterator]() { 642 for (const item of keys) { 643 yield gripFromEntry(objectActor, item, depth); 644 } 645 }, 646 size: keys.length, 647 propertyName(index) { 648 return index; 649 }, 650 propertyDescription(index) { 651 const val = keys[index]; 652 return { 653 enumerable: true, 654 value: gripFromEntry(objectActor, val, depth), 655 }; 656 }, 657 }; 658 } 659 660 function enumCustomStateSetEntries(objectActor, depth) { 661 const { rawObj } = objectActor; 662 // We need to waive `rawObj` as we can't get the iterator from the Xray for SetLike (See Bug 1173651). 663 // We also need to waive Xrays on the result of the call to `values` as we don't have 664 // Xrays to Iterator objects (see Bug 1023984) 665 const values = Array.from( 666 // eslint-disable-next-line no-undef 667 waiveXrays(CustomStateSet.prototype.values.call(waiveXrays(rawObj))) 668 ); 669 670 return { 671 *[Symbol.iterator]() { 672 for (const item of values) { 673 yield gripFromEntry(objectActor, item, depth); 674 } 675 }, 676 size: values.length, 677 propertyName(index) { 678 return index; 679 }, 680 propertyDescription(index) { 681 const val = values[index]; 682 return { 683 enumerable: true, 684 value: gripFromEntry(objectActor, val, depth), 685 }; 686 }, 687 }; 688 } 689 690 /** 691 * Returns true if the parameter can be stored as a 32-bit unsigned integer. 692 * If so, it will be suitable for use as the length of an array object. 693 * 694 * @param num Number 695 * The number to test. 696 * @return Boolean 697 */ 698 function isUint32(num) { 699 return num >>> 0 === num; 700 } 701 702 module.exports = { 703 PropertyIteratorActor, 704 enumCustomStateSetEntries, 705 enumMapEntries, 706 enumMidiInputMapEntries, 707 enumMidiOutputMapEntries, 708 enumSetEntries, 709 enumURLSearchParamsEntries, 710 enumFormDataEntries, 711 enumHeadersEntries, 712 enumHighlightRegistryEntries, 713 enumWeakMapEntries, 714 enumWeakSetEntries, 715 };