summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/object.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/server/actors/object.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/server/actors/object.js')
-rw-r--r--devtools/server/actors/object.js2251
1 files changed, 2251 insertions, 0 deletions
diff --git a/devtools/server/actors/object.js b/devtools/server/actors/object.js
new file mode 100644
index 000000000..1f417b951
--- /dev/null
+++ b/devtools/server/actors/object.js
@@ -0,0 +1,2251 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Cu, Ci } = require("chrome");
+const { GeneratedLocation } = require("devtools/server/actors/common");
+const { DebuggerServer } = require("devtools/server/main");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { assert, dumpn } = DevToolsUtils;
+
+loader.lazyRequireGetter(this, "ThreadSafeChromeUtils");
+
+const TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
+ "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array",
+ "Float64Array"];
+
+// Number of items to preview in objects, arrays, maps, sets, lists,
+// collections, etc.
+const OBJECT_PREVIEW_MAX_ITEMS = 10;
+
+/**
+ * Creates an actor for the specified object.
+ *
+ * @param obj Debugger.Object
+ * The debuggee object.
+ * @param hooks 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
+ * - sources
+ * TabSources getter that manages the sources of a thread
+ * - 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
+ * - globalDebugObject
+ * The Debuggee Global Object as given by the ThreadActor
+ */
+function ObjectActor(obj, {
+ createValueGrip,
+ sources,
+ createEnvironmentActor,
+ getGripDepth,
+ incrementGripDepth,
+ decrementGripDepth,
+ getGlobalDebugObject
+}) {
+ assert(!obj.optimizedOut,
+ "Should not create object actors for optimized out values!");
+ this.obj = obj;
+ this.hooks = {
+ createValueGrip,
+ sources,
+ createEnvironmentActor,
+ getGripDepth,
+ incrementGripDepth,
+ decrementGripDepth,
+ getGlobalDebugObject
+ };
+ this.iterators = new Set();
+}
+
+ObjectActor.prototype = {
+ actorPrefix: "obj",
+
+ /**
+ * Returns a grip for this actor for returning in a protocol message.
+ */
+ grip: function () {
+ this.hooks.incrementGripDepth();
+
+ let g = {
+ "type": "object",
+ "actor": this.actorID
+ };
+
+ // If it's a proxy, lie and tell that it belongs to an invented
+ // "Proxy" class, and avoid calling the [[IsExtensible]] trap
+ if(this.obj.isProxy) {
+ g.class = "Proxy";
+ g.proxyTarget = this.hooks.createValueGrip(this.obj.proxyTarget);
+ g.proxyHandler = this.hooks.createValueGrip(this.obj.proxyHandler);
+ } else {
+ g.class = this.obj.class;
+ g.extensible = this.obj.isExtensible();
+ g.frozen = this.obj.isFrozen();
+ g.sealed = this.obj.isSealed();
+ }
+
+ if (g.class != "DeadObject") {
+ if (g.class == "Promise") {
+ g.promiseState = this._createPromiseState();
+ }
+
+ // FF40+: Allow to know how many properties an object has
+ // to lazily display them when there is a bunch.
+ // Throws on some MouseEvent object in tests.
+ try {
+ // Bug 1163520: Assert on internal functions
+ if (!["Function", "Proxy"].includes(g.class)) {
+ g.ownPropertyLength = this.obj.getOwnPropertyNames().length;
+ }
+ } catch (e) {}
+
+ let raw = this.obj.unsafeDereference();
+
+ // If Cu is not defined, we are running on a worker thread, where xrays
+ // don't exist.
+ if (Cu) {
+ raw = Cu.unwaiveXrays(raw);
+ }
+
+ if (!DevToolsUtils.isSafeJSObject(raw)) {
+ raw = null;
+ }
+
+ let previewers = DebuggerServer.ObjectActorPreviewers[g.class] ||
+ DebuggerServer.ObjectActorPreviewers.Object;
+ for (let fn of previewers) {
+ try {
+ if (fn(this, g, raw)) {
+ break;
+ }
+ } catch (e) {
+ let msg = "ObjectActor.prototype.grip previewer function";
+ DevToolsUtils.reportException(msg, e);
+ }
+ }
+ }
+
+ this.hooks.decrementGripDepth();
+ return g;
+ },
+
+ /**
+ * Returns an object exposing the internal Promise state.
+ */
+ _createPromiseState: function () {
+ const { state, value, reason } = getPromiseState(this.obj);
+ let 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;
+ },
+
+ /**
+ * Releases this actor from the pool.
+ */
+ release: function () {
+ if (this.registeredPool.objectActors) {
+ this.registeredPool.objectActors.delete(this.obj);
+ }
+ this.iterators.forEach(actor => this.registeredPool.removeActor(actor));
+ this.iterators.clear();
+ this.registeredPool.removeActor(this);
+ },
+
+ /**
+ * Handle a protocol request to provide the definition site of this function
+ * object.
+ */
+ onDefinitionSite: function () {
+ if (this.obj.class != "Function") {
+ return {
+ from: this.actorID,
+ error: "objectNotFunction",
+ message: this.actorID + " is not a function."
+ };
+ }
+
+ if (!this.obj.script) {
+ return {
+ from: this.actorID,
+ error: "noScript",
+ message: this.actorID + " has no Debugger.Script"
+ };
+ }
+
+ return this.hooks.sources().getOriginalLocation(new GeneratedLocation(
+ this.hooks.sources().createNonSourceMappedActor(this.obj.script.source),
+ this.obj.script.startLine,
+ 0 // TODO bug 901138: use Debugger.Script.prototype.startColumn
+ )).then((originalLocation) => {
+ return {
+ source: originalLocation.originalSourceActor.form(),
+ line: originalLocation.originalLine,
+ column: originalLocation.originalColumn
+ };
+ });
+ },
+
+ /**
+ * Handle a protocol request to provide the names of the properties defined on
+ * the object and not its prototype.
+ */
+ onOwnPropertyNames: function () {
+ return { from: this.actorID,
+ ownPropertyNames: this.obj.getOwnPropertyNames() };
+ },
+
+ /**
+ * Creates an actor to iterate over an object property names and values.
+ * See PropertyIteratorActor constructor for more info about options param.
+ *
+ * @param request object
+ * The protocol request object.
+ */
+ onEnumProperties: function (request) {
+ let actor = new PropertyIteratorActor(this, request.options);
+ this.registeredPool.addActor(actor);
+ this.iterators.add(actor);
+ return { iterator: actor.grip() };
+ },
+
+ /**
+ * Creates an actor to iterate over entries of a Map/Set-like object.
+ */
+ onEnumEntries: function () {
+ let actor = new PropertyIteratorActor(this, { enumEntries: true });
+ this.registeredPool.addActor(actor);
+ this.iterators.add(actor);
+ return { iterator: actor.grip() };
+ },
+
+ /**
+ * Handle a protocol request to provide the prototype and own properties of
+ * the object.
+ */
+ onPrototypeAndProperties: function () {
+ let ownProperties = Object.create(null);
+ let names;
+ try {
+ names = this.obj.getOwnPropertyNames();
+ } catch (ex) {
+ // The above can throw if this.obj points to a dead object.
+ // TODO: we should use Cu.isDeadWrapper() - see bug 885800.
+ return { from: this.actorID,
+ prototype: this.hooks.createValueGrip(null),
+ ownProperties: ownProperties,
+ safeGetterValues: Object.create(null) };
+ }
+ for (let name of names) {
+ ownProperties[name] = this._propertyDescriptor(name);
+ }
+ return { from: this.actorID,
+ prototype: this.hooks.createValueGrip(this.obj.proto),
+ ownProperties: ownProperties,
+ safeGetterValues: this._findSafeGetterValues(names) };
+ },
+
+ /**
+ * Find the safe getter values for the current Debugger.Object, |this.obj|.
+ *
+ * @private
+ * @param array ownProperties
+ * The array that holds the list of known ownProperties names for
+ * |this.obj|.
+ * @param number [limit=0]
+ * Optional limit of getter values to find.
+ * @return object
+ * An object that maps property names to safe getter descriptors as
+ * defined by the remote debugging protocol.
+ */
+ _findSafeGetterValues: function (ownProperties, limit = 0) {
+ let safeGetterValues = Object.create(null);
+ let obj = this.obj;
+ let level = 0, i = 0;
+
+ // 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 (TYPED_ARRAY_CLASSES.includes(this.obj.class) ||
+ ["Array", "Object", "String"].includes(this.obj.class)) {
+ obj = obj.proto;
+ level++;
+ }
+
+ while (obj) {
+ let getters = this._findSafeGetters(obj);
+ for (let name of getters) {
+ // Avoid overwriting properties from prototypes closer to this.obj. Also
+ // avoid providing safeGetterValues from prototypes if property |name|
+ // is already defined as an own property.
+ if (name in safeGetterValues ||
+ (obj != this.obj && ownProperties.indexOf(name) !== -1)) {
+ continue;
+ }
+
+ // Ignore __proto__ on Object.prototye.
+ if (!obj.proto && name == "__proto__") {
+ continue;
+ }
+
+ let desc = null, getter = null;
+ try {
+ desc = obj.getOwnPropertyDescriptor(name);
+ getter = desc.get;
+ } catch (ex) {
+ // The above can throw if the cache becomes stale.
+ }
+ if (!getter) {
+ obj._safeGetters = null;
+ continue;
+ }
+
+ let result = getter.call(this.obj);
+ if (result && !("throw" in result)) {
+ let getterValue = undefined;
+ if ("return" in result) {
+ getterValue = result.return;
+ } else if ("yield" in result) {
+ getterValue = result.yield;
+ }
+ // WebIDL attributes specified with the LenientThis extended attribute
+ // return undefined and should be ignored.
+ if (getterValue !== undefined) {
+ safeGetterValues[name] = {
+ getterValue: this.hooks.createValueGrip(getterValue),
+ getterPrototypeLevel: level,
+ enumerable: desc.enumerable,
+ writable: level == 0 ? desc.writable : true,
+ };
+ if (limit && ++i == limit) {
+ break;
+ }
+ }
+ }
+ }
+ if (limit && i == limit) {
+ break;
+ }
+
+ obj = obj.proto;
+ level++;
+ }
+
+ return safeGetterValues;
+ },
+
+ /**
+ * Find the safe getters for a given Debugger.Object. Safe getters are native
+ * getters which are safe to execute.
+ *
+ * @private
+ * @param Debugger.Object object
+ * The Debugger.Object where you want to find safe getters.
+ * @return Set
+ * A Set of names of safe getters. This result is cached for each
+ * Debugger.Object.
+ */
+ _findSafeGetters: function (object) {
+ if (object._safeGetters) {
+ return object._safeGetters;
+ }
+
+ let getters = new Set();
+ 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 (let name of names) {
+ let desc = null;
+ try {
+ desc = object.getOwnPropertyDescriptor(name);
+ } catch (e) {
+ // Calling getOwnPropertyDescriptor on wrapped native prototypes is not
+ // allowed (bug 560072).
+ }
+ if (!desc || desc.value !== undefined || !("get" in desc)) {
+ continue;
+ }
+
+ if (DevToolsUtils.hasSafeGetter(desc)) {
+ getters.add(name);
+ }
+ }
+
+ object._safeGetters = getters;
+ return getters;
+ },
+
+ /**
+ * Handle a protocol request to provide the prototype of the object.
+ */
+ onPrototype: function () {
+ return { from: this.actorID,
+ prototype: this.hooks.createValueGrip(this.obj.proto) };
+ },
+
+ /**
+ * Handle a protocol request to provide the property descriptor of the
+ * object's specified property.
+ *
+ * @param request object
+ * The protocol request object.
+ */
+ onProperty: function (request) {
+ if (!request.name) {
+ return { error: "missingParameter",
+ message: "no property name was specified" };
+ }
+
+ return { from: this.actorID,
+ descriptor: this._propertyDescriptor(request.name) };
+ },
+
+ /**
+ * Handle a protocol request to provide the display string for the object.
+ */
+ onDisplayString: function () {
+ const string = stringify(this.obj);
+ return { from: this.actorID,
+ displayString: this.hooks.createValueGrip(string) };
+ },
+
+ /**
+ * A helper method that creates a property descriptor for the provided object,
+ * properly formatted for sending in a protocol response.
+ *
+ * @private
+ * @param string name
+ * The property that the descriptor is generated for.
+ * @param boolean [onlyEnumerable]
+ * Optional: true if you want a descriptor only for an enumerable
+ * property, false otherwise.
+ * @return object|undefined
+ * The property descriptor, or undefined if this is not an enumerable
+ * property and onlyEnumerable=true.
+ */
+ _propertyDescriptor: function (name, onlyEnumerable) {
+ 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 (!desc || onlyEnumerable && !desc.enumerable) {
+ return undefined;
+ }
+
+ let retval = {
+ configurable: desc.configurable,
+ enumerable: desc.enumerable
+ };
+
+ if ("value" in desc) {
+ retval.writable = desc.writable;
+ retval.value = this.hooks.createValueGrip(desc.value);
+ } else {
+ if ("get" in desc) {
+ retval.get = this.hooks.createValueGrip(desc.get);
+ }
+ if ("set" in desc) {
+ retval.set = this.hooks.createValueGrip(desc.set);
+ }
+ }
+ return retval;
+ },
+
+ /**
+ * Handle a protocol request to provide the source code of a function.
+ *
+ * @param request object
+ * The protocol request object.
+ */
+ onDecompile: function (request) {
+ if (this.obj.class !== "Function") {
+ return { error: "objectNotFunction",
+ message: "decompile request is only valid for object grips " +
+ "with a 'Function' class." };
+ }
+
+ return { from: this.actorID,
+ decompiledCode: this.obj.decompile(!!request.pretty) };
+ },
+
+ /**
+ * Handle a protocol request to provide the parameters of a function.
+ */
+ onParameterNames: function () {
+ if (this.obj.class !== "Function") {
+ return { error: "objectNotFunction",
+ message: "'parameterNames' request is only valid for object " +
+ "grips with a 'Function' class." };
+ }
+
+ return { parameterNames: this.obj.parameterNames };
+ },
+
+ /**
+ * Handle a protocol request to release a thread-lifetime grip.
+ */
+ onRelease: function () {
+ this.release();
+ return {};
+ },
+
+ /**
+ * Handle a protocol request to provide the lexical scope of a function.
+ */
+ onScope: function () {
+ if (this.obj.class !== "Function") {
+ return { error: "objectNotFunction",
+ message: "scope request is only valid for object grips with a" +
+ " 'Function' class." };
+ }
+
+ let envActor = this.hooks.createEnvironmentActor(this.obj.environment,
+ this.registeredPool);
+ if (!envActor) {
+ return { error: "notDebuggee",
+ message: "cannot access the environment of this function." };
+ }
+
+ return { from: this.actorID, scope: envActor.form() };
+ },
+
+ /**
+ * Handle a protocol request to get the list of dependent promises of a
+ * promise.
+ *
+ * @return object
+ * Returns an object containing an array of object grips of the
+ * dependent promises
+ */
+ onDependentPromises: function () {
+ if (this.obj.class != "Promise") {
+ return { error: "objectNotPromise",
+ message: "'dependentPromises' request is only valid for " +
+ "object grips with a 'Promise' class." };
+ }
+
+ let promises = this.obj.promiseDependentPromises.map(p => this.hooks.createValueGrip(p));
+
+ return { promises };
+ },
+
+ /**
+ * Handle a protocol request to get the allocation stack of a promise.
+ */
+ onAllocationStack: function () {
+ if (this.obj.class != "Promise") {
+ return { error: "objectNotPromise",
+ message: "'allocationStack' request is only valid for " +
+ "object grips with a 'Promise' class." };
+ }
+
+ let stack = this.obj.promiseAllocationSite;
+ let allocationStacks = [];
+
+ while (stack) {
+ if (stack.source) {
+ let source = this._getSourceOriginalLocation(stack);
+
+ if (source) {
+ allocationStacks.push(source);
+ }
+ }
+ stack = stack.parent;
+ }
+
+ return Promise.all(allocationStacks).then(stacks => {
+ return { allocationStack: stacks };
+ });
+ },
+
+ /**
+ * Handle a protocol request to get the fulfillment stack of a promise.
+ */
+ onFulfillmentStack: function () {
+ if (this.obj.class != "Promise") {
+ return { error: "objectNotPromise",
+ message: "'fulfillmentStack' request is only valid for " +
+ "object grips with a 'Promise' class." };
+ }
+
+ let stack = this.obj.promiseResolutionSite;
+ let fulfillmentStacks = [];
+
+ while (stack) {
+ if (stack.source) {
+ let source = this._getSourceOriginalLocation(stack);
+
+ if (source) {
+ fulfillmentStacks.push(source);
+ }
+ }
+ stack = stack.parent;
+ }
+
+ return Promise.all(fulfillmentStacks).then(stacks => {
+ return { fulfillmentStack: stacks };
+ });
+ },
+
+ /**
+ * Handle a protocol request to get the rejection stack of a promise.
+ */
+ onRejectionStack: function () {
+ if (this.obj.class != "Promise") {
+ return { error: "objectNotPromise",
+ message: "'rejectionStack' request is only valid for " +
+ "object grips with a 'Promise' class." };
+ }
+
+ let stack = this.obj.promiseResolutionSite;
+ let rejectionStacks = [];
+
+ while (stack) {
+ if (stack.source) {
+ let source = this._getSourceOriginalLocation(stack);
+
+ if (source) {
+ rejectionStacks.push(source);
+ }
+ }
+ stack = stack.parent;
+ }
+
+ return Promise.all(rejectionStacks).then(stacks => {
+ return { rejectionStack: stacks };
+ });
+ },
+
+ /**
+ * Helper function for fetching the source location of a SavedFrame stack.
+ *
+ * @param SavedFrame stack
+ * The promise allocation stack frame
+ * @return object
+ * Returns an object containing the source location of the SavedFrame
+ * stack.
+ */
+ _getSourceOriginalLocation: function (stack) {
+ let source;
+
+ // Catch any errors if the source actor cannot be found
+ try {
+ source = this.hooks.sources().getSourceActorByURL(stack.source);
+ } catch (e) {}
+
+ if (!source) {
+ return null;
+ }
+
+ return this.hooks.sources().getOriginalLocation(new GeneratedLocation(
+ source,
+ stack.line,
+ stack.column
+ )).then((originalLocation) => {
+ return {
+ source: originalLocation.originalSourceActor.form(),
+ line: originalLocation.originalLine,
+ column: originalLocation.originalColumn,
+ functionDisplayName: stack.functionDisplayName
+ };
+ });
+ }
+};
+
+ObjectActor.prototype.requestTypes = {
+ "definitionSite": ObjectActor.prototype.onDefinitionSite,
+ "parameterNames": ObjectActor.prototype.onParameterNames,
+ "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties,
+ "enumProperties": ObjectActor.prototype.onEnumProperties,
+ "prototype": ObjectActor.prototype.onPrototype,
+ "property": ObjectActor.prototype.onProperty,
+ "displayString": ObjectActor.prototype.onDisplayString,
+ "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames,
+ "decompile": ObjectActor.prototype.onDecompile,
+ "release": ObjectActor.prototype.onRelease,
+ "scope": ObjectActor.prototype.onScope,
+ "dependentPromises": ObjectActor.prototype.onDependentPromises,
+ "allocationStack": ObjectActor.prototype.onAllocationStack,
+ "fulfillmentStack": ObjectActor.prototype.onFulfillmentStack,
+ "rejectionStack": ObjectActor.prototype.onRejectionStack,
+ "enumEntries": ObjectActor.prototype.onEnumEntries,
+};
+
+/**
+ * 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.
+ */
+function PropertyIteratorActor(objectActor, options) {
+ if (options.enumEntries) {
+ let 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 {
+ throw new Error("Unsupported class to enumerate entries from: " + cls);
+ }
+ } else if (options.ignoreNonIndexedProperties && !options.query) {
+ this.iterator = enumArrayProperties(objectActor, options);
+ } else {
+ this.iterator = enumObjectProperties(objectActor, options);
+ }
+}
+
+PropertyIteratorActor.prototype = {
+ actorPrefix: "propertyIterator",
+
+ grip() {
+ return {
+ type: this.actorPrefix,
+ actor: this.actorID,
+ count: this.iterator.size
+ };
+ },
+
+ names({ indexes }) {
+ let list = [];
+ for (let idx of indexes) {
+ list.push(this.iterator.propertyName(idx));
+ }
+ return {
+ names: indexes
+ };
+ },
+
+ slice({ start, count }) {
+ let ownProperties = Object.create(null);
+ for (let i = start, m = start + count; i < m; i++) {
+ let name = this.iterator.propertyName(i);
+ ownProperties[name] = this.iterator.propertyDescription(i);
+ }
+ return {
+ ownProperties
+ };
+ },
+
+ all() {
+ return this.slice({ start: 0, count: this.length });
+ }
+};
+
+PropertyIteratorActor.prototype.requestTypes = {
+ "names": PropertyIteratorActor.prototype.names,
+ "slice": PropertyIteratorActor.prototype.slice,
+ "all": PropertyIteratorActor.prototype.all,
+};
+
+function enumArrayProperties(objectActor, options) {
+ let length = DevToolsUtils.getProperty(objectActor.obj, "length");
+ if (typeof length !== "number") {
+ // Pseudo arrays are flagged as ArrayLike if they have
+ // subsequent indexed properties without having any length attribute.
+ length = 0;
+ let names = objectActor.obj.getOwnPropertyNames();
+ for (let key of names) {
+ if (isNaN(key) || key != length++) {
+ break;
+ }
+ }
+ }
+
+ return {
+ size: length,
+ 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) {
+ let length = DevToolsUtils.getProperty(objectActor.obj, "length");
+ if (typeof length !== "number") {
+ // Pseudo arrays are flagged as ArrayLike if they have
+ // subsequent indexed properties without having any length attribute.
+ length = 0;
+ for (let key of names) {
+ if (isNaN(key) || key != length++) {
+ break;
+ }
+ }
+ }
+
+ // 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,
+ // like webaudio buffers.
+ if (options.ignoreIndexedProperties) {
+ // Keep items after `length` index
+ names = names.slice(length);
+ } else if (options.ignoreNonIndexedProperties) {
+ // Remove `length` first items
+ names.splice(length);
+ }
+ }
+
+ let safeGetterValues = objectActor._findSafeGetterValues(names, 0);
+ let safeGetterNames = Object.keys(safeGetterValues);
+ // Merge the safe getter values into the existing properties list.
+ for (let 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 && 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) {
+ let 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.
+ let { getterValue, getterPrototypeLevel } = safeGetterValues[name];
+ desc.getterValue = getterValue;
+ desc.getterPrototypeLevel = getterPrototypeLevel;
+ }
+ return desc;
+ }
+ };
+}
+
+/**
+ * Helper function to create a grip from a Map/Set entry
+ */
+function gripFromEntry({ obj, hooks }, entry) {
+ return hooks.createValueGrip(
+ makeDebuggeeValueIfNeeded(obj, Cu.unwaiveXrays(entry)));
+}
+
+function enumMapEntries(objectActor) {
+ // 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.
+ let raw = objectActor.obj.unsafeDereference();
+
+ let keys = [...Cu.waiveXrays(Map.prototype.keys.call(raw))];
+ return {
+ [Symbol.iterator]: function* () {
+ for (let key of keys) {
+ let value = Map.prototype.get.call(raw, key);
+ yield [ key, value ].map(val => gripFromEntry(objectActor, val));
+ }
+ },
+ size: keys.length,
+ propertyName(index) {
+ return index;
+ },
+ propertyDescription(index) {
+ let key = keys[index];
+ let val = Map.prototype.get.call(raw, key);
+ return {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: gripFromEntry(objectActor, key),
+ value: gripFromEntry(objectActor, val)
+ }
+ }
+ };
+ }
+ };
+}
+
+function enumWeakMapEntries(objectActor) {
+ // 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.
+ let raw = objectActor.obj.unsafeDereference();
+ let keys = Cu.waiveXrays(
+ ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(raw));
+
+ return {
+ [Symbol.iterator]: function* () {
+ for (let key of keys) {
+ let value = WeakMap.prototype.get.call(raw, key);
+ yield [ key, value ].map(val => gripFromEntry(objectActor, val));
+ }
+ },
+ size: keys.length,
+ propertyName(index) {
+ return index;
+ },
+ propertyDescription(index) {
+ let key = keys[index];
+ let val = WeakMap.prototype.get.call(raw, key);
+ return {
+ enumerable: true,
+ value: {
+ type: "mapEntry",
+ preview: {
+ key: gripFromEntry(objectActor, key),
+ value: gripFromEntry(objectActor, val)
+ }
+ }
+ };
+ }
+ };
+}
+
+function enumSetEntries(objectActor) {
+ // 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.
+ let raw = objectActor.obj.unsafeDereference();
+ let values = [...Cu.waiveXrays(Set.prototype.values.call(raw))];
+
+ return {
+ [Symbol.iterator]: function* () {
+ for (let item of values) {
+ yield gripFromEntry(objectActor, item);
+ }
+ },
+ size: values.length,
+ propertyName(index) {
+ return index;
+ },
+ propertyDescription(index) {
+ let val = values[index];
+ return {
+ enumerable: true,
+ value: gripFromEntry(objectActor, val)
+ };
+ }
+ };
+}
+
+function enumWeakSetEntries(objectActor) {
+ // 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.
+ let raw = objectActor.obj.unsafeDereference();
+ let keys = Cu.waiveXrays(
+ ThreadSafeChromeUtils.nondeterministicGetWeakSetKeys(raw));
+
+ return {
+ [Symbol.iterator]: function* () {
+ for (let item of keys) {
+ yield gripFromEntry(objectActor, item);
+ }
+ },
+ size: keys.length,
+ propertyName(index) {
+ return index;
+ },
+ propertyDescription(index) {
+ let val = keys[index];
+ return {
+ enumerable: true,
+ value: gripFromEntry(objectActor, val)
+ };
+ }
+ };
+}
+
+/**
+ * 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().
+ *
+ * Functions must return false if they cannot provide preview
+ * information for the debugger object, or true otherwise.
+ */
+DebuggerServer.ObjectActorPreviewers = {
+ 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);
+ }],
+
+ 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) {
+ // Calling getOwnPropertyDescriptor with displayName might throw
+ // with "permission denied" errors for some functions.
+ dumpn(e);
+ }
+
+ if (userDisplayName && typeof userDisplayName.value == "string" &&
+ userDisplayName.value) {
+ grip.userDisplayName = hooks.createValueGrip(userDisplayName.value);
+ }
+
+ let dbgGlobal = hooks.getGlobalDebugObject();
+ if (dbgGlobal) {
+ let script = dbgGlobal.makeDebuggeeValue(obj.unsafeDereference()).script;
+ if (script) {
+ grip.location = {
+ url: script.url,
+ line: script.startLine
+ };
+ }
+ }
+
+ return true;
+ }],
+
+ RegExp: [function ({obj, hooks}, grip) {
+ // Avoid having any special preview for the RegExp.prototype itself.
+ if (!obj.proto || obj.proto.class != "RegExp") {
+ return false;
+ }
+
+ let str = RegExp.prototype.toString.call(obj.unsafeDereference());
+ grip.displayString = hooks.createValueGrip(str);
+ return true;
+ }],
+
+ Date: [function ({obj, hooks}, grip) {
+ let time = Date.prototype.getTime.call(obj.unsafeDereference());
+
+ grip.preview = {
+ timestamp: hooks.createValueGrip(time),
+ };
+ return true;
+ }],
+
+ Array: [function ({obj, hooks}, grip) {
+ let length = DevToolsUtils.getProperty(obj, "length");
+ if (typeof length != "number") {
+ return false;
+ }
+
+ grip.preview = {
+ kind: "ArrayLike",
+ length: length,
+ };
+
+ if (hooks.getGripDepth() > 1) {
+ return true;
+ }
+
+ let raw = obj.unsafeDereference();
+ let items = grip.preview.items = [];
+
+ for (let i = 0; i < length; ++i) {
+ // 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.
+ let desc = Object.getOwnPropertyDescriptor(Cu.waiveXrays(raw), i);
+ if (desc && !desc.get && !desc.set) {
+ let value = Cu.unwaiveXrays(desc.value);
+ value = makeDebuggeeValueIfNeeded(obj, value);
+ items.push(hooks.createValueGrip(value));
+ } else {
+ items.push(null);
+ }
+
+ if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ return true;
+ }],
+
+ Set: [function (objectActor, grip) {
+ let 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;
+ }
+
+ let items = grip.preview.items = [];
+ for (let item of enumSetEntries(objectActor)) {
+ items.push(item);
+ if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ return true;
+ }],
+
+ WeakSet: [function (objectActor, grip) {
+ let enumEntries = enumWeakSetEntries(objectActor);
+
+ grip.preview = {
+ kind: "ArrayLike",
+ length: enumEntries.size
+ };
+
+ // Avoid recursive object grips.
+ if (objectActor.hooks.getGripDepth() > 1) {
+ return true;
+ }
+
+ let items = grip.preview.items = [];
+ for (let item of enumEntries) {
+ items.push(item);
+ if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ return true;
+ }],
+
+ Map: [function (objectActor, grip) {
+ let 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;
+ }
+
+ let entries = grip.preview.entries = [];
+ for (let entry of enumMapEntries(objectActor)) {
+ entries.push(entry);
+ if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ return true;
+ }],
+
+ WeakMap: [function (objectActor, grip) {
+ let enumEntries = enumWeakMapEntries(objectActor);
+
+ grip.preview = {
+ kind: "MapLike",
+ size: enumEntries.size
+ };
+
+ if (objectActor.hooks.getGripDepth() > 1) {
+ return true;
+ }
+
+ let entries = grip.preview.entries = [];
+ for (let 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;
+ }
+
+ let keys = obj.getOwnPropertyNames();
+ grip.preview = {
+ kind: "MapLike",
+ size: keys.length,
+ };
+
+ if (hooks.getGripDepth() > 1) {
+ return true;
+ }
+
+ let entries = grip.preview.entries = [];
+ for (let key of keys) {
+ let value = makeDebuggeeValueIfNeeded(obj, rawObj[key]);
+ entries.push([key, hooks.createValueGrip(value)]);
+ if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ }
+
+ return true;
+ }],
+
+ Proxy: [function ({obj, hooks}, grip, rawObj) {
+ grip.preview = {
+ kind: "Object",
+ ownProperties: Object.create(null),
+ ownPropertiesLength: 2
+ };
+
+ if (hooks.getGripDepth() > 1) {
+ return true;
+ }
+
+ grip.preview.ownProperties['<target>'] = {value: grip.proxyTarget};
+ grip.preview.ownProperties['<handler>'] = {value: grip.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 {obj, hooks} = objectActor;
+
+ if (!obj.proto || obj.proto.class != className) {
+ return false;
+ }
+
+ 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;
+ }
+
+ let canHandle = GenericObject(objectActor, grip, rawObj, className === "String");
+ if (!canHandle) {
+ return false;
+ }
+
+ grip.preview.wrappedValue =
+ hooks.createValueGrip(makeDebuggeeValueIfNeeded(obj, v));
+ return true;
+}
+
+function GenericObject(objectActor, grip, rawObj, specialStringBehavior = false) {
+ let {obj, hooks} = objectActor;
+ if (grip.preview || grip.displayString || hooks.getGripDepth() > 1) {
+ return false;
+ }
+
+ let i = 0, names = [];
+ let preview = grip.preview = {
+ kind: "Object",
+ ownProperties: Object.create(null),
+ };
+
+ try {
+ names = obj.getOwnPropertyNames();
+ } catch (ex) {
+ // Calling getOwnPropertyNames() on some wrapped native prototypes is not
+ // allowed: "cannot modify properties of a WrappedNative". See bug 952093.
+ }
+
+ preview.ownPropertiesLength = names.length;
+
+ let length;
+ if (specialStringBehavior) {
+ length = DevToolsUtils.getProperty(obj, "length");
+ if (typeof length != "number") {
+ specialStringBehavior = false;
+ }
+ }
+
+ for (let name of names) {
+ if (specialStringBehavior && /^[0-9]+$/.test(name)) {
+ let num = parseInt(name, 10);
+ if (num.toString() === name && num >= 0 && num < length) {
+ continue;
+ }
+ }
+
+ let 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) {
+ preview.safeGetterValues = objectActor._findSafeGetterValues(
+ Object.keys(preview.ownProperties),
+ OBJECT_PREVIEW_MAX_ITEMS - i);
+ }
+
+ return true;
+}
+
+// Preview functions that do not rely on the object class.
+DebuggerServer.ObjectActorPreviewers.Object = [
+ function TypedArray({obj, hooks}, grip) {
+ if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) {
+ return false;
+ }
+
+ let length = DevToolsUtils.getProperty(obj, "length");
+ if (typeof length != "number") {
+ return false;
+ }
+
+ grip.preview = {
+ kind: "ArrayLike",
+ length: length,
+ };
+
+ if (hooks.getGripDepth() > 1) {
+ return true;
+ }
+
+ let raw = obj.unsafeDereference();
+ let global = Cu.getGlobalForObject(DebuggerServer);
+ let classProto = global[obj.class].prototype;
+ // The Xray machinery for TypedArrays denies indexed access on the grounds
+ // that it's slow, and advises callers to do a structured clone instead.
+ let safeView = Cu.cloneInto(classProto.subarray.call(raw, 0,
+ OBJECT_PREVIEW_MAX_ITEMS), global);
+ let items = grip.preview.items = [];
+ for (let i = 0; i < safeView.length; i++) {
+ items.push(safeView[i]);
+ }
+
+ return true;
+ },
+
+ function Error({obj, hooks}, grip) {
+ switch (obj.class) {
+ case "Error":
+ case "EvalError":
+ case "RangeError":
+ case "ReferenceError":
+ case "SyntaxError":
+ case "TypeError":
+ case "URIError":
+ let name = DevToolsUtils.getProperty(obj, "name");
+ let msg = DevToolsUtils.getProperty(obj, "message");
+ let stack = DevToolsUtils.getProperty(obj, "stack");
+ let fileName = DevToolsUtils.getProperty(obj, "fileName");
+ let lineNumber = DevToolsUtils.getProperty(obj, "lineNumber");
+ let 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),
+ };
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ function CSSMediaRule({obj, hooks}, grip, rawObj) {
+ if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSMediaRule)) {
+ return false;
+ }
+ grip.preview = {
+ kind: "ObjectWithText",
+ text: hooks.createValueGrip(rawObj.conditionText),
+ };
+ return true;
+ },
+
+ function CSSStyleRule({obj, hooks}, grip, rawObj) {
+ if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSStyleRule)) {
+ return false;
+ }
+ grip.preview = {
+ kind: "ObjectWithText",
+ text: hooks.createValueGrip(rawObj.selectorText),
+ };
+ return true;
+ },
+
+ function ObjectWithURL({obj, hooks}, grip, rawObj) {
+ if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSImportRule ||
+ rawObj instanceof Ci.nsIDOMCSSStyleSheet ||
+ rawObj instanceof Ci.nsIDOMLocation ||
+ rawObj instanceof Ci.nsIDOMWindow)) {
+ return false;
+ }
+
+ let url;
+ if (rawObj instanceof Ci.nsIDOMWindow && rawObj.location) {
+ url = rawObj.location.href;
+ } else if (rawObj.href) {
+ url = rawObj.href;
+ } else {
+ return false;
+ }
+
+ grip.preview = {
+ kind: "ObjectWithURL",
+ url: hooks.createValueGrip(url),
+ };
+
+ return true;
+ },
+
+ function ArrayLike({obj, hooks}, grip, rawObj) {
+ if (isWorker || !rawObj ||
+ obj.class != "DOMStringList" &&
+ obj.class != "DOMTokenList" &&
+ !(rawObj instanceof Ci.nsIDOMMozNamedAttrMap ||
+ rawObj instanceof Ci.nsIDOMCSSRuleList ||
+ rawObj instanceof Ci.nsIDOMCSSValueList ||
+ rawObj instanceof Ci.nsIDOMFileList ||
+ rawObj instanceof Ci.nsIDOMFontFaceList ||
+ rawObj instanceof Ci.nsIDOMMediaList ||
+ rawObj instanceof Ci.nsIDOMNodeList ||
+ rawObj instanceof Ci.nsIDOMStyleSheetList)) {
+ return false;
+ }
+
+ if (typeof rawObj.length != "number") {
+ return false;
+ }
+
+ grip.preview = {
+ kind: "ArrayLike",
+ length: rawObj.length,
+ };
+
+ if (hooks.getGripDepth() > 1) {
+ return true;
+ }
+
+ let items = grip.preview.items = [];
+
+ for (let i = 0; i < rawObj.length &&
+ items.length < OBJECT_PREVIEW_MAX_ITEMS; i++) {
+ let value = makeDebuggeeValueIfNeeded(obj, rawObj[i]);
+ items.push(hooks.createValueGrip(value));
+ }
+
+ return true;
+ },
+
+ function CSSStyleDeclaration({obj, hooks}, grip, rawObj) {
+ if (isWorker || !rawObj ||
+ !(rawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) {
+ return false;
+ }
+
+ grip.preview = {
+ kind: "MapLike",
+ size: rawObj.length,
+ };
+
+ let entries = grip.preview.entries = [];
+
+ for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS &&
+ i < rawObj.length; i++) {
+ let prop = rawObj[i];
+ let value = rawObj.getPropertyValue(prop);
+ entries.push([prop, hooks.createValueGrip(value)]);
+ }
+
+ return true;
+ },
+
+ function DOMNode({obj, hooks}, grip, rawObj) {
+ if (isWorker || obj.class == "Object" || !rawObj ||
+ !(rawObj instanceof Ci.nsIDOMNode)) {
+ return false;
+ }
+
+ let preview = grip.preview = {
+ kind: "DOMNode",
+ nodeType: rawObj.nodeType,
+ nodeName: rawObj.nodeName,
+ };
+
+ if (rawObj instanceof Ci.nsIDOMDocument && rawObj.location) {
+ preview.location = hooks.createValueGrip(rawObj.location.href);
+ } else if (rawObj instanceof Ci.nsIDOMDocumentFragment) {
+ preview.childNodesLength = rawObj.childNodes.length;
+
+ if (hooks.getGripDepth() < 2) {
+ preview.childNodes = [];
+ for (let node of rawObj.childNodes) {
+ let actor = hooks.createValueGrip(obj.makeDebuggeeValue(node));
+ preview.childNodes.push(actor);
+ if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ }
+ }
+ } else if (rawObj instanceof Ci.nsIDOMElement) {
+ // Add preview for DOM element attributes.
+ if (rawObj instanceof Ci.nsIDOMHTMLElement) {
+ preview.nodeName = preview.nodeName.toLowerCase();
+ }
+
+ let i = 0;
+ preview.attributes = {};
+ preview.attributesLength = rawObj.attributes.length;
+ for (let attr of rawObj.attributes) {
+ preview.attributes[attr.nodeName] = hooks.createValueGrip(attr.value);
+ }
+ } else if (rawObj instanceof Ci.nsIDOMAttr) {
+ preview.value = hooks.createValueGrip(rawObj.value);
+ } else if (rawObj instanceof Ci.nsIDOMText ||
+ rawObj instanceof Ci.nsIDOMComment) {
+ preview.textContent = hooks.createValueGrip(rawObj.textContent);
+ }
+
+ return true;
+ },
+
+ function DOMEvent({obj, hooks}, grip, rawObj) {
+ if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMEvent)) {
+ return false;
+ }
+
+ let preview = grip.preview = {
+ kind: "DOMEvent",
+ type: rawObj.type,
+ properties: Object.create(null),
+ };
+
+ if (hooks.getGripDepth() < 2) {
+ let target = obj.makeDebuggeeValue(rawObj.target);
+ preview.target = hooks.createValueGrip(target);
+ }
+
+ let props = [];
+ if (rawObj instanceof Ci.nsIDOMMouseEvent) {
+ props.push("buttons", "clientX", "clientY", "layerX", "layerY");
+ } else if (rawObj instanceof Ci.nsIDOMKeyEvent) {
+ let modifiers = [];
+ if (rawObj.altKey) {
+ modifiers.push("Alt");
+ }
+ if (rawObj.ctrlKey) {
+ modifiers.push("Control");
+ }
+ if (rawObj.metaKey) {
+ modifiers.push("Meta");
+ }
+ if (rawObj.shiftKey) {
+ modifiers.push("Shift");
+ }
+ preview.eventKind = "key";
+ preview.modifiers = modifiers;
+
+ props.push("key", "charCode", "keyCode");
+ } else if (rawObj instanceof Ci.nsIDOMTransitionEvent) {
+ props.push("propertyName", "pseudoElement");
+ } else if (rawObj instanceof Ci.nsIDOMAnimationEvent) {
+ props.push("animationName", "pseudoElement");
+ } else if (rawObj instanceof Ci.nsIDOMClipboardEvent) {
+ props.push("clipboardData");
+ }
+
+ // Add event-specific properties.
+ for (let prop of props) {
+ let value = rawObj[prop];
+ if (value && (typeof value == "object" || typeof value == "function")) {
+ // 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 (let 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({obj, hooks}, grip, rawObj) {
+ if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMDOMException)) {
+ return false;
+ }
+
+ 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),
+ };
+
+ return true;
+ },
+
+ function PseudoArray({obj, hooks}, grip, rawObj) {
+ let length;
+
+ let keys = obj.getOwnPropertyNames();
+ if (keys.length == 0) {
+ return false;
+ }
+
+ // If no item is going to be displayed in preview, better display as sparse object.
+ // The first key should contain the smallest integer index (if any).
+ if(keys[0] >= OBJECT_PREVIEW_MAX_ITEMS) {
+ return false;
+ }
+
+ // Pseudo-arrays should only have array indices and, optionally, a "length" property.
+ // Since integer indices are sorted first, check if the last property is "length".
+ if(keys[keys.length-1] === "length") {
+ keys.pop();
+ length = DevToolsUtils.getProperty(obj, "length");
+ } else {
+ // Otherwise, let length be the (presumably) greatest array index plus 1.
+ length = +keys[keys.length-1] + 1;
+ }
+ // Check if length is a valid array length, i.e. is a Uint32 number.
+ if(typeof length !== "number" || length >>> 0 !== length) {
+ return false;
+ }
+
+ // Ensure all keys are increasing array indices smaller than length. The order is not
+ // guaranteed for exotic objects but, in most cases, big array indices and properties
+ // which are not integer indices should be at the end. Then, iterating backwards
+ // allows us to return earlier when the object is not completely a pseudo-array.
+ let prev = length;
+ for(let i = keys.length - 1; i >= 0; --i) {
+ let key = keys[i];
+ let numKey = key >>> 0; // ToUint32(key)
+ if (numKey + '' !== key || numKey >= prev) {
+ return false;
+ }
+ prev = numKey;
+ }
+
+ grip.preview = {
+ kind: "ArrayLike",
+ length: length,
+ };
+
+ // Avoid recursive object grips.
+ if (hooks.getGripDepth() > 1) {
+ return true;
+ }
+
+ let items = grip.preview.items = [];
+ let numItems = Math.min(OBJECT_PREVIEW_MAX_ITEMS, length);
+
+ for (let i = 0; i < numItems; ++i) {
+ let desc = obj.getOwnPropertyDescriptor(i);
+ if (desc && 'value' in desc) {
+ items.push(hooks.createValueGrip(desc.value));
+ } else {
+ items.push(null);
+ }
+ }
+
+ return true;
+ },
+
+ function Object(objectActor, grip, rawObj) {
+ return GenericObject(objectActor, grip, rawObj, /* specialStringBehavior = */ false);
+ },
+];
+
+/**
+ * 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.");
+ }
+
+ let state = { state: obj.promiseState };
+ if (state.state === "fulfilled") {
+ state.value = obj.promiseValue;
+ } else if (state.state === "rejected") {
+ state.reason = obj.promiseReason;
+ }
+ return state;
+}
+
+/**
+ * Determine if a given value is non-primitive.
+ *
+ * @param Any value
+ * The value to test.
+ * @return Boolean
+ * Whether the value is non-primitive.
+ */
+function isObject(value) {
+ const type = typeof value;
+ return type == "object" ? value !== null : type == "function";
+}
+
+/**
+ * Create a function that can safely stringify Debugger.Objects of a given
+ * builtin type.
+ *
+ * @param Function ctor
+ * The builtin class constructor.
+ * @return Function
+ * The stringifier for the class.
+ */
+function createBuiltinStringifier(ctor) {
+ return obj => ctor.prototype.toString.call(obj.unsafeDereference());
+}
+
+/**
+ * Stringify a Debugger.Object-wrapped Error instance.
+ *
+ * @param Debugger.Object obj
+ * The object to stringify.
+ * @return String
+ * The stringification of the object.
+ */
+function errorStringify(obj) {
+ let name = DevToolsUtils.getProperty(obj, "name");
+ if (name === "" || name === undefined) {
+ name = obj.class;
+ } else if (isObject(name)) {
+ name = stringify(name);
+ }
+
+ let message = DevToolsUtils.getProperty(obj, "message");
+ if (isObject(message)) {
+ message = stringify(message);
+ }
+
+ if (message === "" || message === undefined) {
+ return name;
+ }
+ return name + ": " + message;
+}
+
+/**
+ * Stringify a Debugger.Object based on its class.
+ *
+ * @param Debugger.Object obj
+ * The object to stringify.
+ * @return String
+ * The stringification for the object.
+ */
+function stringify(obj) {
+ if (obj.class == "DeadObject") {
+ const error = new Error("Dead object encountered.");
+ DevToolsUtils.reportException("stringify", error);
+ return "<dead object>";
+ }
+
+ const stringifier = stringifiers[obj.class] || stringifiers.Object;
+
+ try {
+ return stringifier(obj);
+ } catch (e) {
+ DevToolsUtils.reportException("stringify", e);
+ return "<failed to stringify object>";
+ }
+}
+
+// Used to prevent infinite recursion when an array is found inside itself.
+var seen = null;
+
+var stringifiers = {
+ Error: errorStringify,
+ EvalError: errorStringify,
+ RangeError: errorStringify,
+ ReferenceError: errorStringify,
+ SyntaxError: errorStringify,
+ TypeError: errorStringify,
+ URIError: errorStringify,
+ Boolean: createBuiltinStringifier(Boolean),
+ Function: createBuiltinStringifier(Function),
+ Number: createBuiltinStringifier(Number),
+ RegExp: createBuiltinStringifier(RegExp),
+ String: createBuiltinStringifier(String),
+ Object: obj => "[object " + obj.class + "]",
+ Array: obj => {
+ // If we're at the top level then we need to create the Set for tracking
+ // previously stringified arrays.
+ const topLevel = !seen;
+ if (topLevel) {
+ seen = new Set();
+ } else if (seen.has(obj)) {
+ return "";
+ }
+
+ seen.add(obj);
+
+ const len = DevToolsUtils.getProperty(obj, "length");
+ let string = "";
+
+ // The following check is only required because the debuggee could possibly
+ // be a Proxy and return any value. For normal objects, array.length is
+ // always a non-negative integer.
+ if (typeof len == "number" && len > 0) {
+ for (let i = 0; i < len; i++) {
+ const desc = obj.getOwnPropertyDescriptor(i);
+ if (desc) {
+ const { value } = desc;
+ if (value != null) {
+ string += isObject(value) ? stringify(value) : value;
+ }
+ }
+
+ if (i < len - 1) {
+ string += ",";
+ }
+ }
+ }
+
+ if (topLevel) {
+ seen = null;
+ }
+
+ return string;
+ },
+ DOMException: obj => {
+ const message = DevToolsUtils.getProperty(obj, "message") || "<no message>";
+ const result = (+DevToolsUtils.getProperty(obj, "result")).toString(16);
+ const code = DevToolsUtils.getProperty(obj, "code");
+ const name = DevToolsUtils.getProperty(obj, "name") || "<unknown>";
+
+ return '[Exception... "' + message + '" ' +
+ 'code: "' + code + '" ' +
+ 'nsresult: "0x' + result + " (" + name + ')"]';
+ },
+ Promise: obj => {
+ const { state, value, reason } = getPromiseState(obj);
+ let statePreview = state;
+ if (state != "pending") {
+ const settledValue = state === "fulfilled" ? value : reason;
+ statePreview += ": " + (typeof settledValue === "object" && settledValue !== null
+ ? stringify(settledValue)
+ : settledValue);
+ }
+ return "Promise (" + statePreview + ")";
+ },
+};
+
+/**
+ * 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 (value && (typeof value == "object" || typeof value == "function")) {
+ return obj.makeDebuggeeValue(value);
+ }
+ return value;
+}
+
+/**
+ * Creates an actor for the specied "very long" string. "Very long" is specified
+ * at the server's discretion.
+ *
+ * @param string String
+ * The string.
+ */
+function LongStringActor(string) {
+ this.string = string;
+ this.stringLength = string.length;
+}
+
+LongStringActor.prototype = {
+ actorPrefix: "longString",
+
+ disconnect: function () {
+ // Because longStringActors is not a weak map, we won't automatically leave
+ // it so we need to manually leave on disconnect so that we don't leak
+ // memory.
+ this._releaseActor();
+ },
+
+ /**
+ * Returns a grip for this actor for returning in a protocol message.
+ */
+ grip: function () {
+ return {
+ "type": "longString",
+ "initial": this.string.substring(
+ 0, DebuggerServer.LONG_STRING_INITIAL_LENGTH),
+ "length": this.stringLength,
+ "actor": this.actorID
+ };
+ },
+
+ /**
+ * Handle a request to extract part of this actor's string.
+ *
+ * @param request object
+ * The protocol request object.
+ */
+ onSubstring: function (request) {
+ return {
+ "from": this.actorID,
+ "substring": this.string.substring(request.start, request.end)
+ };
+ },
+
+ /**
+ * Handle a request to release this LongStringActor instance.
+ */
+ onRelease: function () {
+ // TODO: also check if registeredPool === threadActor.threadLifetimePool
+ // when the web console moves aray from manually releasing pause-scoped
+ // actors.
+ this._releaseActor();
+ this.registeredPool.removeActor(this);
+ return {};
+ },
+
+ _releaseActor: function () {
+ if (this.registeredPool && this.registeredPool.longStringActors) {
+ delete this.registeredPool.longStringActors[this.string];
+ }
+ }
+};
+
+LongStringActor.prototype.requestTypes = {
+ "substring": LongStringActor.prototype.onSubstring,
+ "release": LongStringActor.prototype.onRelease
+};
+
+/**
+ * Create a grip for the given debuggee value. If the value is an
+ * object, will create an actor with the given lifetime.
+ */
+function createValueGrip(value, pool, makeObjectGrip) {
+ switch (typeof value) {
+ case "boolean":
+ return value;
+
+ case "string":
+ if (stringIsLong(value)) {
+ return longStringGrip(value, pool);
+ }
+ return 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 "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":
+ let form = {
+ type: "symbol"
+ };
+ let name = getSymbolName(value);
+ if (name !== undefined) {
+ form.name = createValueGrip(name, pool, makeObjectGrip);
+ }
+ return form;
+
+ default:
+ assert(false, "Failed to provide a grip for: " + value);
+ return null;
+ }
+}
+
+const symbolProtoToString = Symbol.prototype.toString;
+
+function getSymbolName(symbol) {
+ const name = symbolProtoToString.call(symbol).slice("Symbol(".length, -1);
+ return name || undefined;
+}
+
+/**
+ * Returns true if the string is long enough to use a LongStringActor instead
+ * 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 >= DebuggerServer.LONG_STRING_LENGTH;
+}
+
+/**
+ * Create a grip for the given string.
+ *
+ * @param str String
+ * The string we are creating a grip for.
+ * @param pool ActorPool
+ * The actor pool where the new actor will be added.
+ */
+function longStringGrip(str, pool) {
+ if (!pool.longStringActors) {
+ pool.longStringActors = {};
+ }
+
+ if (pool.longStringActors.hasOwnProperty(str)) {
+ return pool.longStringActors[str].grip();
+ }
+
+ let actor = new LongStringActor(str);
+ pool.addActor(actor);
+ pool.longStringActors[str] = actor;
+ return actor.grip();
+}
+
+exports.ObjectActor = ObjectActor;
+exports.PropertyIteratorActor = PropertyIteratorActor;
+exports.LongStringActor = LongStringActor;
+exports.createValueGrip = createValueGrip;
+exports.stringIsLong = stringIsLong;
+exports.longStringGrip = longStringGrip;