diff options
Diffstat (limited to 'devtools/client/fronts/object.js')
-rw-r--r-- | devtools/client/fronts/object.js | 465 |
1 files changed, 465 insertions, 0 deletions
diff --git a/devtools/client/fronts/object.js b/devtools/client/fronts/object.js new file mode 100644 index 0000000000..fa1f1d8ba1 --- /dev/null +++ b/devtools/client/fronts/object.js @@ -0,0 +1,465 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { objectSpec } = require("resource://devtools/shared/specs/object.js"); +const { + FrontClassWithSpec, + registerFront, +} = require("resource://devtools/shared/protocol.js"); +const { + LongStringFront, +} = require("resource://devtools/client/fronts/string.js"); + +const SUPPORT_ENUM_ENTRIES_SET = new Set([ + "Headers", + "Map", + "WeakMap", + "Set", + "WeakSet", + "Storage", + "URLSearchParams", + "FormData", + "MIDIInputMap", + "MIDIOutputMap", +]); + +/** + * A ObjectFront is used as a front end for the ObjectActor that is + * created on the server, hiding implementation details. + */ +class ObjectFront extends FrontClassWithSpec(objectSpec) { + constructor(conn = null, targetFront = null, parentFront = null, data) { + if (!parentFront) { + throw new Error("ObjectFront require a parent front"); + } + + super(conn, targetFront, parentFront); + + this._grip = data; + this.actorID = this._grip.actor; + this.valid = true; + + parentFront.manage(this); + } + + skipDestroy() { + // Object fronts are simple fronts, they don't need to be cleaned up on + // toolbox destroy. `conn` is a DebuggerClient instance, check the + // `isToolboxDestroy` flag to skip the destroy. + return this.conn && this.conn.isToolboxDestroy; + } + + getGrip() { + return this._grip; + } + + get isFrozen() { + return this._grip.frozen; + } + + get isSealed() { + return this._grip.sealed; + } + + get isExtensible() { + return this._grip.extensible; + } + + /** + * Request the prototype and own properties of the object. + */ + async getPrototypeAndProperties() { + const result = await super.prototypeAndProperties(); + + if (result.prototype) { + result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this); + } + + // The result packet can have multiple properties that hold grips which we may need + // to turn into fronts. + const gripKeys = ["value", "getterValue", "get", "set"]; + + if (result.ownProperties) { + Object.entries(result.ownProperties).forEach(([key, descriptor]) => { + if (descriptor) { + for (const gripKey of gripKeys) { + if (descriptor.hasOwnProperty(gripKey)) { + result.ownProperties[key][gripKey] = getAdHocFrontOrPrimitiveGrip( + descriptor[gripKey], + this + ); + } + } + } + }); + } + + if (result.safeGetterValues) { + Object.entries(result.safeGetterValues).forEach(([key, descriptor]) => { + if (descriptor) { + for (const gripKey of gripKeys) { + if (descriptor.hasOwnProperty(gripKey)) { + result.safeGetterValues[key][gripKey] = + getAdHocFrontOrPrimitiveGrip(descriptor[gripKey], this); + } + } + } + }); + } + + if (result.ownSymbols) { + result.ownSymbols.forEach((descriptor, i, arr) => { + if (descriptor) { + for (const gripKey of gripKeys) { + if (descriptor.hasOwnProperty(gripKey)) { + arr[i][gripKey] = getAdHocFrontOrPrimitiveGrip( + descriptor[gripKey], + this + ); + } + } + } + }); + } + + return result; + } + + /** + * Request a PropertyIteratorFront instance to ease listing + * properties for this object. + * + * @param options Object + * A dictionary object with various boolean attributes: + * - ignoreIndexedProperties Boolean + * If true, filters out Array items. + * e.g. properties names between `0` and `object.length`. + * - ignoreNonIndexedProperties Boolean + * If true, filters out items that aren't array items + * e.g. properties names that are not a number between `0` + * and `object.length`. + * - sort Boolean + * If true, the iterator will sort the properties by name + * before dispatching them. + */ + enumProperties(options) { + return super.enumProperties(options); + } + + /** + * Request a PropertyIteratorFront instance to enumerate entries in a + * Map/Set-like object. + */ + enumEntries() { + if (!SUPPORT_ENUM_ENTRIES_SET.has(this._grip.class)) { + console.error( + `enumEntries can't be called for "${ + this._grip.class + }" grips. Supported grips are: ${[...SUPPORT_ENUM_ENTRIES_SET].join( + ", " + )}.` + ); + return null; + } + return super.enumEntries(); + } + + /** + * Request a SymbolIteratorFront instance to enumerate symbols in an object. + */ + enumSymbols() { + if (this._grip.type !== "object") { + console.error("enumSymbols is only valid for objects grips."); + return null; + } + return super.enumSymbols(); + } + + /** + * Request the property descriptor of the object's specified property. + * + * @param name string The name of the requested property. + */ + getProperty(name) { + return super.property(name); + } + + /** + * Request the value of the object's specified property. + * + * @param name string The name of the requested property. + * @param receiverId string|null The actorId of the receiver to be used for getters. + */ + async getPropertyValue(name, receiverId) { + const response = await super.propertyValue(name, receiverId); + + if (response.value) { + const { value } = response; + if (value.return) { + response.value.return = getAdHocFrontOrPrimitiveGrip( + value.return, + this + ); + } + + if (value.throw) { + response.value.throw = getAdHocFrontOrPrimitiveGrip(value.throw, this); + } + } + return response; + } + + /** + * Get the body of a custom formatted object. + */ + async customFormatterBody() { + const result = await super.customFormatterBody(); + + if (!result?.customFormatterBody) { + return result; + } + + const createFrontsInJsonMl = item => { + if (Array.isArray(item)) { + return item.map(i => createFrontsInJsonMl(i)); + } + return getAdHocFrontOrPrimitiveGrip(item, this); + }; + + result.customFormatterBody = createFrontsInJsonMl( + result.customFormatterBody + ); + + return result; + } + + /** + * Request the prototype of the object. + */ + async getPrototype() { + const result = await super.prototype(); + + if (!result.prototype) { + return result; + } + + result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this); + + return result; + } + + /** + * Request the state of a promise. + */ + async getPromiseState() { + if (this._grip.class !== "Promise") { + console.error("getPromiseState is only valid for promise grips."); + return null; + } + + let response, promiseState; + try { + response = await super.promiseState(); + promiseState = response.promiseState; + } catch (error) { + // @backward-compat { version 85 } On older server, the promiseState request didn't + // didn't exist (bug 1552648). The promise state was directly included in the grip. + if (error.message.includes("unrecognizedPacketType")) { + promiseState = this._grip.promiseState; + response = { promiseState }; + } else { + throw error; + } + } + + const { value, reason } = promiseState; + + if (value) { + promiseState.value = getAdHocFrontOrPrimitiveGrip(value, this); + } + + if (reason) { + promiseState.reason = getAdHocFrontOrPrimitiveGrip(reason, this); + } + + return response; + } + + /** + * Request the target and handler internal slots of a proxy. + */ + async getProxySlots() { + if (this._grip.class !== "Proxy") { + console.error("getProxySlots is only valid for proxy grips."); + return null; + } + + const response = await super.proxySlots(); + const { proxyHandler, proxyTarget } = response; + + if (proxyHandler) { + response.proxyHandler = getAdHocFrontOrPrimitiveGrip(proxyHandler, this); + } + + if (proxyTarget) { + response.proxyTarget = getAdHocFrontOrPrimitiveGrip(proxyTarget, this); + } + + return response; + } + + get isSyntaxError() { + return this._grip.preview && this._grip.preview.name == "SyntaxError"; + } +} + +/** + * When we are asking the server for the value of a given variable, we might get different + * type of objects: + * - a primitive (string, number, null, false, boolean) + * - a long string + * - an "object" (i.e. not primitive nor long string) + * + * Each of those type need a different front, or none: + * - a primitive does not allow further interaction with the server, so we don't need + * to have a dedicated front. + * - a long string needs a longStringFront to be able to retrieve the full string. + * - an object need an objectFront to retrieve properties, symbols and prototype. + * + * In the case an ObjectFront is created, we also check if the object has properties + * that should be turned into fronts as well. + * + * @param {String|Number|Object} options: The packet returned by the server. + * @param {Front} parentFront + * + * @returns {Number|String|Object|LongStringFront|ObjectFront} + */ +function getAdHocFrontOrPrimitiveGrip(packet, parentFront) { + // We only want to try to create a front when it makes sense, i.e when it has an + // actorID, unless: + // - it's a Symbol (See Bug 1600299) + // - it's a mapEntry (the preview.key and preview.value properties can hold actors) + // - or it is already a front (happens when we are using the legacy listeners in the ResourceCommand) + const isPacketAnObject = packet && typeof packet === "object"; + const isFront = !!packet.typeName; + if ( + !isPacketAnObject || + packet.type == "symbol" || + (packet.type !== "mapEntry" && !packet.actor) || + isFront + ) { + return packet; + } + + const { conn } = parentFront; + // If the parent front is a target, consider it as the target to use for all objects + const targetFront = parentFront.isTargetFront + ? parentFront + : parentFront.targetFront; + + // We may have already created a front for this object actor since some actor (e.g. the + // thread actor) cache the object actors they create. + const existingFront = conn.getFrontByID(packet.actor); + if (existingFront) { + return existingFront; + } + + const { type } = packet; + + if (type === "longString") { + const longStringFront = new LongStringFront(conn, targetFront, parentFront); + longStringFront.form(packet); + parentFront.manage(longStringFront); + return longStringFront; + } + + if (type === "mapEntry" && packet.preview) { + const { key, value } = packet.preview; + packet.preview.key = getAdHocFrontOrPrimitiveGrip( + key, + parentFront, + targetFront + ); + packet.preview.value = getAdHocFrontOrPrimitiveGrip( + value, + parentFront, + targetFront + ); + return packet; + } + + const objectFront = new ObjectFront(conn, targetFront, parentFront, packet); + createChildFronts(objectFront, packet); + return objectFront; +} + +/** + * Create child fronts of the passed object front given a packet. Those child fronts are + * usually mapping actors of the packet sub-properties (preview items, promise fullfilled + * values, …). + * + * @param {ObjectFront} objectFront + * @param {String|Number|Object} packet: The packet returned by the server + */ +function createChildFronts(objectFront, packet) { + if (packet.preview) { + const { message, entries } = packet.preview; + + // The message could be a longString. + if (packet.preview.message) { + packet.preview.message = getAdHocFrontOrPrimitiveGrip( + message, + objectFront + ); + } + + // Handle Map/WeakMap preview entries (the preview might be directly used if has all the + // items needed, i.e. if the Map has less than 10 items). + if (entries && Array.isArray(entries)) { + packet.preview.entries = entries.map(([key, value]) => [ + getAdHocFrontOrPrimitiveGrip(key, objectFront), + getAdHocFrontOrPrimitiveGrip(value, objectFront), + ]); + } + } + + if (packet && typeof packet.ownProperties === "object") { + for (const [name, descriptor] of Object.entries(packet.ownProperties)) { + // The descriptor can have multiple properties that hold grips which we may need + // to turn into fronts. + const gripKeys = ["value", "getterValue", "get", "set"]; + for (const key of gripKeys) { + if ( + descriptor && + typeof descriptor === "object" && + descriptor.hasOwnProperty(key) + ) { + packet.ownProperties[name][key] = getAdHocFrontOrPrimitiveGrip( + descriptor[key], + objectFront + ); + } + } + } + } + + // Handle custom formatters + if (packet && packet.useCustomFormatter && Array.isArray(packet.header)) { + const createFrontsInJsonMl = item => { + if (Array.isArray(item)) { + return item.map(i => createFrontsInJsonMl(i)); + } + return getAdHocFrontOrPrimitiveGrip(item, objectFront); + }; + + packet.header = createFrontsInJsonMl(packet.header); + } +} + +registerFront(ObjectFront); + +exports.ObjectFront = ObjectFront; +exports.getAdHocFrontOrPrimitiveGrip = getAdHocFrontOrPrimitiveGrip; |