object.js (14258B)
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 { objectSpec } = require("resource://devtools/shared/specs/object.js"); 8 const { 9 FrontClassWithSpec, 10 registerFront, 11 } = require("resource://devtools/shared/protocol.js"); 12 const { 13 LongStringFront, 14 } = require("resource://devtools/client/fronts/string.js"); 15 16 const SUPPORT_ENUM_ENTRIES_SET = new Set([ 17 "CustomStateSet", 18 "FormData", 19 "Headers", 20 "HighlightRegistry", 21 "Map", 22 "MIDIInputMap", 23 "MIDIOutputMap", 24 "Set", 25 "Storage", 26 "URLSearchParams", 27 "WeakMap", 28 "WeakSet", 29 ]); 30 31 /** 32 * A ObjectFront is used as a front end for the ObjectActor that is 33 * created on the server, hiding implementation details. 34 */ 35 class ObjectFront extends FrontClassWithSpec(objectSpec) { 36 constructor(conn = null, targetFront = null, parentFront = null, data) { 37 if (!parentFront) { 38 throw new Error("ObjectFront require a parent front"); 39 } 40 41 super(conn, targetFront, parentFront); 42 43 this._grip = data; 44 this.actorID = this._grip.actor; 45 this.valid = true; 46 47 parentFront.manage(this); 48 } 49 50 form(data) { 51 this.actorID = data.actor; 52 this._grip = data; 53 } 54 55 skipDestroy() { 56 // Object fronts are simple fronts, they don't need to be cleaned up on 57 // toolbox destroy. `conn` is a DebuggerClient instance, check the 58 // `isToolboxDestroy` flag to skip the destroy. 59 return this.conn && this.conn.isToolboxDestroy; 60 } 61 62 getGrip() { 63 return this._grip; 64 } 65 66 get isFrozen() { 67 return this._grip.frozen; 68 } 69 70 get isSealed() { 71 return this._grip.sealed; 72 } 73 74 get isExtensible() { 75 return this._grip.extensible; 76 } 77 78 /** 79 * Request the prototype and own properties of the object. 80 */ 81 async getPrototypeAndProperties() { 82 const result = await super.prototypeAndProperties(); 83 84 if (result.prototype) { 85 result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this); 86 } 87 88 // The result packet can have multiple properties that hold grips which we may need 89 // to turn into fronts. 90 const gripKeys = ["value", "getterValue", "get", "set"]; 91 92 if (result.ownProperties) { 93 Object.entries(result.ownProperties).forEach(([key, descriptor]) => { 94 if (descriptor) { 95 for (const gripKey of gripKeys) { 96 if (descriptor.hasOwnProperty(gripKey)) { 97 result.ownProperties[key][gripKey] = getAdHocFrontOrPrimitiveGrip( 98 descriptor[gripKey], 99 this 100 ); 101 } 102 } 103 } 104 }); 105 } 106 107 if (result.safeGetterValues) { 108 Object.entries(result.safeGetterValues).forEach(([key, descriptor]) => { 109 if (descriptor) { 110 for (const gripKey of gripKeys) { 111 if (descriptor.hasOwnProperty(gripKey)) { 112 result.safeGetterValues[key][gripKey] = 113 getAdHocFrontOrPrimitiveGrip(descriptor[gripKey], this); 114 } 115 } 116 } 117 }); 118 } 119 120 if (result.ownSymbols) { 121 result.ownSymbols.forEach((descriptor, i, arr) => { 122 if (descriptor) { 123 for (const gripKey of gripKeys) { 124 if (descriptor.hasOwnProperty(gripKey)) { 125 arr[i][gripKey] = getAdHocFrontOrPrimitiveGrip( 126 descriptor[gripKey], 127 this 128 ); 129 } 130 } 131 } 132 }); 133 } 134 135 return result; 136 } 137 138 /** 139 * Request a PropertyIteratorFront instance to ease listing 140 * properties for this object. 141 * 142 * @param options Object 143 * A dictionary object with various boolean attributes: 144 * - ignoreIndexedProperties Boolean 145 * If true, filters out Array items. 146 * e.g. properties names between `0` and `object.length`. 147 * - ignoreNonIndexedProperties Boolean 148 * If true, filters out items that aren't array items 149 * e.g. properties names that are not a number between `0` 150 * and `object.length`. 151 * - sort Boolean 152 * If true, the iterator will sort the properties by name 153 * before dispatching them. 154 */ 155 enumProperties(options) { 156 return super.enumProperties(options); 157 } 158 159 /** 160 * Request a PropertyIteratorFront instance to enumerate entries in a 161 * Map/Set-like object. 162 */ 163 enumEntries() { 164 if (!SUPPORT_ENUM_ENTRIES_SET.has(this._grip.class)) { 165 console.error( 166 `enumEntries can't be called for "${ 167 this._grip.class 168 }" grips. Supported grips are: ${[...SUPPORT_ENUM_ENTRIES_SET].join( 169 ", " 170 )}.` 171 ); 172 return null; 173 } 174 return super.enumEntries(); 175 } 176 177 /** 178 * Request a SymbolIteratorFront instance to enumerate symbols in an object. 179 */ 180 enumSymbols() { 181 if (this._grip.type !== "object") { 182 console.error("enumSymbols is only valid for objects grips."); 183 return null; 184 } 185 return super.enumSymbols(); 186 } 187 188 /** 189 * Request the property descriptor of the object's specified property. 190 * 191 * @param name string The name of the requested property. 192 */ 193 getProperty(name) { 194 return super.property(name); 195 } 196 197 /** 198 * Request the value of the object's specified property. 199 * 200 * @param name string The name of the requested property. 201 * @param receiverId string|null The actorId of the receiver to be used for getters. 202 */ 203 async getPropertyValue(name, receiverId) { 204 const response = await super.propertyValue(name, receiverId); 205 206 if (response.value) { 207 const { value } = response; 208 if (value.return) { 209 response.value.return = getAdHocFrontOrPrimitiveGrip( 210 value.return, 211 this 212 ); 213 } 214 215 if (value.throw) { 216 response.value.throw = getAdHocFrontOrPrimitiveGrip(value.throw, this); 217 } 218 } 219 return response; 220 } 221 222 /** 223 * Get the body of a custom formatted object. 224 */ 225 async customFormatterBody() { 226 const result = await super.customFormatterBody(); 227 228 if (!result?.customFormatterBody) { 229 return result; 230 } 231 232 const createFrontsInJsonMl = item => { 233 if (Array.isArray(item)) { 234 return item.map(i => createFrontsInJsonMl(i)); 235 } 236 return getAdHocFrontOrPrimitiveGrip(item, this); 237 }; 238 239 result.customFormatterBody = createFrontsInJsonMl( 240 result.customFormatterBody 241 ); 242 243 return result; 244 } 245 246 /** 247 * Request the prototype of the object. 248 */ 249 async getPrototype() { 250 const result = await super.prototype(); 251 252 if (!result.prototype) { 253 return result; 254 } 255 256 result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this); 257 258 return result; 259 } 260 261 /** 262 * Request the state of a promise. 263 */ 264 async getPromiseState() { 265 if (this._grip.class !== "Promise") { 266 console.error("getPromiseState is only valid for promise grips."); 267 return null; 268 } 269 270 let response, promiseState; 271 try { 272 response = await super.promiseState(); 273 promiseState = response.promiseState; 274 } catch (error) { 275 // @backward-compat { version 85 } On older server, the promiseState request didn't 276 // didn't exist (bug 1552648). The promise state was directly included in the grip. 277 if (error.message.includes("unrecognizedPacketType")) { 278 promiseState = this._grip.promiseState; 279 response = { promiseState }; 280 } else { 281 throw error; 282 } 283 } 284 285 const { value, reason } = promiseState; 286 287 if (value) { 288 promiseState.value = getAdHocFrontOrPrimitiveGrip(value, this); 289 } 290 291 if (reason) { 292 promiseState.reason = getAdHocFrontOrPrimitiveGrip(reason, this); 293 } 294 295 return response; 296 } 297 298 /** 299 * Request the target and handler internal slots of a proxy. 300 */ 301 async getProxySlots() { 302 if (this._grip.class !== "Proxy") { 303 console.error("getProxySlots is only valid for proxy grips."); 304 return null; 305 } 306 307 const response = await super.proxySlots(); 308 const { proxyHandler, proxyTarget } = response; 309 310 if (proxyHandler) { 311 response.proxyHandler = getAdHocFrontOrPrimitiveGrip(proxyHandler, this); 312 } 313 314 if (proxyTarget) { 315 response.proxyTarget = getAdHocFrontOrPrimitiveGrip(proxyTarget, this); 316 } 317 318 return response; 319 } 320 321 get isSyntaxError() { 322 return this._grip.preview && this._grip.preview.name == "SyntaxError"; 323 } 324 } 325 326 /** 327 * When we are asking the server for the value of a given variable, we might get different 328 * type of objects: 329 * - a primitive (string, number, null, false, boolean) 330 * - a long string 331 * - an "object" (i.e. not primitive nor long string) 332 * 333 * Each of those type need a different front, or none: 334 * - a primitive does not allow further interaction with the server, so we don't need 335 * to have a dedicated front. 336 * - a long string needs a longStringFront to be able to retrieve the full string. 337 * - an object need an objectFront to retrieve properties, symbols and prototype. 338 * 339 * In the case an ObjectFront is created, we also check if the object has properties 340 * that should be turned into fronts as well. 341 * 342 * @param {string | number | object} options: The packet returned by the server. 343 * @param {Front} parentFront 344 * 345 * @returns {number | string | object | LongStringFront | ObjectFront} 346 */ 347 function getAdHocFrontOrPrimitiveGrip(packet, parentFront) { 348 // We only want to try to create a front when it makes sense, i.e when it has an 349 // actorID, unless: 350 // - it's a Symbol (See Bug 1600299) 351 // - it's a mapEntry (the preview.key and preview.value properties can hold actors) 352 // - it's a highlightRegistryEntry (the preview.value properties can hold actors) 353 // - or it is already a front (happens when we are using the legacy listeners in the ResourceCommand) 354 const isPacketAnObject = packet && typeof packet === "object"; 355 const isFront = !!packet?.typeName; 356 if ( 357 !isPacketAnObject || 358 packet.type == "symbol" || 359 (packet.type !== "mapEntry" && 360 packet.type !== "highlightRegistryEntry" && 361 !packet.actor) || 362 isFront 363 ) { 364 return packet; 365 } 366 367 const { conn } = parentFront; 368 // If the parent front is a target, consider it as the target to use for all objects 369 const targetFront = parentFront.isTargetFront 370 ? parentFront 371 : parentFront.targetFront; 372 373 // We may have already created a front for this object actor since some actor (e.g. the 374 // thread actor) cache the object actors they create. 375 const existingFront = conn.getFrontByID(packet.actor); 376 if (existingFront) { 377 // This methods replicates Protocol.js logic when we receive an actor "form" (here `packet`): 378 // https://searchfox.org/mozilla-central/rev/aecbd5cdd28a09e11872bc829d9e6e4b943e6e49/devtools/shared/protocol/types.js#346 379 // We notify the Object Front about the new "form" so that it can update itself 380 // with latest data provided by the server. 381 // This will help ensure that the object previews get updated. 382 existingFront.form(packet); 383 384 // The `packet` may contain nested actor forms which should be converted into Fronts. 385 createChildFronts(existingFront, packet); 386 387 return existingFront; 388 } 389 390 const { type } = packet; 391 392 if (type === "longString") { 393 const longStringFront = new LongStringFront(conn, targetFront, parentFront); 394 longStringFront.form(packet); 395 parentFront.manage(longStringFront); 396 return longStringFront; 397 } 398 399 if ( 400 (type === "mapEntry" || type === "highlightRegistryEntry") && 401 packet.preview 402 ) { 403 const { key, value } = packet.preview; 404 packet.preview.key = getAdHocFrontOrPrimitiveGrip( 405 key, 406 parentFront, 407 targetFront 408 ); 409 packet.preview.value = getAdHocFrontOrPrimitiveGrip( 410 value, 411 parentFront, 412 targetFront 413 ); 414 return packet; 415 } 416 417 const objectFront = new ObjectFront(conn, targetFront, parentFront, packet); 418 createChildFronts(objectFront, packet); 419 return objectFront; 420 } 421 422 /** 423 * Create child fronts of the passed object front given a packet. Those child fronts are 424 * usually mapping actors of the packet sub-properties (preview items, promise fullfilled 425 * values, …). 426 * 427 * @param {ObjectFront} objectFront 428 * @param {string | number | object} packet: The packet returned by the server 429 */ 430 function createChildFronts(objectFront, packet) { 431 if (packet.preview) { 432 const { message, entries } = packet.preview; 433 434 // The message could be a longString. 435 if (packet.preview.message) { 436 packet.preview.message = getAdHocFrontOrPrimitiveGrip( 437 message, 438 objectFront 439 ); 440 } 441 442 // Handle Map/WeakMap preview entries (the preview might be directly used if has all the 443 // items needed, i.e. if the Map has less than 10 items). 444 if (entries && Array.isArray(entries)) { 445 packet.preview.entries = entries.map(([key, value]) => [ 446 getAdHocFrontOrPrimitiveGrip(key, objectFront), 447 getAdHocFrontOrPrimitiveGrip(value, objectFront), 448 ]); 449 } 450 } 451 452 if (packet && typeof packet.ownProperties === "object") { 453 for (const [name, descriptor] of Object.entries(packet.ownProperties)) { 454 // The descriptor can have multiple properties that hold grips which we may need 455 // to turn into fronts. 456 const gripKeys = ["value", "getterValue", "get", "set"]; 457 for (const key of gripKeys) { 458 if ( 459 descriptor && 460 typeof descriptor === "object" && 461 descriptor.hasOwnProperty(key) 462 ) { 463 packet.ownProperties[name][key] = getAdHocFrontOrPrimitiveGrip( 464 descriptor[key], 465 objectFront 466 ); 467 } 468 } 469 } 470 } 471 472 // Handle custom formatters 473 if (packet && packet.useCustomFormatter && Array.isArray(packet.header)) { 474 const createFrontsInJsonMl = item => { 475 if (Array.isArray(item)) { 476 return item.map(i => createFrontsInJsonMl(i)); 477 } 478 return getAdHocFrontOrPrimitiveGrip(item, objectFront); 479 }; 480 481 packet.header = createFrontsInJsonMl(packet.header); 482 } 483 } 484 485 registerFront(ObjectFront); 486 487 exports.ObjectFront = ObjectFront; 488 exports.getAdHocFrontOrPrimitiveGrip = getAdHocFrontOrPrimitiveGrip;