summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/object
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/server/actors/object
parentInitial commit. (diff)
downloadfirefox-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.js847
-rw-r--r--devtools/server/actors/object/moz.build12
-rw-r--r--devtools/server/actors/object/previewers.js1142
-rw-r--r--devtools/server/actors/object/private-properties-iterator.js70
-rw-r--r--devtools/server/actors/object/property-iterator.js685
-rw-r--r--devtools/server/actors/object/symbol-iterator.js67
-rw-r--r--devtools/server/actors/object/symbol.js109
-rw-r--r--devtools/server/actors/object/utils.js615
-rw-r--r--devtools/server/actors/objects-manager.js39
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;