diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/server/actors/object | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | devtools/server/actors/object.js | 847 | ||||
-rw-r--r-- | devtools/server/actors/object/moz.build | 12 | ||||
-rw-r--r-- | devtools/server/actors/object/previewers.js | 1142 | ||||
-rw-r--r-- | devtools/server/actors/object/private-properties-iterator.js | 70 | ||||
-rw-r--r-- | devtools/server/actors/object/property-iterator.js | 685 | ||||
-rw-r--r-- | devtools/server/actors/object/symbol-iterator.js | 67 | ||||
-rw-r--r-- | devtools/server/actors/object/symbol.js | 109 | ||||
-rw-r--r-- | devtools/server/actors/object/utils.js | 615 | ||||
-rw-r--r-- | devtools/server/actors/objects-manager.js | 39 |
9 files changed, 3586 insertions, 0 deletions
diff --git a/devtools/server/actors/object.js b/devtools/server/actors/object.js new file mode 100644 index 0000000000..9b52c4ebe7 --- /dev/null +++ b/devtools/server/actors/object.js @@ -0,0 +1,847 @@ +/* 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. + loadInDevToolsLoader: false, + } + ).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?._parent?.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<any>} 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" + ); +} diff --git a/devtools/server/actors/object/moz.build b/devtools/server/actors/object/moz.build new file mode 100644 index 0000000000..28fc2307da --- /dev/null +++ b/devtools/server/actors/object/moz.build @@ -0,0 +1,12 @@ +# 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/. + +DevToolsModules( + "previewers.js", + "private-properties-iterator.js", + "property-iterator.js", + "symbol-iterator.js", + "symbol.js", + "utils.js", +) diff --git a/devtools/server/actors/object/previewers.js b/devtools/server/actors/object/previewers.js new file mode 100644 index 0000000000..451858a826 --- /dev/null +++ b/devtools/server/actors/object/previewers.js @@ -0,0 +1,1142 @@ +/* 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 { DevToolsServer } = require("resource://devtools/server/devtools-server.js"); +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +loader.lazyRequireGetter( + this, + "ObjectUtils", + "resource://devtools/server/actors/object/utils.js" +); +loader.lazyRequireGetter( + this, + "PropertyIterators", + "resource://devtools/server/actors/object/property-iterator.js" +); + +// Number of items to preview in objects, arrays, maps, sets, lists, +// collections, etc. +const OBJECT_PREVIEW_MAX_ITEMS = 10; + +const ERROR_CLASSNAMES = new Set([ + "Error", + "EvalError", + "RangeError", + "ReferenceError", + "SyntaxError", + "TypeError", + "URIError", + "InternalError", + "AggregateError", + "CompileError", + "DebuggeeWouldRun", + "LinkError", + "RuntimeError", + "Exception", // This related to Components.Exception() +]); +const ARRAY_LIKE_CLASSNAMES = new Set([ + "DOMStringList", + "DOMTokenList", + "CSSRuleList", + "MediaList", + "StyleSheetList", + "NamedNodeMap", + "FileList", + "NodeList", +]); +const OBJECT_WITH_URL_CLASSNAMES = new Set([ + "CSSImportRule", + "CSSStyleSheet", + "Location", +]); + +/** + * Functions for adding information to ObjectActor grips for the purpose of + * having customized output. This object holds arrays mapped by + * Debugger.Object.prototype.class. + * + * In each array you can add functions that take three + * arguments: + * - the ObjectActor instance and its hooks to make a preview for, + * - the grip object being prepared for the client, + * - the raw JS object after calling Debugger.Object.unsafeDereference(). This + * argument is only provided if the object is safe for reading properties and + * executing methods. See DevToolsUtils.isSafeJSObject(). + * - the object class (result of objectActor.obj.class). This is passed so we don't have + * to access it on each previewer, which can add some overhead. + * + * Functions must return false if they cannot provide preview + * information for the debugger object, or true otherwise. + */ +const previewers = { + String: [ + function(objectActor, grip, rawObj) { + return wrappedPrimitivePreviewer( + "String", + String, + objectActor, + grip, + rawObj + ); + }, + ], + + Boolean: [ + function(objectActor, grip, rawObj) { + return wrappedPrimitivePreviewer( + "Boolean", + Boolean, + objectActor, + grip, + rawObj + ); + }, + ], + + Number: [ + function(objectActor, grip, rawObj) { + return wrappedPrimitivePreviewer( + "Number", + Number, + objectActor, + grip, + rawObj + ); + }, + ], + + Symbol: [ + function(objectActor, grip, rawObj) { + return wrappedPrimitivePreviewer( + "Symbol", + Symbol, + objectActor, + grip, + rawObj + ); + }, + ], + + Function: [ + function({ obj, hooks }, grip) { + if (obj.name) { + grip.name = obj.name; + } + + if (obj.displayName) { + grip.displayName = obj.displayName.substr(0, 500); + } + + if (obj.parameterNames) { + grip.parameterNames = obj.parameterNames; + } + + // Check if the developer has added a de-facto standard displayName + // property for us to use. + let userDisplayName; + try { + userDisplayName = obj.getOwnPropertyDescriptor("displayName"); + } catch (e) { + // The above can throw "permission denied" errors when the debuggee + // does not subsume the function's compartment. + } + + if ( + userDisplayName && + typeof userDisplayName.value == "string" && + userDisplayName.value + ) { + grip.userDisplayName = hooks.createValueGrip(userDisplayName.value); + } + + grip.isAsync = obj.isAsyncFunction; + grip.isGenerator = obj.isGeneratorFunction; + + if (obj.script) { + // NOTE: Debugger.Script.prototype.startColumn is 1-based. + // Convert to 0-based, while keeping the wasm's column (1) as is. + // (bug 1863878) + const columnBase = obj.script.format === "wasm" ? 0 : 1; + grip.location = { + url: obj.script.url, + line: obj.script.startLine, + column: obj.script.startColumn - columnBase, + }; + } + + return true; + }, + ], + + RegExp: [ + function({ obj, hooks }, grip) { + const str = DevToolsUtils.callPropertyOnObject(obj, "toString"); + if (typeof str != "string") { + return false; + } + + grip.displayString = hooks.createValueGrip(str); + return true; + }, + ], + + Date: [ + function({ obj, hooks }, grip) { + const time = DevToolsUtils.callPropertyOnObject(obj, "getTime"); + if (typeof time != "number") { + return false; + } + + grip.preview = { + timestamp: hooks.createValueGrip(time), + }; + return true; + }, + ], + + Array: [ + function({ obj, hooks }, grip) { + const length = ObjectUtils.getArrayLength(obj); + + grip.preview = { + kind: "ArrayLike", + length: length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + const raw = obj.unsafeDereference(); + const items = (grip.preview.items = []); + + for (let i = 0; i < length; ++i) { + if (raw && !isWorker) { + // Array Xrays filter out various possibly-unsafe properties (like + // functions, and claim that the value is undefined instead. This + // is generally the right thing for privileged code accessing untrusted + // objects, but quite confusing for Object previews. So we manually + // override this protection by waiving Xrays on the array, and re-applying + // Xrays on any indexed value props that we pull off of it. + const desc = Object.getOwnPropertyDescriptor(Cu.waiveXrays(raw), i); + if (desc && !desc.get && !desc.set) { + let value = Cu.unwaiveXrays(desc.value); + value = ObjectUtils.makeDebuggeeValueIfNeeded(obj, value); + items.push(hooks.createValueGrip(value)); + } else if (!desc) { + items.push(null); + } else { + const item = {}; + if (desc.get) { + let getter = Cu.unwaiveXrays(desc.get); + getter = ObjectUtils.makeDebuggeeValueIfNeeded(obj, getter); + item.get = hooks.createValueGrip(getter); + } + if (desc.set) { + let setter = Cu.unwaiveXrays(desc.set); + setter = ObjectUtils.makeDebuggeeValueIfNeeded(obj, setter); + item.set = hooks.createValueGrip(setter); + } + items.push(item); + } + } else if (raw && !obj.getOwnPropertyDescriptor(i)) { + items.push(null); + } else { + // Workers do not have access to Cu. + const value = DevToolsUtils.getProperty(obj, i); + items.push(hooks.createValueGrip(value)); + } + + if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + Set: [ + function(objectActor, grip) { + const size = DevToolsUtils.getProperty(objectActor.obj, "size"); + if (typeof size != "number") { + return false; + } + + grip.preview = { + kind: "ArrayLike", + length: size, + }; + + // Avoid recursive object grips. + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const items = (grip.preview.items = []); + for (const item of PropertyIterators.enumSetEntries(objectActor)) { + items.push(item); + if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + WeakSet: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumWeakSetEntries(objectActor); + + grip.preview = { + kind: "ArrayLike", + length: enumEntries.size, + }; + + // Avoid recursive object grips. + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const items = (grip.preview.items = []); + for (const item of enumEntries) { + items.push(item); + if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + Map: [ + function(objectActor, grip) { + const size = DevToolsUtils.getProperty(objectActor.obj, "size"); + if (typeof size != "number") { + return false; + } + + grip.preview = { + kind: "MapLike", + size: size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of PropertyIterators.enumMapEntries(objectActor)) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + WeakMap: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumWeakMapEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + URLSearchParams: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumURLSearchParamsEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + FormData: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumFormDataEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + Headers: [ + function(objectActor, grip) { + // Bug 1863776: Headers can't be yet previewed from workers + if (isWorker) { + return false; + } + const enumEntries = PropertyIterators.enumHeadersEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + + HighlightRegistry: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumHighlightRegistryEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + MIDIInputMap: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumMidiInputMapEntries( + objectActor + ); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + MIDIOutputMap: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumMidiOutputMapEntries( + objectActor + ); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + DOMStringMap: [ + function({ obj, hooks }, grip, rawObj) { + if (!rawObj) { + return false; + } + + const keys = obj.getOwnPropertyNames(); + grip.preview = { + kind: "MapLike", + size: keys.length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const key of keys) { + const value = ObjectUtils.makeDebuggeeValueIfNeeded(obj, rawObj[key]); + entries.push([key, hooks.createValueGrip(value)]); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + Promise: [ + function({ obj, hooks }, grip, rawObj) { + const { state, value, reason } = ObjectUtils.getPromiseState(obj); + const ownProperties = Object.create(null); + ownProperties["<state>"] = { value: state }; + let ownPropertiesLength = 1; + + // Only expose <value> or <reason> in top-level promises, to avoid recursion. + // <state> is not problematic because it's a string. + if (hooks.getGripDepth() === 1) { + if (state == "fulfilled") { + ownProperties["<value>"] = { value: hooks.createValueGrip(value) }; + ++ownPropertiesLength; + } else if (state == "rejected") { + ownProperties["<reason>"] = { value: hooks.createValueGrip(reason) }; + ++ownPropertiesLength; + } + } + + grip.preview = { + kind: "Object", + ownProperties, + ownPropertiesLength, + }; + + return true; + }, + ], + + Proxy: [ + function({ obj, hooks }, grip, rawObj) { + // Only preview top-level proxies, avoiding recursion. Otherwise, since both the + // target and handler can also be proxies, we could get an exponential behavior. + if (hooks.getGripDepth() > 1) { + return true; + } + + // The `isProxy` getter of the debuggee object only detects proxies without + // security wrappers. If false, the target and handler are not available. + const hasTargetAndHandler = obj.isProxy; + + grip.preview = { + kind: "Object", + ownProperties: Object.create(null), + ownPropertiesLength: 2 * hasTargetAndHandler, + }; + + if (hasTargetAndHandler) { + Object.assign(grip.preview.ownProperties, { + "<target>": { value: hooks.createValueGrip(obj.proxyTarget) }, + "<handler>": { value: hooks.createValueGrip(obj.proxyHandler) }, + }); + } + + return true; + }, + ], +}; + +/** + * Generic previewer for classes wrapping primitives, like String, + * Number and Boolean. + * + * @param string className + * Class name to expect. + * @param object classObj + * The class to expect, eg. String. The valueOf() method of the class is + * invoked on the given object. + * @param ObjectActor objectActor + * The object actor + * @param Object grip + * The result grip to fill in + * @return Booolean true if the object was handled, false otherwise + */ +function wrappedPrimitivePreviewer( + className, + classObj, + objectActor, + grip, + rawObj +) { + let v = null; + try { + v = classObj.prototype.valueOf.call(rawObj); + } catch (ex) { + // valueOf() can throw if the raw JS object is "misbehaved". + return false; + } + + if (v === null) { + return false; + } + + const { obj, hooks } = objectActor; + + const canHandle = GenericObject(objectActor, grip, rawObj, className); + if (!canHandle) { + return false; + } + + grip.preview.wrappedValue = hooks.createValueGrip( + ObjectUtils.makeDebuggeeValueIfNeeded(obj, v) + ); + return true; +} + +/** + * @param {ObjectActor} objectActor + * @param {Object} grip: The grip built by the objectActor, for which we need to populate + * the `preview` property. + * @param {*} rawObj: The native js object + * @param {String} className: objectActor.obj.class + * @returns + */ +function GenericObject(objectActor, grip, rawObj, className) { + const { obj, hooks } = objectActor; + if (grip.preview || grip.displayString || hooks.getGripDepth() > 1) { + return false; + } + + const preview = (grip.preview = { + kind: "Object", + ownProperties: Object.create(null), + }); + + const names = ObjectUtils.getPropNamesFromObject(obj, rawObj); + preview.ownPropertiesLength = names.length; + + let length, + i = 0; + let specialStringBehavior = className === "String"; + if (specialStringBehavior) { + length = DevToolsUtils.getProperty(obj, "length"); + if (typeof length != "number") { + specialStringBehavior = false; + } + } + + for (const name of names) { + if (specialStringBehavior && /^[0-9]+$/.test(name)) { + const num = parseInt(name, 10); + if (num.toString() === name && num >= 0 && num < length) { + continue; + } + } + + const desc = objectActor._propertyDescriptor(name, true); + if (!desc) { + continue; + } + + preview.ownProperties[name] = desc; + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + if (i === OBJECT_PREVIEW_MAX_ITEMS) { + return true; + } + + const privatePropertiesSymbols = ObjectUtils.getSafePrivatePropertiesSymbols( + obj + ); + if (privatePropertiesSymbols.length > 0) { + preview.privatePropertiesLength = privatePropertiesSymbols.length; + preview.privateProperties = []; + + // Retrieve private properties, which are represented as non-enumerable Symbols + for (const privateProperty of privatePropertiesSymbols) { + if ( + !privateProperty.description || + !privateProperty.description.startsWith("#") + ) { + continue; + } + const descriptor = objectActor._propertyDescriptor(privateProperty); + if (!descriptor) { + continue; + } + + preview.privateProperties.push( + Object.assign( + { + descriptor, + }, + hooks.createValueGrip(privateProperty) + ) + ); + + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + + if (i === OBJECT_PREVIEW_MAX_ITEMS) { + return true; + } + + const symbols = ObjectUtils.getSafeOwnPropertySymbols(obj); + if (symbols.length > 0) { + preview.ownSymbolsLength = symbols.length; + preview.ownSymbols = []; + + for (const symbol of symbols) { + const descriptor = objectActor._propertyDescriptor(symbol, true); + if (!descriptor) { + continue; + } + + preview.ownSymbols.push( + Object.assign( + { + descriptor, + }, + hooks.createValueGrip(symbol) + ) + ); + + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + + if (i === OBJECT_PREVIEW_MAX_ITEMS) { + return true; + } + + const safeGetterValues = objectActor._findSafeGetterValues( + Object.keys(preview.ownProperties), + OBJECT_PREVIEW_MAX_ITEMS - i + ); + if (Object.keys(safeGetterValues).length) { + preview.safeGetterValues = safeGetterValues; + } + + return true; +} + +// Preview functions that do not rely on the object class. +previewers.Object = [ + function TypedArray({ obj, hooks }, grip) { + if (!ObjectUtils.isTypedArray(obj)) { + return false; + } + + grip.preview = { + kind: "ArrayLike", + length: ObjectUtils.getArrayLength(obj), + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + const previewLength = Math.min( + OBJECT_PREVIEW_MAX_ITEMS, + grip.preview.length + ); + grip.preview.items = []; + for (let i = 0; i < previewLength; i++) { + const desc = obj.getOwnPropertyDescriptor(i); + if (!desc) { + break; + } + grip.preview.items.push(desc.value); + } + + return true; + }, + + function Error(objectActor, grip, rawObj, className) { + if (!ERROR_CLASSNAMES.has(className)) { + return false; + } + + const { hooks, obj } = objectActor; + + // The name and/or message could be getters, and even if it's unsafe, we do want + // to show it to the user (See Bug 1710694). + const name = DevToolsUtils.getProperty(obj, "name", true); + const msg = DevToolsUtils.getProperty(obj, "message", true); + const stack = DevToolsUtils.getProperty(obj, "stack"); + const fileName = DevToolsUtils.getProperty(obj, "fileName"); + const lineNumber = DevToolsUtils.getProperty(obj, "lineNumber"); + const columnNumber = DevToolsUtils.getProperty(obj, "columnNumber"); + + grip.preview = { + kind: "Error", + name: hooks.createValueGrip(name), + message: hooks.createValueGrip(msg), + stack: hooks.createValueGrip(stack), + fileName: hooks.createValueGrip(fileName), + lineNumber: hooks.createValueGrip(lineNumber), + columnNumber: hooks.createValueGrip(columnNumber), + }; + + const errorHasCause = obj.getOwnPropertyNames().includes("cause"); + if (errorHasCause) { + grip.preview.cause = hooks.createValueGrip( + DevToolsUtils.getProperty(obj, "cause", true) + ); + } + + return true; + }, + + function CSSMediaRule(objectActor, grip, rawObj, className) { + if (!rawObj || className != "CSSMediaRule" || isWorker) { + return false; + } + const { hooks } = objectActor; + grip.preview = { + kind: "ObjectWithText", + text: hooks.createValueGrip(rawObj.conditionText), + }; + return true; + }, + + function CSSStyleRule(objectActor, grip, rawObj, className) { + if (!rawObj || className != "CSSStyleRule" || isWorker) { + return false; + } + const { hooks } = objectActor; + grip.preview = { + kind: "ObjectWithText", + text: hooks.createValueGrip(rawObj.selectorText), + }; + return true; + }, + + function ObjectWithURL(objectActor, grip, rawObj, className) { + if (isWorker || !rawObj) { + return false; + } + + const isWindow = Window.isInstance(rawObj); + if (!OBJECT_WITH_URL_CLASSNAMES.has(className) && !isWindow) { + return false; + } + + const { hooks } = objectActor; + + let url; + if (isWindow && rawObj.location) { + try { + url = rawObj.location.href; + } catch(e) { + // This can happen when we have a cross-process window. + // In such case, let's retrieve the url from the iframe. + // For window.top from a remote iframe, there's no way we can't retrieve the URL, + // so return a label that help user know what's going on. + url = rawObj.browsingContext?.embedderElement?.src || "Restricted"; + } + } else if (rawObj.href) { + url = rawObj.href; + } else { + return false; + } + + grip.preview = { + kind: "ObjectWithURL", + url: hooks.createValueGrip(url), + }; + + return true; + }, + + function ArrayLike(objectActor, grip, rawObj, className) { + if ( + !rawObj || + !ARRAY_LIKE_CLASSNAMES.has(className) || + typeof rawObj.length != "number" || + isWorker + ) { + return false; + } + + const { obj, hooks } = objectActor; + grip.preview = { + kind: "ArrayLike", + length: rawObj.length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + const items = (grip.preview.items = []); + + for ( + let i = 0; + i < rawObj.length && items.length < OBJECT_PREVIEW_MAX_ITEMS; + i++ + ) { + const value = ObjectUtils.makeDebuggeeValueIfNeeded(obj, rawObj[i]); + items.push(hooks.createValueGrip(value)); + } + + return true; + }, + + function CSSStyleDeclaration(objectActor, grip, rawObj, className) { + if ( + !rawObj || + (className != "CSSStyleDeclaration" && className != "CSS2Properties") || + isWorker + ) { + return false; + } + + const { hooks } = objectActor; + grip.preview = { + kind: "MapLike", + size: rawObj.length, + }; + + const entries = (grip.preview.entries = []); + + for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS && i < rawObj.length; i++) { + const prop = rawObj[i]; + const value = rawObj.getPropertyValue(prop); + entries.push([prop, hooks.createValueGrip(value)]); + } + + return true; + }, + + function DOMNode(objectActor, grip, rawObj, className) { + if ( + className == "Object" || + !rawObj || + !Node.isInstance(rawObj) || + isWorker + ) { + return false; + } + + const { obj, hooks } = objectActor; + + const preview = (grip.preview = { + kind: "DOMNode", + nodeType: rawObj.nodeType, + nodeName: rawObj.nodeName, + isConnected: rawObj.isConnected === true, + }); + + if (rawObj.nodeType == rawObj.DOCUMENT_NODE && rawObj.location) { + preview.location = hooks.createValueGrip(rawObj.location.href); + } else if (obj.class == "DocumentFragment") { + preview.childNodesLength = rawObj.childNodes.length; + + if (hooks.getGripDepth() < 2) { + preview.childNodes = []; + for (const node of rawObj.childNodes) { + const actor = hooks.createValueGrip(obj.makeDebuggeeValue(node)); + preview.childNodes.push(actor); + if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + } else if (Element.isInstance(rawObj)) { + // For HTML elements (in an HTML document, at least), the nodeName is an + // uppercased version of the actual element name. Check for HTML + // elements, that is elements in the HTML namespace, and lowercase the + // nodeName in that case. + if (rawObj.namespaceURI == "http://www.w3.org/1999/xhtml") { + preview.nodeName = preview.nodeName.toLowerCase(); + } + + // Add preview for DOM element attributes. + preview.attributes = {}; + preview.attributesLength = rawObj.attributes.length; + for (const attr of rawObj.attributes) { + preview.attributes[attr.nodeName] = hooks.createValueGrip(attr.value); + } + } else if (obj.class == "Attr") { + preview.value = hooks.createValueGrip(rawObj.value); + } else if ( + obj.class == "Text" || + obj.class == "CDATASection" || + obj.class == "Comment" + ) { + preview.textContent = hooks.createValueGrip(rawObj.textContent); + } + + return true; + }, + + function DOMEvent(objectActor, grip, rawObj) { + if (!rawObj || !Event.isInstance(rawObj) || isWorker) { + return false; + } + + const { obj, hooks } = objectActor; + const preview = (grip.preview = { + kind: "DOMEvent", + type: rawObj.type, + properties: Object.create(null), + }); + + if (hooks.getGripDepth() < 2) { + const target = obj.makeDebuggeeValue(rawObj.target); + preview.target = hooks.createValueGrip(target); + } + + if (obj.class == "KeyboardEvent") { + preview.eventKind = "key"; + preview.modifiers = ObjectUtils.getModifiersForEvent(rawObj); + } + + const props = ObjectUtils.getPropsForEvent(obj.class); + + // Add event-specific properties. + for (const prop of props) { + let value = rawObj[prop]; + if (ObjectUtils.isObjectOrFunction(value)) { + // Skip properties pointing to objects. + if (hooks.getGripDepth() > 1) { + continue; + } + value = obj.makeDebuggeeValue(value); + } + preview.properties[prop] = hooks.createValueGrip(value); + } + + // Add any properties we find on the event object. + if (!props.length) { + let i = 0; + for (const prop in rawObj) { + let value = rawObj[prop]; + if ( + prop == "target" || + prop == "type" || + value === null || + typeof value == "function" + ) { + continue; + } + if (value && typeof value == "object") { + if (hooks.getGripDepth() > 1) { + continue; + } + value = obj.makeDebuggeeValue(value); + } + preview.properties[prop] = hooks.createValueGrip(value); + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + + return true; + }, + + function DOMException(objectActor, grip, rawObj, className) { + if (!rawObj || className !== "DOMException" || isWorker) { + return false; + } + + const { hooks } = objectActor; + grip.preview = { + kind: "DOMException", + name: hooks.createValueGrip(rawObj.name), + message: hooks.createValueGrip(rawObj.message), + code: hooks.createValueGrip(rawObj.code), + result: hooks.createValueGrip(rawObj.result), + filename: hooks.createValueGrip(rawObj.filename), + lineNumber: hooks.createValueGrip(rawObj.lineNumber), + columnNumber: hooks.createValueGrip(rawObj.columnNumber), + stack: hooks.createValueGrip(rawObj.stack), + }; + + return true; + }, + + function Object(objectActor, grip, rawObj, className) { + return GenericObject(objectActor, grip, rawObj, className); + }, +]; + +module.exports = previewers; diff --git a/devtools/server/actors/object/private-properties-iterator.js b/devtools/server/actors/object/private-properties-iterator.js new file mode 100644 index 0000000000..12dc7c98e8 --- /dev/null +++ b/devtools/server/actors/object/private-properties-iterator.js @@ -0,0 +1,70 @@ +/* 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.js"); +const { + privatePropertiesIteratorSpec, +} = require("resource://devtools/shared/specs/private-properties-iterator.js"); + +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); + +/** + * Creates an actor to iterate over an object's private properties. + * + * @param objectActor ObjectActor + * The object actor. + */ +class PrivatePropertiesIteratorActor extends Actor { + constructor(objectActor, conn) { + super(conn, privatePropertiesIteratorSpec); + + let privateProperties = []; + if (DevToolsUtils.isSafeDebuggerObject(objectActor.obj)) { + try { + privateProperties = objectActor.obj.getOwnPrivateProperties(); + } catch (err) { + // The above can throw when the debuggee does not subsume the object's + // compartment, or for some WrappedNatives like Cu.Sandbox. + } + } + + this.iterator = { + size: privateProperties.length, + propertyDescription(index) { + // private properties are represented as Symbols on platform + const symbol = privateProperties[index]; + return { + name: symbol.description, + descriptor: objectActor._propertyDescriptor(symbol), + }; + }, + }; + } + + form() { + return { + type: this.typeName, + actor: this.actorID, + count: this.iterator.size, + }; + } + + slice({ start, count }) { + const privateProperties = []; + for (let i = start, m = start + count; i < m; i++) { + privateProperties.push(this.iterator.propertyDescription(i)); + } + return { + privateProperties, + }; + } + + all() { + return this.slice({ start: 0, count: this.iterator.size }); + } + } + +exports.PrivatePropertiesIteratorActor = PrivatePropertiesIteratorActor; diff --git a/devtools/server/actors/object/property-iterator.js b/devtools/server/actors/object/property-iterator.js new file mode 100644 index 0000000000..7bd2c0a704 --- /dev/null +++ b/devtools/server/actors/object/property-iterator.js @@ -0,0 +1,685 @@ +/* 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.js"); +const { + propertyIteratorSpec, +} = require("resource://devtools/shared/specs/property-iterator.js"); + +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +loader.lazyRequireGetter( + this, + "ObjectUtils", + "resource://devtools/server/actors/object/utils.js" +); + +/** + * Creates an actor to iterate over an object's property names and values. + * + * @param objectActor ObjectActor + * The object actor. + * @param options Object + * A dictionary object with various boolean attributes: + * - enumEntries Boolean + * If true, enumerates the entries of a Map or Set object + * instead of enumerating properties. + * - 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. + * - query String + * If non-empty, will filter the properties by names and values + * containing this query string. The match is not case-sensitive. + * Regarding value filtering it just compare to the stringification + * of the property value. + */ +class PropertyIteratorActor extends Actor { + constructor(objectActor, options, conn) { + super(conn, propertyIteratorSpec); + if (!DevToolsUtils.isSafeDebuggerObject(objectActor.obj)) { + this.iterator = { + size: 0, + propertyName: index => undefined, + propertyDescription: index => undefined, + }; + } else if (options.enumEntries) { + const cls = objectActor.obj.class; + if (cls == "Map") { + this.iterator = enumMapEntries(objectActor); + } else if (cls == "WeakMap") { + this.iterator = enumWeakMapEntries(objectActor); + } else if (cls == "Set") { + this.iterator = enumSetEntries(objectActor); + } else if (cls == "WeakSet") { + this.iterator = enumWeakSetEntries(objectActor); + } else if (cls == "Storage") { + this.iterator = enumStorageEntries(objectActor); + } else if (cls == "URLSearchParams") { + this.iterator = enumURLSearchParamsEntries(objectActor); + } else if (cls == "Headers") { + this.iterator = enumHeadersEntries(objectActor); + } else if (cls == "HighlightRegistry") { + this.iterator = enumHighlightRegistryEntries(objectActor); + } else if (cls == "FormData") { + this.iterator = enumFormDataEntries(objectActor); + } else if (cls == "MIDIInputMap") { + this.iterator = enumMidiInputMapEntries(objectActor); + } else if (cls == "MIDIOutputMap") { + this.iterator = enumMidiOutputMapEntries(objectActor); + } else { + throw new Error( + "Unsupported class to enumerate entries from: " + cls + ); + } + } else if ( + ObjectUtils.isArray(objectActor.obj) && + options.ignoreNonIndexedProperties && + !options.query + ) { + this.iterator = enumArrayProperties(objectActor, options); + } else { + this.iterator = enumObjectProperties(objectActor, options); + } + } + + form() { + return { + type: this.typeName, + actor: this.actorID, + count: this.iterator.size, + }; + } + + names({ indexes }) { + const list = []; + for (const idx of indexes) { + list.push(this.iterator.propertyName(idx)); + } + return indexes; + } + + slice({ start, count }) { + const ownProperties = Object.create(null); + for (let i = start, m = start + count; i < m; i++) { + const name = this.iterator.propertyName(i); + ownProperties[name] = this.iterator.propertyDescription(i); + } + + return { + ownProperties, + }; + } + + all() { + return this.slice({ start: 0, count: this.iterator.size }); + } + } + +function waiveXrays(obj) { + return isWorker ? obj : Cu.waiveXrays(obj); +} + +function unwaiveXrays(obj) { + return isWorker ? obj : Cu.unwaiveXrays(obj); +} + +/** + * Helper function to create a grip from a Map/Set entry + */ +function gripFromEntry({ obj, hooks }, entry) { + entry = unwaiveXrays(entry); + return hooks.createValueGrip( + ObjectUtils.makeDebuggeeValueIfNeeded(obj, entry) + ); +} + +function enumArrayProperties(objectActor, options) { + return { + size: ObjectUtils.getArrayLength(objectActor.obj), + propertyName(index) { + return index; + }, + propertyDescription(index) { + return objectActor._propertyDescriptor(index); + }, + }; +} + +function enumObjectProperties(objectActor, options) { + let names = []; + try { + names = objectActor.obj.getOwnPropertyNames(); + } catch (ex) { + // Calling getOwnPropertyNames() on some wrapped native prototypes is not + // allowed: "cannot modify properties of a WrappedNative". See bug 952093. + } + + if (options.ignoreNonIndexedProperties || options.ignoreIndexedProperties) { + const length = DevToolsUtils.getProperty(objectActor.obj, "length"); + let sliceIndex; + + const isLengthTrustworthy = + isUint32(length) && + (!length || ObjectUtils.isArrayIndex(names[length - 1])) && + !ObjectUtils.isArrayIndex(names[length]); + + if (!isLengthTrustworthy) { + // The length property may not reflect what the object looks like, let's find + // where indexed properties end. + + if (!ObjectUtils.isArrayIndex(names[0])) { + // If the first item is not a number, this means there is no indexed properties + // in this object. + sliceIndex = 0; + } else { + sliceIndex = names.length; + while (sliceIndex > 0) { + if (ObjectUtils.isArrayIndex(names[sliceIndex - 1])) { + break; + } + sliceIndex--; + } + } + } else { + sliceIndex = length; + } + + // It appears that getOwnPropertyNames always returns indexed properties + // first, so we can safely slice `names` for/against indexed properties. + // We do such clever operation to optimize very large array inspection. + if (options.ignoreIndexedProperties) { + // Keep items after `sliceIndex` index + names = names.slice(sliceIndex); + } else if (options.ignoreNonIndexedProperties) { + // Keep `sliceIndex` first items + names.length = sliceIndex; + } + } + + const safeGetterValues = objectActor._findSafeGetterValues(names); + const safeGetterNames = Object.keys(safeGetterValues); + // Merge the safe getter values into the existing properties list. + for (const name of safeGetterNames) { + if (!names.includes(name)) { + names.push(name); + } + } + + if (options.query) { + let { query } = options; + query = query.toLowerCase(); + names = names.filter(name => { + // Filter on attribute names + if (name.toLowerCase().includes(query)) { + return true; + } + // and then on attribute values + let desc; + try { + desc = objectActor.obj.getOwnPropertyDescriptor(name); + } catch (e) { + // Calling getOwnPropertyDescriptor on wrapped native prototypes is not + // allowed (bug 560072). + } + if (desc?.value && String(desc.value).includes(query)) { + return true; + } + return false; + }); + } + + if (options.sort) { + names.sort(); + } + + return { + size: names.length, + propertyName(index) { + return names[index]; + }, + propertyDescription(index) { + const name = names[index]; + let desc = objectActor._propertyDescriptor(name); + if (!desc) { + desc = safeGetterValues[name]; + } else if (name in safeGetterValues) { + // Merge the safe getter values into the existing properties list. + const { getterValue, getterPrototypeLevel } = safeGetterValues[name]; + desc.getterValue = getterValue; + desc.getterPrototypeLevel = getterPrototypeLevel; + } + return desc; + }, + }; +} + +function getMapEntries(obj) { + // Iterating over a Map via .entries goes through various intermediate + // objects - an Iterator object, then a 2-element Array object, then the + // actual values we care about. We don't have Xrays to Iterator objects, + // so we get Opaque wrappers for them. And even though we have Xrays to + // Arrays, the semantics often deny access to the entires based on the + // nature of the values. So we need waive Xrays for the iterator object + // and the tupes, and then re-apply them on the underlying values until + // we fix bug 1023984. + // + // Even then though, we might want to continue waiving Xrays here for the + // same reason we do so for Arrays above - this filtering behavior is likely + // to be more confusing than beneficial in the case of Object previews. + const raw = obj.unsafeDereference(); + const iterator = obj.makeDebuggeeValue( + waiveXrays(Map.prototype.keys.call(raw)) + ); + return [...DevToolsUtils.makeDebuggeeIterator(iterator)].map(k => { + const key = waiveXrays(ObjectUtils.unwrapDebuggeeValue(k)); + const value = Map.prototype.get.call(raw, key); + return [key, value]; + }); +} + +function enumMapEntries(objectActor) { + const entries = getMapEntries(objectActor.obj); + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, value].map(val => gripFromEntry(objectActor, val)); + } + }, + size: entries.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const [key, val] = entries[index]; + return { + enumerable: true, + value: { + type: "mapEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, val), + }, + }, + }; + }, + }; +} + +function enumStorageEntries(objectActor) { + // Iterating over local / sessionStorage entries goes through various + // intermediate objects - an Iterator object, then a 2-element Array object, + // then the actual values we care about. We don't have Xrays to Iterator + // objects, so we get Opaque wrappers for them. + const raw = objectActor.obj.unsafeDereference(); + const keys = []; + for (let i = 0; i < raw.length; i++) { + keys.push(raw.key(i)); + } + return { + [Symbol.iterator]: function*() { + for (const key of keys) { + const value = raw.getItem(key); + yield [key, value].map(val => gripFromEntry(objectActor, val)); + } + }, + size: keys.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const key = keys[index]; + const val = raw.getItem(key); + return { + enumerable: true, + value: { + type: "storageEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, val), + }, + }, + }; + }, + }; +} + +function enumURLSearchParamsEntries(objectActor) { + let obj = objectActor.obj; + let raw = obj.unsafeDereference(); + const entries = [...waiveXrays(URLSearchParams.prototype.entries.call(raw))]; + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, value]; + } + }, + size: entries.length, + propertyName(index) { + // UrlSearchParams entries can have the same key multiple times (e.g. `?a=1&a=2`), + // so let's return the index as a name to be able to display them properly in the client. + return index; + }, + propertyDescription(index) { + const [key, value] = entries[index]; + + return { + enumerable: true, + value: { + type: "urlSearchParamsEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, value), + }, + }, + }; + }, + }; +} + +function enumFormDataEntries(objectActor) { + let obj = objectActor.obj; + let raw = obj.unsafeDereference(); + const entries = [...waiveXrays(FormData.prototype.entries.call(raw))]; + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, value]; + } + }, + size: entries.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const [key, value] = entries[index]; + + return { + enumerable: true, + value: { + type: "formDataEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, value), + }, + }, + }; + }, + }; +} + +function enumHeadersEntries(objectActor) { + let raw = objectActor.obj.unsafeDereference(); + const entries = [...waiveXrays(Headers.prototype.entries.call(raw))]; + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, value]; + } + }, + size: entries.length, + propertyName(index) { + return entries[index][0]; + }, + propertyDescription(index) { + return { + enumerable: true, + value: gripFromEntry(objectActor, entries[index][1]), + }; + }, + }; +} + +function enumHighlightRegistryEntries(objectActor) { + const entriesFuncDbgObj = objectActor.obj.getProperty("entries").return; + const entriesDbgObj = entriesFuncDbgObj ? entriesFuncDbgObj.call(objectActor.obj).return : null; + const entries = entriesDbgObj + ? [...waiveXrays( entriesDbgObj.unsafeDereference())] + : []; + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, gripFromEntry(objectActor, value)]; + } + }, + size: entries.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const [key, value] = entries[index]; + return { + enumerable: true, + value: { + type: "highlightRegistryEntry", + preview: { + key: key, + value: gripFromEntry(objectActor, value), + }, + }, + }; + }, + }; +} + +function enumMidiInputMapEntries(objectActor) { + let raw = objectActor.obj.unsafeDereference(); + // We need to waive `raw` as we can't get the iterator from the Xray for MapLike (See Bug 1173651). + // We also need to waive Xrays on the result of the call to `entries` as we don't have + // Xrays to Iterator objects (see Bug 1023984) + const entries = Array.from( + waiveXrays(MIDIInputMap.prototype.entries.call(waiveXrays(raw))) + ); + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, gripFromEntry(objectActor, value)]; + } + }, + size: entries.length, + propertyName(index) { + return entries[index][0]; + }, + propertyDescription(index) { + return { + enumerable: true, + value: gripFromEntry(objectActor, entries[index][1]), + }; + }, + }; +} + +function enumMidiOutputMapEntries(objectActor) { + let raw = objectActor.obj.unsafeDereference(); + // We need to waive `raw` as we can't get the iterator from the Xray for MapLike (See Bug 1173651). + // We also need to waive Xrays on the result of the call to `entries` as we don't have + // Xrays to Iterator objects (see Bug 1023984) + const entries = Array.from( + waiveXrays(MIDIOutputMap.prototype.entries.call(waiveXrays(raw))) + ); + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, gripFromEntry(objectActor, value)]; + } + }, + size: entries.length, + propertyName(index) { + return entries[index][0]; + }, + propertyDescription(index) { + return { + enumerable: true, + value: gripFromEntry(objectActor, entries[index][1]), + }; + }, + }; +} + +function getWeakMapEntries(obj) { + // We currently lack XrayWrappers for WeakMap, so when we iterate over + // the values, the temporary iterator objects get created in the target + // compartment. However, we _do_ have Xrays to Object now, so we end up + // Xraying those temporary objects, and filtering access to |it.value| + // based on whether or not it's Xrayable and/or callable, which breaks + // the for/of iteration. + // + // This code is designed to handle untrusted objects, so we can safely + // waive Xrays on the iterable, and relying on the Debugger machinery to + // make sure we handle the resulting objects carefully. + const raw = obj.unsafeDereference(); + const keys = waiveXrays(ChromeUtils.nondeterministicGetWeakMapKeys(raw)); + + return keys.map(k => [k, WeakMap.prototype.get.call(raw, k)]); +} + +function enumWeakMapEntries(objectActor) { + const entries = getWeakMapEntries(objectActor.obj); + + return { + [Symbol.iterator]: function*() { + for (let i = 0; i < entries.length; i++) { + yield entries[i].map(val => gripFromEntry(objectActor, val)); + } + }, + size: entries.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const [key, val] = entries[index]; + return { + enumerable: true, + value: { + type: "mapEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, val), + }, + }, + }; + }, + }; +} + +function getSetValues(obj) { + // We currently lack XrayWrappers for Set, so when we iterate over + // the values, the temporary iterator objects get created in the target + // compartment. However, we _do_ have Xrays to Object now, so we end up + // Xraying those temporary objects, and filtering access to |it.value| + // based on whether or not it's Xrayable and/or callable, which breaks + // the for/of iteration. + // + // This code is designed to handle untrusted objects, so we can safely + // waive Xrays on the iterable, and relying on the Debugger machinery to + // make sure we handle the resulting objects carefully. + const raw = obj.unsafeDereference(); + const iterator = obj.makeDebuggeeValue( + waiveXrays(Set.prototype.values.call(raw)) + ); + return [...DevToolsUtils.makeDebuggeeIterator(iterator)]; +} + +function enumSetEntries(objectActor) { + const values = getSetValues(objectActor.obj).map(v => + waiveXrays(ObjectUtils.unwrapDebuggeeValue(v)) + ); + + return { + [Symbol.iterator]: function*() { + for (const item of values) { + yield gripFromEntry(objectActor, item); + } + }, + size: values.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const val = values[index]; + return { + enumerable: true, + value: gripFromEntry(objectActor, val), + }; + }, + }; +} + +function getWeakSetEntries(obj) { + // We currently lack XrayWrappers for WeakSet, so when we iterate over + // the values, the temporary iterator objects get created in the target + // compartment. However, we _do_ have Xrays to Object now, so we end up + // Xraying those temporary objects, and filtering access to |it.value| + // based on whether or not it's Xrayable and/or callable, which breaks + // the for/of iteration. + // + // This code is designed to handle untrusted objects, so we can safely + // waive Xrays on the iterable, and relying on the Debugger machinery to + // make sure we handle the resulting objects carefully. + const raw = obj.unsafeDereference(); + return waiveXrays(ChromeUtils.nondeterministicGetWeakSetKeys(raw)); +} + +function enumWeakSetEntries(objectActor) { + const keys = getWeakSetEntries(objectActor.obj); + + return { + [Symbol.iterator]: function*() { + for (const item of keys) { + yield gripFromEntry(objectActor, item); + } + }, + size: keys.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const val = keys[index]; + return { + enumerable: true, + value: gripFromEntry(objectActor, val), + }; + }, + }; +} + +/** + * Returns true if the parameter can be stored as a 32-bit unsigned integer. + * If so, it will be suitable for use as the length of an array object. + * + * @param num Number + * The number to test. + * @return Boolean + */ +function isUint32(num) { + return num >>> 0 === num; +} + +module.exports = { + PropertyIteratorActor, + enumMapEntries, + enumMidiInputMapEntries, + enumMidiOutputMapEntries, + enumSetEntries, + enumURLSearchParamsEntries, + enumFormDataEntries, + enumHeadersEntries, + enumHighlightRegistryEntries, + enumWeakMapEntries, + enumWeakSetEntries, +}; diff --git a/devtools/server/actors/object/symbol-iterator.js b/devtools/server/actors/object/symbol-iterator.js new file mode 100644 index 0000000000..e0b05f9cd1 --- /dev/null +++ b/devtools/server/actors/object/symbol-iterator.js @@ -0,0 +1,67 @@ +/* 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.js"); +const { symbolIteratorSpec } = require("resource://devtools/shared/specs/symbol-iterator.js"); + +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); + +/** + * Creates an actor to iterate over an object's symbols. + * + * @param objectActor ObjectActor + * The object actor. + */ +class SymbolIteratorActor extends Actor { + constructor(objectActor, conn) { + super(conn, symbolIteratorSpec); + + let symbols = []; + if (DevToolsUtils.isSafeDebuggerObject(objectActor.obj)) { + try { + symbols = objectActor.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. + } + } + + this.iterator = { + size: symbols.length, + symbolDescription(index) { + const symbol = symbols[index]; + return { + name: symbol.toString(), + descriptor: objectActor._propertyDescriptor(symbol), + }; + }, + }; + } + + form() { + return { + type: this.typeName, + actor: this.actorID, + count: this.iterator.size, + }; + } + + slice({ start, count }) { + const ownSymbols = []; + for (let i = start, m = start + count; i < m; i++) { + ownSymbols.push(this.iterator.symbolDescription(i)); + } + return { + ownSymbols, + }; + } + + all() { + return this.slice({ start: 0, count: this.iterator.size }); + } +} + +exports.SymbolIteratorActor = SymbolIteratorActor; diff --git a/devtools/server/actors/object/symbol.js b/devtools/server/actors/object/symbol.js new file mode 100644 index 0000000000..bd8bb97005 --- /dev/null +++ b/devtools/server/actors/object/symbol.js @@ -0,0 +1,109 @@ +/* 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.js"); +const { symbolSpec } = require("resource://devtools/shared/specs/symbol.js"); +loader.lazyRequireGetter( + this, + "createValueGrip", + "resource://devtools/server/actors/object/utils.js", + true +); + +/** + * Creates an actor for the specified symbol. + * + * @param {DevToolsServerConnection} conn: The connection to the client. + * @param {Symbol} symbol: The symbol we want to create an actor for. + */ +class SymbolActor extends Actor { + constructor(conn, symbol) { + super(conn, symbolSpec); + this.symbol = symbol; + } + + rawValue() { + return this.symbol; + } + + destroy() { + // Because symbolActors is not a weak map, we won't automatically leave + // it so we need to manually leave on destroy so that we don't leak + // memory. + this._releaseActor(); + super.destroy(); + } + + /** + * Returns a grip for this actor for returning in a protocol message. + */ + form() { + const form = { + type: this.typeName, + actor: this.actorID, + }; + const name = getSymbolName(this.symbol); + if (name !== undefined) { + // Create a grip for the name because it might be a longString. + form.name = createValueGrip(name, this.getParent()); + } + return form; + } + + /** + * Handle a request to release this SymbolActor instance. + */ + release() { + // TODO: also check if this.getParent() === threadActor.threadLifetimePool + // when the web console moves away from manually releasing pause-scoped + // actors. + this._releaseActor(); + this.destroy(); + return {}; + } + + _releaseActor() { + const parent = this.getParent(); + if (parent && parent.symbolActors) { + delete parent.symbolActors[this.symbol]; + } + } +} + +const symbolProtoToString = Symbol.prototype.toString; + +function getSymbolName(symbol) { + const name = symbolProtoToString.call(symbol).slice("Symbol(".length, -1); + return name || undefined; +} + +/** + * Create a grip for the given symbol. + * + * @param sym Symbol + * The symbol we are creating a grip for. + * @param pool Pool + * The actor pool where the new actor will be added. + */ +function symbolGrip(sym, pool) { + if (!pool.symbolActors) { + pool.symbolActors = Object.create(null); + } + + if (sym in pool.symbolActors) { + return pool.symbolActors[sym].form(); + } + + const actor = new SymbolActor(pool.conn, sym); + pool.manage(actor); + pool.symbolActors[sym] = actor; + return actor.form(); +} + +module.exports = { + SymbolActor, + symbolGrip, +}; diff --git a/devtools/server/actors/object/utils.js b/devtools/server/actors/object/utils.js new file mode 100644 index 0000000000..d397b4badf --- /dev/null +++ b/devtools/server/actors/object/utils.js @@ -0,0 +1,615 @@ +/* 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 { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +const { assert } = DevToolsUtils; + +loader.lazyRequireGetter( + this, + "LongStringActor", + "resource://devtools/server/actors/string.js", + true +); + +loader.lazyRequireGetter( + this, + "symbolGrip", + "resource://devtools/server/actors/object/symbol.js", + true +); + +loader.lazyRequireGetter( + this, + "ObjectActor", + "resource://devtools/server/actors/object.js", + true +); + +loader.lazyRequireGetter( + this, + "EnvironmentActor", + "resource://devtools/server/actors/environment.js", + true +); + +/** + * Get thisDebugger.Object referent's `promiseState`. + * + * @returns Object + * An object of one of the following forms: + * - { state: "pending" } + * - { state: "fulfilled", value } + * - { state: "rejected", reason } + */ +function getPromiseState(obj) { + if (obj.class != "Promise") { + throw new Error( + "Can't call `getPromiseState` on `Debugger.Object`s that don't " + + "refer to Promise objects." + ); + } + + const state = { state: obj.promiseState }; + if (state.state === "fulfilled") { + state.value = obj.promiseValue; + } else if (state.state === "rejected") { + state.reason = obj.promiseReason; + } + return state; +} + +/** + * Returns true if value is an object or function. + * + * @param value + * @returns {boolean} + */ + +function isObjectOrFunction(value) { + // Handle null, whose typeof is object + if (!value) { + return false; + } + + const type = typeof value; + return type == "object" || type == "function"; +} + +/** + * Make a debuggee value for the given object, if needed. Primitive values + * are left the same. + * + * Use case: you have a raw JS object (after unsafe dereference) and you want to + * send it to the client. In that case you need to use an ObjectActor which + * requires a debuggee value. The Debugger.Object.prototype.makeDebuggeeValue() + * method works only for JS objects and functions. + * + * @param Debugger.Object obj + * @param any value + * @return object + */ +function makeDebuggeeValueIfNeeded(obj, value) { + if (isObjectOrFunction(value)) { + return obj.makeDebuggeeValue(value); + } + return value; +} + +/** + * Convert a debuggee value into the underlying raw object, if needed. + */ +function unwrapDebuggeeValue(value) { + if (value && typeof value == "object") { + return value.unsafeDereference(); + } + return value; +} + +/** + * Create a grip for the given debuggee value. If the value is an object or a long string, + * it will create an actor and add it to the pool + * @param {any} value: The debuggee value. + * @param {Pool} pool: The pool where the created actor will be added. + * @param {Function} makeObjectGrip: Function that will be called to create the grip for + * non-primitive values. + */ +function createValueGrip(value, pool, makeObjectGrip) { + switch (typeof value) { + case "boolean": + return value; + + case "string": + return createStringGrip(pool, value); + + case "number": + if (value === Infinity) { + return { type: "Infinity" }; + } else if (value === -Infinity) { + return { type: "-Infinity" }; + } else if (Number.isNaN(value)) { + return { type: "NaN" }; + } else if (!value && 1 / value === -Infinity) { + return { type: "-0" }; + } + return value; + + case "bigint": + return { + type: "BigInt", + text: value.toString(), + }; + + // TODO(bug 1772157) + // Record/tuple grips aren't fully implemented yet. + case "record": + return { + class: "Record", + }; + case "tuple": + return { + class: "Tuple", + }; + case "undefined": + return { type: "undefined" }; + + case "object": + if (value === null) { + return { type: "null" }; + } else if ( + value.optimizedOut || + value.uninitialized || + value.missingArguments + ) { + // The slot is optimized out, an uninitialized binding, or + // arguments on a dead scope + return { + type: "null", + optimizedOut: value.optimizedOut, + uninitialized: value.uninitialized, + missingArguments: value.missingArguments, + }; + } + return makeObjectGrip(value, pool); + + case "symbol": + return symbolGrip(value, pool); + + default: + assert(false, "Failed to provide a grip for: " + value); + return null; + } +} + +/** + * of passing the value directly over the protocol. + * + * @param str String + * The string we are checking the length of. + */ +function stringIsLong(str) { + return str.length >= DevToolsServer.LONG_STRING_LENGTH; +} + +const TYPED_ARRAY_CLASSES = [ + "Uint8Array", + "Uint8ClampedArray", + "Uint16Array", + "Uint32Array", + "Int8Array", + "Int16Array", + "Int32Array", + "Float32Array", + "Float64Array", + "BigInt64Array", + "BigUint64Array", +]; + +/** + * Returns true if a debuggee object is a typed array. + * + * @param obj Debugger.Object + * The debuggee object to test. + * @return Boolean + */ +function isTypedArray(object) { + return TYPED_ARRAY_CLASSES.includes(object.class); +} + +/** + * Returns true if a debuggee object is an array, including a typed array. + * + * @param obj Debugger.Object + * The debuggee object to test. + * @return Boolean + */ +function isArray(object) { + return isTypedArray(object) || object.class === "Array"; +} + +/** + * Returns the length of an array (or typed array). + * + * @param object Debugger.Object + * The debuggee object of the array. + * @return Number + * @throws if the object is not an array. + */ +function getArrayLength(object) { + if (!isArray(object)) { + throw new Error("Expected an array, got a " + object.class); + } + + // Real arrays have a reliable `length` own property. + if (object.class === "Array") { + return DevToolsUtils.getProperty(object, "length"); + } + + // For typed arrays, `DevToolsUtils.getProperty` is not reliable because the `length` + // getter could be shadowed by an own property, and `getOwnPropertyNames` is + // unnecessarily slow. Obtain the `length` getter safely and call it manually. + const typedProto = Object.getPrototypeOf(Uint8Array.prototype); + const getter = Object.getOwnPropertyDescriptor(typedProto, "length").get; + return getter.call(object.unsafeDereference()); +} + +/** + * Returns true if the parameter is suitable to be an array index. + * + * @param str String + * @return Boolean + */ +function isArrayIndex(str) { + // Transform the parameter to a 32-bit unsigned integer. + const num = str >>> 0; + // Check that the parameter is a canonical Uint32 index. + return ( + num + "" === str && + // Array indices cannot attain the maximum Uint32 value. + num != -1 >>> 0 + ); +} + +/** + * Returns true if a debuggee object is a local or sessionStorage object. + * + * @param object Debugger.Object + * The debuggee object to test. + * @return Boolean + */ +function isStorage(object) { + return object.class === "Storage"; +} + +/** + * Returns the length of a local or sessionStorage object. + * + * @param object Debugger.Object + * The debuggee object of the array. + * @return Number + * @throws if the object is not a local or sessionStorage object. + */ +function getStorageLength(object) { + if (!isStorage(object)) { + throw new Error("Expected a storage object, got a " + object.class); + } + return DevToolsUtils.getProperty(object, "length"); +} + +/** + * Returns an array of properties based on event class name. + * + * @param className + * @returns {Array} + */ +function getPropsForEvent(className) { + const positionProps = ["buttons", "clientX", "clientY", "layerX", "layerY"]; + const eventToPropsMap = { + MouseEvent: positionProps, + DragEvent: positionProps, + PointerEvent: positionProps, + SimpleGestureEvent: positionProps, + WheelEvent: positionProps, + KeyboardEvent: ["key", "charCode", "keyCode"], + TransitionEvent: ["propertyName", "pseudoElement"], + AnimationEvent: ["animationName", "pseudoElement"], + ClipboardEvent: ["clipboardData"], + }; + + if (className in eventToPropsMap) { + return eventToPropsMap[className]; + } + + return []; +} + +/** + * Returns an array of of all properties of an object + * + * @param obj + * @param rawObj + * @returns {Array|Iterable} If rawObj is localStorage/sessionStorage, we don't return an + * array but an iterable object (with the proper `length` property) to avoid + * performance issues. + */ +function getPropNamesFromObject(obj, rawObj) { + try { + if (isStorage(obj)) { + // local and session storage cannot be iterated over using + // Object.getOwnPropertyNames() because it skips keys that are duplicated + // on the prototype e.g. "key", "getKeys" so we need to gather the real + // keys using the storage.key() function. + // As the method is pretty slow, we return an iterator here, so we don't consume + // more than we need, especially since we're calling this from previewers in which + // we only need the first 10 entries for the preview (See Bug 1741804). + + // Still return the proper number of entries. + const length = rawObj.length; + const iterable = { length }; + iterable[Symbol.iterator] = function*() { + for (let j = 0; j < length; j++) { + yield rawObj.key(j); + } + }; + return iterable; + } + + return obj.getOwnPropertyNames(); + } catch (ex) { + // Calling getOwnPropertyNames() on some wrapped native prototypes is not + // allowed: "cannot modify properties of a WrappedNative". See bug 952093. + } + + return []; +} + +/** + * Returns an array of private properties of an object + * + * @param obj + * @returns {Array} + */ +function getSafePrivatePropertiesSymbols(obj) { + try { + return obj.getOwnPrivateProperties(); + } catch (ex) { + return []; + } +} + +/** + * Returns an array of all symbol properties of an object + * + * @param obj + * @returns {Array} + */ +function getSafeOwnPropertySymbols(obj) { + try { + return obj.getOwnPropertySymbols(); + } catch (ex) { + return []; + } +} + +/** + * Returns an array modifiers based on keys + * + * @param rawObj + * @returns {Array} + */ +function getModifiersForEvent(rawObj) { + const modifiers = []; + const keysToModifiersMap = { + altKey: "Alt", + ctrlKey: "Control", + metaKey: "Meta", + shiftKey: "Shift", + }; + + for (const key in keysToModifiersMap) { + if (keysToModifiersMap.hasOwnProperty(key) && rawObj[key]) { + modifiers.push(keysToModifiersMap[key]); + } + } + + return modifiers; +} + +/** + * Make a debuggee value for the given value. + * + * @param TargetActor targetActor + * The Target Actor from which this object originates. + * @param mixed value + * The value you want to get a debuggee value for. + * @return object + * Debuggee value for |value|. + */ +function makeDebuggeeValue(targetActor, value) { + // Primitive types are debuggee values and Debugger.Object.makeDebuggeeValue + // would return them unchanged. So avoid the expense of: + // getGlobalForObject+makeGlobalObjectReference+makeDebugeeValue for them. + // + // It is actually easier to identify non primitive which can only be object or function. + if (!isObjectOrFunction(value)) { + return value; + } + + // `value` may come from various globals. + // And Debugger.Object.makeDebuggeeValue only works for objects + // related to the same global. So fetch the global first, + // in order to instantiate a Debugger.Object for it. + // + // In the worker thread, we don't have access to Cu, + // but at the same time, there is only one global, the worker one. + const valueGlobal = isWorker ? targetActor.workerGlobal : Cu.getGlobalForObject(value); + let dbgGlobal; + try { + dbgGlobal = targetActor.dbg.makeGlobalObjectReference( + valueGlobal + ); + } catch(e) { + // makeGlobalObjectReference will throw if the global is invisible to Debugger, + // in this case instantiate a Debugger.Object for the top level global + // of the target. Even if value will come from another global, it will "work", + // but the Debugger.Object created via dbgGlobal.makeDebuggeeValue will throw + // on most methods as the object will also be invisible to Debuggee... + if (e.message.includes("object in compartment marked as invisible to Debugger")) { + dbgGlobal = targetActor.dbg.makeGlobalObjectReference( + targetActor.window + ); + + } else { + throw e; + } + } + + return dbgGlobal.makeDebuggeeValue(value); +} + +/** + * Create a grip for the given string. + * + * @param TargetActor targetActor + * The Target Actor from which this object originates. + */ +function createStringGrip(targetActor, string) { + if (string && stringIsLong(string)) { + const actor = new LongStringActor(targetActor.conn, string); + targetActor.manage(actor); + return actor.form(); + } + return string; +} + +/** + * Create a grip for the given value. + * + * @param TargetActor targetActor + * The Target Actor from which this object originates. + * @param mixed value + * The value you want to get a debuggee value for. + * @param Number depth + * Depth of the object compared to the top level object, + * when we are inspecting nested attributes. + * @param Object [objectActorAttributes] + * An optional object whose properties will be assigned to the ObjectActor if one + * is created. + * @return object + */ +function createValueGripForTarget( + targetActor, + value, + depth = 0, + objectActorAttributes = {} +) { + const makeObjectGrip = (objectActorValue, pool) => + createObjectGrip( + targetActor, + depth, + objectActorValue, + pool, + objectActorAttributes + ); + return createValueGrip(value, targetActor, makeObjectGrip); +} + +/** + * Create and return an environment actor that corresponds to the provided + * Debugger.Environment. This is a straightforward clone of the ThreadActor's + * method except that it stores the environment actor in the web console + * actor's pool. + * + * @param Debugger.Environment environment + * The lexical environment we want to extract. + * @param TargetActor targetActor + * The Target Actor to use as parent actor. + * @return The EnvironmentActor for |environment| or |undefined| for host + * functions or functions scoped to a non-debuggee global. + */ +function createEnvironmentActor(environment, targetActor) { + if (!environment) { + return undefined; + } + + if (environment.actor) { + return environment.actor; + } + + const actor = new EnvironmentActor(environment, targetActor); + targetActor.manage(actor); + environment.actor = actor; + + return actor; +} + +/** + * Create a grip for the given object. + * + * @param TargetActor targetActor + * The Target Actor from which this object originates. + * @param Number depth + * Depth of the object compared to the top level object, + * when we are inspecting nested attributes. + * @param object object + * The object you want. + * @param object pool + * A Pool where the new actor instance is added. + * @param object [objectActorAttributes] + * An optional object whose properties will be assigned to the ObjectActor being created. + * @param object + * The object grip. + */ +function createObjectGrip( + targetActor, + depth, + object, + pool, + objectActorAttributes = {} +) { + let gripDepth = depth; + const actor = new ObjectActor( + object, + { + ...objectActorAttributes, + thread: targetActor.threadActor, + getGripDepth: () => gripDepth, + incrementGripDepth: () => gripDepth++, + decrementGripDepth: () => gripDepth--, + createValueGrip: v => createValueGripForTarget(targetActor, v, gripDepth), + createEnvironmentActor: env => createEnvironmentActor(env, targetActor), + }, + targetActor.conn + ); + pool.manage(actor); + + return actor.form(); +} + +module.exports = { + getPromiseState, + makeDebuggeeValueIfNeeded, + unwrapDebuggeeValue, + createValueGrip, + stringIsLong, + isTypedArray, + isArray, + isStorage, + getArrayLength, + getStorageLength, + isArrayIndex, + getPropsForEvent, + getPropNamesFromObject, + getSafeOwnPropertySymbols, + getSafePrivatePropertiesSymbols, + getModifiersForEvent, + isObjectOrFunction, + createStringGrip, + makeDebuggeeValue, + createValueGripForTarget, +}; diff --git a/devtools/server/actors/objects-manager.js b/devtools/server/actors/objects-manager.js new file mode 100644 index 0000000000..529b33b246 --- /dev/null +++ b/devtools/server/actors/objects-manager.js @@ -0,0 +1,39 @@ +/* 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.js"); +const { + objectsManagerSpec, +} = require("resource://devtools/shared/specs/objects-manager.js"); + +/** + * This actor is a singleton per Target which allows interacting with JS Object + * inspected by DevTools. Typically from the Console or Debugger. + */ +class ObjectsManagerActor extends Actor { + constructor(conn, targetActor) { + super(conn, objectsManagerSpec); + } + + /** + * Release Actors by bulk by specifying their actor IDs. + * (Passing the whole Front [i.e. Actor's form] would be more expensive than passing only their IDs) + * + * @param {Array<string>} actorIDs + * List of all actor's IDs to release. + */ + releaseObjects(actorIDs) { + for (const actorID of actorIDs) { + const actor = this.conn.getActor(actorID); + // Note that release will also typically call Actor's destroy and unregister the actor from its Pool + if (actor) { + actor.release(); + } + } + } +} + +exports.ObjectsManagerActor = ObjectsManagerActor; |