diff options
Diffstat (limited to 'devtools/server/actors/object.js')
-rw-r--r-- | devtools/server/actors/object.js | 810 |
1 files changed, 810 insertions, 0 deletions
diff --git a/devtools/server/actors/object.js b/devtools/server/actors/object.js new file mode 100644 index 0000000000..55dac64275 --- /dev/null +++ b/devtools/server/actors/object.js @@ -0,0 +1,810 @@ +/* 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 { Cu } = require("chrome"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const { assert } = DevToolsUtils; + +const protocol = require("devtools/shared/protocol"); +const { objectSpec } = require("devtools/shared/specs/object"); + +loader.lazyRequireGetter( + this, + "PropertyIteratorActor", + "devtools/server/actors/object/property-iterator", + true +); +loader.lazyRequireGetter( + this, + "SymbolIteratorActor", + "devtools/server/actors/object/symbol-iterator", + true +); +loader.lazyRequireGetter( + this, + "previewers", + "devtools/server/actors/object/previewers" +); +loader.lazyRequireGetter( + this, + "stringify", + "devtools/server/actors/object/stringifiers" +); + +// ContentDOMReference requires ChromeUtils, which isn't available in worker context. +if (!isWorker) { + loader.lazyRequireGetter( + this, + "ContentDOMReference", + "resource://gre/modules/ContentDOMReference.jsm", + true + ); +} + +const { + getArrayLength, + getPromiseState, + getStorageLength, + isArray, + isStorage, + isTypedArray, +} = require("devtools/server/actors/object/utils"); + +const proto = { + /** + * 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 + */ + initialize( + obj, + { + thread, + createValueGrip: createValueGripHook, + createEnvironmentActor, + getGripDepth, + incrementGripDepth, + decrementGripDepth, + }, + conn + ) { + assert( + !obj.optimizedOut, + "Should not create object actors for optimized out values!" + ); + protocol.Actor.prototype.initialize.call(this, conn); + + this.conn = conn; + this.obj = obj; + this.thread = thread; + this.hooks = { + createValueGrip: createValueGripHook, + createEnvironmentActor, + getGripDepth, + incrementGripDepth, + decrementGripDepth, + }; + }, + + rawValue: function() { + 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: function() { + 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; + } + + 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) && 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 = ContentDOMReference.get(raw); + } catch (e) {} + } + + return g; + }, + + _getOwnPropertyLength: function() { + 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.getOwnPropertyNames().length; + } 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: function() { + 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: function(grip, raw) { + for (const previewer of previewers[this.obj.class] || previewers.Object) { + try { + const previewerResult = previewer(this, grip, raw); + 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: function() { + 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 }; + }, + + /** + * Handle a protocol request to provide the names of the properties defined on + * the object and not its prototype. + */ + ownPropertyNames: function() { + let props = []; + if (DevToolsUtils.isSafeDebuggerObject(this.obj)) { + try { + props = this.obj.getOwnPropertyNames(); + } catch (err) { + // The above can throw when the debuggee does not subsume the object's + // compartment, or for some WrappedNatives like Cu.Sandbox. + } + } + return { ownPropertyNames: props }; + }, + + /** + * 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: function(options) { + return PropertyIteratorActor(this, options, this.conn); + }, + + /** + * Creates an actor to iterate over entries of a Map/Set-like object. + */ + enumEntries: function() { + return PropertyIteratorActor(this, { enumEntries: true }, this.conn); + }, + + /** + * Creates an actor to iterate over an object symbols properties. + */ + enumSymbols: function() { + return SymbolIteratorActor(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: function() { + 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=0] + * 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. + */ + // eslint-disable-next-line complexity + _findSafeGetterValues: function(ownProperties, limit = 0) { + const safeGetterValues = Object.create(null); + let obj = this.obj; + let level = 0, + i = 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)) { + const getters = this._findSafeGetters(obj); + for (const name of getters) { + // 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; + } + + let desc = null, + getter = null; + try { + desc = obj.getOwnPropertyDescriptor(name); + getter = desc.get; + } catch (ex) { + // The above can throw if the cache becomes stale. + } + if (!getter) { + obj._safeGetters = null; + continue; + } + + const result = getter.call(this.obj); + if (!result || "throw" in result) { + continue; + } + + let getterValue = undefined; + if ("return" in result) { + getterValue = result.return; + } else if ("yield" in result) { + getterValue = result.yield; + } + + // Treat an already-rejected Promise as we would a thrown exception + // by not including it as a safe getter value (see Bug 1477765). + if ( + getterValue && + getterValue.class == "Promise" && + getterValue.promiseState == "rejected" + ) { + // 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. + if (getterValue !== undefined) { + safeGetterValues[name] = { + getterValue: this.hooks.createValueGrip(getterValue), + getterPrototypeLevel: level, + enumerable: desc.enumerable, + writable: level == 0 ? desc.writable : true, + }; + if (limit && ++i == limit) { + break; + } + } + } + if (limit && i == limit) { + break; + } + + obj = obj.proto; + level++; + } + + return safeGetterValues; + }, + + /** + * 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: function(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)) { + getters.add(name); + } + } + + object._safeGetters = getters; + return getters; + }, + + /** + * Handle a protocol request to provide the prototype of the object. + */ + prototype: function() { + 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: function(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: function(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<any>} args + * The array of un-decoded actor objects, or primitives. + */ + apply: function(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 eqivalent + * object grip for use by the API. + * + * See https://developer.mozilla.org/en-US/docs/Tools/Debugger-API/Conventions#completion-values + * 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; + }, + + /** + * Handle a protocol request to provide the display string for the object. + */ + displayString: function() { + const string = stringify(this.obj); + return { displayString: this.hooks.createValueGrip(string) }; + }, + + /** + * 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: function(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 provide the source code of a function. + * + * @param pretty boolean + */ + decompile: function(pretty) { + if (this.obj.class !== "Function") { + return this.throwError( + "objectNotFunction", + "decompile request is only valid for grips with a 'Function' class." + ); + } + + return { decompiledCode: this.obj.decompile(!!pretty) }; + }, + + /** + * Handle a protocol request to provide the parameters of a function. + */ + parameterNames: function() { + if (this.obj.class !== "Function") { + return this.throwError( + "objectNotFunction", + "'parameterNames' request is only valid for grips with a 'Function' class." + ); + } + + return { parameterNames: this.obj.parameterNames }; + }, + + /** + * Handle a protocol request to get the target and handler internal slots of a proxy. + */ + proxySlots: function() { + // 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: function() {}, +}; + +exports.ObjectActor = protocol.ActorClassWithSpec(objectSpec, proto); +exports.ObjectActorProto = proto; |