/* 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 { Actor } = require("resource://devtools/shared/protocol/Actor.js"); const { objectSpec } = require("resource://devtools/shared/specs/object.js"); const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); const { assert } = DevToolsUtils; loader.lazyRequireGetter( this, "PropertyIteratorActor", "resource://devtools/server/actors/object/property-iterator.js", true ); loader.lazyRequireGetter( this, "SymbolIteratorActor", "resource://devtools/server/actors/object/symbol-iterator.js", true ); loader.lazyRequireGetter( this, "PrivatePropertiesIteratorActor", "resource://devtools/server/actors/object/private-properties-iterator.js", true ); loader.lazyRequireGetter( this, "previewers", "resource://devtools/server/actors/object/previewers.js" ); loader.lazyRequireGetter( this, ["customFormatterHeader", "customFormatterBody"], "resource://devtools/server/actors/utils/custom-formatters.js", true ); // This is going to be used by findSafeGetters, where we want to avoid calling getters for // deprecated properties (otherwise a warning message is displayed in the console). // We could do something like EagerEvaluation, where we create a new Sandbox which is then // used to compare functions, but, we'd need to make new classes available in // the Sandbox, and possibly do it again when a new property gets deprecated. // Since this is only to be able to automatically call getters, we can simply check against // a list of unsafe getters that we generate from webidls. loader.lazyRequireGetter( this, "unsafeGettersNames", "resource://devtools/server/actors/webconsole/webidl-unsafe-getters-names.js" ); // ContentDOMReference requires ChromeUtils, which isn't available in worker context. const lazy = {}; if (!isWorker) { loader.lazyGetter( lazy, "ContentDOMReference", () => ChromeUtils.importESModule( "resource://gre/modules/ContentDOMReference.sys.mjs", // ContentDOMReference needs to be retrieved from the shared global // since it is a shared singleton. { global: "shared" } ).ContentDOMReference ); } const { getArrayLength, getPromiseState, getStorageLength, isArray, isStorage, isTypedArray, } = require("resource://devtools/server/actors/object/utils.js"); class ObjectActor extends Actor { /** * Creates an actor for the specified object. * * @param obj Debugger.Object * The debuggee object. * @param Object * A collection of abstract methods that are implemented by the caller. * ObjectActor requires the following functions to be implemented by * the caller: * - createValueGrip * Creates a value grip for the given object * - createEnvironmentActor * Creates and return an environment actor * - getGripDepth * An actor's grip depth getter * - incrementGripDepth * Increment the actor's grip depth * - decrementGripDepth * Decrement the actor's grip depth * @param DevToolsServerConnection conn */ constructor( obj, { thread, createValueGrip: createValueGripHook, createEnvironmentActor, getGripDepth, incrementGripDepth, decrementGripDepth, customFormatterObjectTagDepth, customFormatterConfigDbgObj, }, conn ) { super(conn, objectSpec); assert( !obj.optimizedOut, "Should not create object actors for optimized out values!" ); this.obj = obj; this.thread = thread; this.hooks = { createValueGrip: createValueGripHook, createEnvironmentActor, getGripDepth, incrementGripDepth, decrementGripDepth, customFormatterObjectTagDepth, customFormatterConfigDbgObj, }; } rawValue() { return this.obj.unsafeDereference(); } addWatchpoint(property, label, watchpointType) { this.thread.addWatchpoint(this, { property, label, watchpointType }); } removeWatchpoint(property) { this.thread.removeWatchpoint(this, property); } removeWatchpoints() { this.thread.removeWatchpoint(this); } /** * Returns a grip for this actor for returning in a protocol message. */ form() { const g = { type: "object", actor: this.actorID, }; const unwrapped = DevToolsUtils.unwrap(this.obj); if (unwrapped === undefined) { // Objects belonging to an invisible-to-debugger compartment might be proxies, // so just in case they shouldn't be accessed. g.class = "InvisibleToDebugger: " + this.obj.class; return g; } // Only process custom formatters if the feature is enabled. if (this.thread?.targetActor?.customFormatters) { const result = customFormatterHeader(this); if (result) { const { formatter, ...header } = result; this._customFormatterItem = formatter; return { ...g, ...header, }; } } if (unwrapped?.isProxy) { // Proxy objects can run traps when accessed, so just create a preview with // the target and the handler. g.class = "Proxy"; this.hooks.incrementGripDepth(); previewers.Proxy[0](this, g, null); this.hooks.decrementGripDepth(); return g; } const ownPropertyLength = this._getOwnPropertyLength(); Object.assign(g, { // If the debuggee does not subsume the object's compartment, most properties won't // be accessible. Cross-orgin Window and Location objects might expose some, though. // Change the displayed class, but when creating the preview use the original one. class: unwrapped === null ? "Restricted" : this.obj.class, ownPropertyLength: Number.isFinite(ownPropertyLength) ? ownPropertyLength : undefined, extensible: this.obj.isExtensible(), frozen: this.obj.isFrozen(), sealed: this.obj.isSealed(), isError: this.obj.isError, }); this.hooks.incrementGripDepth(); if (g.class == "Function") { g.isClassConstructor = this.obj.isClassConstructor; } const raw = this.getRawObject(); this._populateGripPreview(g, raw); this.hooks.decrementGripDepth(); if (raw && Node.isInstance(raw) && lazy.ContentDOMReference) { // ContentDOMReference.get takes a DOM element and returns an object with // its browsing context id, as well as a unique identifier. We are putting it in // the grip here in order to be able to retrieve the node later, potentially from a // different DevToolsServer running in the same process. // If ContentDOMReference.get throws, we simply don't add the property to the grip. try { g.contentDomReference = lazy.ContentDOMReference.get(raw); } catch (e) {} } return g; } customFormatterBody() { return customFormatterBody(this, this._customFormatterItem); } _getOwnPropertyLength() { if (isTypedArray(this.obj)) { // Bug 1348761: getOwnPropertyNames is unnecessary slow on TypedArrays return getArrayLength(this.obj); } if (isStorage(this.obj)) { return getStorageLength(this.obj); } try { return this.obj.getOwnPropertyNamesLength(); } catch (err) { // The above can throw when the debuggee does not subsume the object's // compartment, or for some WrappedNatives like Cu.Sandbox. } return null; } getRawObject() { let raw = this.obj.unsafeDereference(); // If Cu is not defined, we are running on a worker thread, where xrays // don't exist. if (raw && Cu) { raw = Cu.unwaiveXrays(raw); } if (raw && !DevToolsUtils.isSafeJSObject(raw)) { raw = null; } return raw; } /** * Populate the `preview` property on `grip` given its type. */ _populateGripPreview(grip, raw) { // Cache obj.class as it can be costly if this is in a hot path (e.g. logging objects // within a for loop). const className = this.obj.class; for (const previewer of previewers[className] || previewers.Object) { try { const previewerResult = previewer(this, grip, raw, className); if (previewerResult) { return; } } catch (e) { const msg = "ObjectActor.prototype._populateGripPreview previewer function"; DevToolsUtils.reportException(msg, e); } } } /** * Returns an object exposing the internal Promise state. */ promiseState() { const { state, value, reason } = getPromiseState(this.obj); const promiseState = { state }; if (state == "fulfilled") { promiseState.value = this.hooks.createValueGrip(value); } else if (state == "rejected") { promiseState.reason = this.hooks.createValueGrip(reason); } promiseState.creationTimestamp = Date.now() - this.obj.promiseLifetime; // Only add the timeToSettle property if the Promise isn't pending. if (state !== "pending") { promiseState.timeToSettle = this.obj.promiseTimeToResolution; } return { promiseState }; } /** * Creates an actor to iterate over an object property names and values. * See PropertyIteratorActor constructor for more info about options param. * * @param options object */ enumProperties(options) { return new PropertyIteratorActor(this, options, this.conn); } /** * Creates an actor to iterate over entries of a Map/Set-like object. */ enumEntries() { return new PropertyIteratorActor(this, { enumEntries: true }, this.conn); } /** * Creates an actor to iterate over an object symbols properties. */ enumSymbols() { return new SymbolIteratorActor(this, this.conn); } /** * Creates an actor to iterate over an object private properties. */ enumPrivateProperties() { return new PrivatePropertiesIteratorActor(this, this.conn); } /** * Handle a protocol request to provide the prototype and own properties of * the object. * * @returns {Object} An object containing the data of this.obj, of the following form: * - {Object} prototype: The descriptor of this.obj's prototype. * - {Object} ownProperties: an object where the keys are the names of the * this.obj's ownProperties, and the values the descriptors of * the properties. * - {Array} ownSymbols: An array containing all descriptors of this.obj's * ownSymbols. Here we have an array, and not an object like for * ownProperties, because we can have multiple symbols with the same * name in this.obj, e.g. `{[Symbol()]: "a", [Symbol()]: "b"}`. * - {Object} safeGetterValues: an object that maps this.obj's property names * with safe getters descriptors. */ prototypeAndProperties() { let objProto = null; let names = []; let symbols = []; if (DevToolsUtils.isSafeDebuggerObject(this.obj)) { try { objProto = this.obj.proto; names = this.obj.getOwnPropertyNames(); symbols = this.obj.getOwnPropertySymbols(); } catch (err) { // The above can throw when the debuggee does not subsume the object's // compartment, or for some WrappedNatives like Cu.Sandbox. } } const ownProperties = Object.create(null); const ownSymbols = []; for (const name of names) { ownProperties[name] = this._propertyDescriptor(name); } for (const sym of symbols) { ownSymbols.push({ name: sym.toString(), descriptor: this._propertyDescriptor(sym), }); } return { prototype: this.hooks.createValueGrip(objProto), ownProperties, ownSymbols, safeGetterValues: this._findSafeGetterValues(names), }; } /** * Find the safe getter values for the current Debugger.Object, |this.obj|. * * @private * @param array ownProperties * The array that holds the list of known ownProperties names for * |this.obj|. * @param number [limit=Infinity] * Optional limit of getter values to find. * @return object * An object that maps property names to safe getter descriptors as * defined by the remote debugging protocol. */ _findSafeGetterValues(ownProperties, limit = Infinity) { const safeGetterValues = Object.create(null); let obj = this.obj; let level = 0, currentGetterValuesCount = 0; // Do not search safe getters in unsafe objects. if (!DevToolsUtils.isSafeDebuggerObject(obj)) { return safeGetterValues; } // Most objects don't have any safe getters but inherit some from their // prototype. Avoid calling getOwnPropertyNames on objects that may have // many properties like Array, strings or js objects. That to avoid // freezing firefox when doing so. if (isArray(this.obj) || ["Object", "String"].includes(this.obj.class)) { obj = obj.proto; level++; } while (obj && DevToolsUtils.isSafeDebuggerObject(obj)) { for (const name of this._findSafeGetters(obj)) { // Avoid overwriting properties from prototypes closer to this.obj. Also // avoid providing safeGetterValues from prototypes if property |name| // is already defined as an own property. if ( name in safeGetterValues || (obj != this.obj && ownProperties.includes(name)) ) { continue; } // Ignore __proto__ on Object.prototye. if (!obj.proto && name == "__proto__") { continue; } const desc = safeGetOwnPropertyDescriptor(obj, name); if (!desc?.get) { // If no getter matches the name, the cache is stale and should be cleaned up. obj._safeGetters = null; continue; } const getterValue = this._evaluateGetter(desc.get); if (getterValue === undefined) { continue; } // Treat an already-rejected Promise as we would a thrown exception // by not including it as a safe getter value (see Bug 1477765). if (isRejectedPromise(getterValue)) { // Until we have a good way to handle Promise rejections through the // debugger API (Bug 1478076), call `catch` when it's safe to do so. const raw = getterValue.unsafeDereference(); if (DevToolsUtils.isSafeJSObject(raw)) { raw.catch(e => e); } continue; } // WebIDL attributes specified with the LenientThis extended attribute // return undefined and should be ignored. safeGetterValues[name] = { getterValue: this.hooks.createValueGrip(getterValue), getterPrototypeLevel: level, enumerable: desc.enumerable, writable: level == 0 ? desc.writable : true, }; ++currentGetterValuesCount; if (currentGetterValuesCount == limit) { return safeGetterValues; } } obj = obj.proto; level++; } return safeGetterValues; } /** * Evaluate the getter function |desc.get|. * @param {Object} getter */ _evaluateGetter(getter) { const result = getter.call(this.obj); if (!result || "throw" in result) { return undefined; } let getterValue = undefined; if ("return" in result) { getterValue = result.return; } else if ("yield" in result) { getterValue = result.yield; } return getterValue; } /** * Find the safe getters for a given Debugger.Object. Safe getters are native * getters which are safe to execute. * * @private * @param Debugger.Object object * The Debugger.Object where you want to find safe getters. * @return Set * A Set of names of safe getters. This result is cached for each * Debugger.Object. */ _findSafeGetters(object) { if (object._safeGetters) { return object._safeGetters; } const getters = new Set(); if (!DevToolsUtils.isSafeDebuggerObject(object)) { object._safeGetters = getters; return getters; } let names = []; try { names = object.getOwnPropertyNames(); } catch (ex) { // Calling getOwnPropertyNames() on some wrapped native prototypes is not // allowed: "cannot modify properties of a WrappedNative". See bug 952093. } for (const name of names) { let desc = null; try { desc = object.getOwnPropertyDescriptor(name); } catch (e) { // Calling getOwnPropertyDescriptor on wrapped native prototypes is not // allowed (bug 560072). } if (!desc || desc.value !== undefined || !("get" in desc)) { continue; } if ( DevToolsUtils.hasSafeGetter(desc) && !unsafeGettersNames.includes(name) ) { getters.add(name); } } object._safeGetters = getters; return getters; } /** * Handle a protocol request to provide the prototype of the object. */ prototype() { let objProto = null; if (DevToolsUtils.isSafeDebuggerObject(this.obj)) { objProto = this.obj.proto; } return { prototype: this.hooks.createValueGrip(objProto) }; } /** * Handle a protocol request to provide the property descriptor of the * object's specified property. * * @param name string * The property we want the description of. */ property(name) { if (!name) { return this.throwError( "missingParameter", "no property name was specified" ); } return { descriptor: this._propertyDescriptor(name) }; } /** * Handle a protocol request to provide the value of the object's * specified property. * * Note: Since this will evaluate getters, it can trigger execution of * content code and may cause side effects. This endpoint should only be used * when you are confident that the side-effects will be safe, or the user * is expecting the effects. * * @param {string} name * The property we want the value of. * @param {string|null} receiverId * The actorId of the receiver to be used if the property is a getter. * If null or invalid, the receiver will be the referent. */ propertyValue(name, receiverId) { if (!name) { return this.throwError( "missingParameter", "no property name was specified" ); } let receiver; if (receiverId) { const receiverActor = this.conn.getActor(receiverId); if (receiverActor) { receiver = receiverActor.obj; } } const value = receiver ? this.obj.getProperty(name, receiver) : this.obj.getProperty(name); return { value: this._buildCompletion(value) }; } /** * Handle a protocol request to evaluate a function and provide the value of * the result. * * Note: Since this will evaluate the function, it can trigger execution of * content code and may cause side effects. This endpoint should only be used * when you are confident that the side-effects will be safe, or the user * is expecting the effects. * * @param {any} context * The 'this' value to call the function with. * @param {Array} args * The array of un-decoded actor objects, or primitives. */ apply(context, args) { if (!this.obj.callable) { return this.throwError("notCallable", "debugee object is not callable"); } const debugeeContext = this._getValueFromGrip(context); const debugeeArgs = args && args.map(this._getValueFromGrip, this); const value = this.obj.apply(debugeeContext, debugeeArgs); return { value: this._buildCompletion(value) }; } _getValueFromGrip(grip) { if (typeof grip !== "object" || !grip) { return grip; } if (typeof grip.actor !== "string") { return this.throwError( "invalidGrip", "grip argument did not include actor ID" ); } const actor = this.conn.getActor(grip.actor); if (!actor) { return this.throwError( "unknownActor", "grip actor did not match a known object" ); } return actor.obj; } /** * Converts a Debugger API completion value record into an equivalent * object grip for use by the API. * * See https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/ * for more specifics on the expected behavior. */ _buildCompletion(value) { let completionGrip = null; // .apply result will be falsy if the script being executed is terminated // via the "slow script" dialog. if (value) { completionGrip = {}; if ("return" in value) { completionGrip.return = this.hooks.createValueGrip(value.return); } if ("throw" in value) { completionGrip.throw = this.hooks.createValueGrip(value.throw); } } return completionGrip; } /** * A helper method that creates a property descriptor for the provided object, * properly formatted for sending in a protocol response. * * @private * @param string name * The property that the descriptor is generated for. * @param boolean [onlyEnumerable] * Optional: true if you want a descriptor only for an enumerable * property, false otherwise. * @return object|undefined * The property descriptor, or undefined if this is not an enumerable * property and onlyEnumerable=true. */ _propertyDescriptor(name, onlyEnumerable) { if (!DevToolsUtils.isSafeDebuggerObject(this.obj)) { return undefined; } let desc; try { desc = this.obj.getOwnPropertyDescriptor(name); } catch (e) { // Calling getOwnPropertyDescriptor on wrapped native prototypes is not // allowed (bug 560072). Inform the user with a bogus, but hopefully // explanatory, descriptor. return { configurable: false, writable: false, enumerable: false, value: e.name, }; } if (isStorage(this.obj)) { if (name === "length") { return undefined; } return desc; } if (!desc || (onlyEnumerable && !desc.enumerable)) { return undefined; } const retval = { configurable: desc.configurable, enumerable: desc.enumerable, }; const obj = this.rawValue(); if ("value" in desc) { retval.writable = desc.writable; retval.value = this.hooks.createValueGrip(desc.value); } else if (this.thread.getWatchpoint(obj, name.toString())) { const watchpoint = this.thread.getWatchpoint(obj, name.toString()); retval.value = this.hooks.createValueGrip(watchpoint.desc.value); retval.watchpoint = watchpoint.watchpointType; } else { if ("get" in desc) { retval.get = this.hooks.createValueGrip(desc.get); } if ("set" in desc) { retval.set = this.hooks.createValueGrip(desc.set); } } return retval; } /** * Handle a protocol request to get the target and handler internal slots of a proxy. */ proxySlots() { // There could be transparent security wrappers, unwrap to check if it's a proxy. // However, retrieve proxyTarget and proxyHandler from `this.obj` to avoid exposing // the unwrapped target and handler. const unwrapped = DevToolsUtils.unwrap(this.obj); if (!unwrapped || !unwrapped.isProxy) { return this.throwError( "objectNotProxy", "'proxySlots' request is only valid for grips with a 'Proxy' class." ); } return { proxyTarget: this.hooks.createValueGrip(this.obj.proxyTarget), proxyHandler: this.hooks.createValueGrip(this.obj.proxyHandler), }; } /** * Release the actor, when it isn't needed anymore. * Protocol.js uses this release method to call the destroy method. */ release() { if (this.hooks) { this.hooks.customFormatterConfigDbgObj = null; } this._customFormatterItem = null; this.obj = null; this.thread = null; } } exports.ObjectActor = ObjectActor; function safeGetOwnPropertyDescriptor(obj, name) { let desc = null; try { desc = obj.getOwnPropertyDescriptor(name); } catch (ex) { // The above can throw if the cache becomes stale. } return desc; } /** * Check if the value is rejected promise * * @param {Object} getterValue * @returns {boolean} true if the value is rejected promise, false otherwise. */ function isRejectedPromise(getterValue) { return ( getterValue && getterValue.class == "Promise" && getterValue.promiseState == "rejected" ); }