Bug 843004 - Part 2: ObjectActor grip changes to allow pretty output; r=benvie,past
authorMihai Sucan <mihai.sucan@gmail.com>
Thu, 19 Dec 2013 15:04:59 +0200
changeset 161568 7b1a9fc65b07104d4a6c7b502ee1a588b473a2ea
parent 161567 c97caf1598c2f7955a8f006ba3b2442b5cf34181
child 161569 3ad3e3d575345946d8ee6d8320c1f09724ffd88e
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersbenvie, past
bugs843004
milestone29.0a1
Bug 843004 - Part 2: ObjectActor grip changes to allow pretty output; r=benvie,past
toolkit/devtools/DevToolsUtils.js
toolkit/devtools/DevToolsUtils.jsm
toolkit/devtools/server/actors/script.js
toolkit/devtools/server/actors/webconsole.js
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -1,16 +1,17 @@
 /* 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";
 
 /* General utilities used throughout devtools. */
 
+let Cu = Components.utils;
 let { Promise: promise } = Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
 let { Services } = Components.utils.import("resource://gre/modules/Services.jsm", {});
 
 /**
  * Turn the error |aError| into a string, without fail.
  */
 this.safeErrorString = function safeErrorString(aError) {
   try {
@@ -232,8 +233,29 @@ this.getProperty = function getProperty(
  * @return Boolean
  *         Whether a safe getter was found.
  */
 this.hasSafeGetter = function hasSafeGetter(aDesc) {
   let fn = aDesc.get;
   return fn && fn.callable && fn.class == "Function" && fn.script === undefined;
 };
 
+/**
+ * Check if it is safe to read properties and execute methods from the given JS
+ * object. Safety is defined as being protected from unintended code execution
+ * from content scripts (or cross-compartment code).
+ *
+ * See bugs 945920 and 946752 for discussion.
+ *
+ * @type Object aObj
+ *       The object to check.
+ * @return Boolean
+ *         True if it is safe to read properties from aObj, or false otherwise.
+ */
+this.isSafeJSObject = function isSafeJSObject(aObj) {
+  if (Cu.getGlobalForObject(aObj) ==
+      Cu.getGlobalForObject(isSafeJSObject)) {
+    return true; // aObj is not a cross-compartment wrapper.
+  }
+
+  return Cu.isXrayWrapper(aObj);
+};
+
--- a/toolkit/devtools/DevToolsUtils.jsm
+++ b/toolkit/devtools/DevToolsUtils.jsm
@@ -23,9 +23,10 @@ this.DevToolsUtils = {
   reportException: reportException,
   makeInfallible: makeInfallible,
   zip: zip,
   yieldingEach: yieldingEach,
   reportingDisabled: false , // Used by tests.
   defineLazyPrototypeGetter: defineLazyPrototypeGetter,
   getProperty: getProperty,
   hasSafeGetter: hasSafeGetter,
+  isSafeJSObject: isSafeJSObject,
 };
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -1,16 +1,24 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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";
 
+let 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.
+let OBJECT_PREVIEW_MAX_ITEMS = 10;
+
 /**
  * BreakpointStore objects keep track of all breakpoints that get set so that we
  * can reset them when the same script is introduced to the thread again (such
  * as after a refresh).
  */
 function BreakpointStore() {
   // If we have a whole-line breakpoint set at LINE in URL, then
   //
@@ -435,25 +443,30 @@ function ThreadActor(aHooks, aGlobal)
   this.findGlobals = this.globalManager.findGlobals.bind(this);
   this.onNewGlobal = this.globalManager.onNewGlobal.bind(this);
   this.onNewSource = this.onNewSource.bind(this);
   this._allEventsListener = this._allEventsListener.bind(this);
 
   this._options = {
     useSourceMaps: false
   };
+
+  this._gripDepth = 0;
 }
 
 /**
  * The breakpoint store must be shared across instances of ThreadActor so that
  * page reloads don't blow away all of our breakpoints.
  */
 ThreadActor.breakpointStore = new BreakpointStore();
 
 ThreadActor.prototype = {
+  // Used by the ObjectActor to keep track of the depth of grip() calls.
+  _gripDepth: null,
+
   actorPrefix: "context",
 
   get state() { return this._state; },
   get attached() this.state == "attached" ||
                  this.state == "running" ||
                  this.state == "paused",
 
   get breakpointStore() { return ThreadActor.breakpointStore; },
@@ -2725,17 +2738,17 @@ function errorStringify(aObj) {
  * Stringify a Debugger.Object based on its class.
  *
  * @param Debugger.Object aObj
  *        The object to stringify.
  * @return String
  *         The stringification for the object.
  */
 function stringify(aObj) {
-  if (Cu.isDeadWrapper(aObj)) {
+  if (aObj.class == "DeadObject") {
     const error = new Error("Dead object encountered.");
     DevToolsUtils.reportException("stringify", error);
     return "<dead object>";
   }
   const stringifier = stringifiers[aObj.class] || stringifiers.Object;
   return stringifier(aObj);
 }
 
@@ -2824,48 +2837,43 @@ function ObjectActor(aObj, aThreadActor)
 
 ObjectActor.prototype = {
   actorPrefix: "obj",
 
   /**
    * Returns a grip for this actor for returning in a protocol message.
    */
   grip: function () {
+    this.threadActor._gripDepth++;
+
     let g = {
       "type": "object",
       "class": this.obj.class,
       "actor": this.actorID,
       "extensible": this.obj.isExtensible(),
       "frozen": this.obj.isFrozen(),
       "sealed": this.obj.isSealed()
     };
 
-    // Add additional properties for functions.
-    if (this.obj.class === "Function") {
-      if (this.obj.name) {
-        g.name = this.obj.name;
-      }
-      if (this.obj.displayName) {
-        g.displayName = this.obj.displayName;
+    if (this.obj.class != "DeadObject") {
+      let raw = Cu.unwaiveXrays(this.obj.unsafeDereference());
+      if (!DevToolsUtils.isSafeJSObject(raw)) {
+        raw = null;
       }
 
-      // Check if the developer has added a de-facto standard displayName
-      // property for us to use.
-      try {
-        let desc = this.obj.getOwnPropertyDescriptor("displayName");
-        if (desc && desc.value && typeof desc.value == "string") {
-          g.userDisplayName = this.threadActor.createValueGrip(desc.value);
+      let previewers = DebuggerServer.ObjectActorPreviewers[this.obj.class] ||
+                       DebuggerServer.ObjectActorPreviewers.Object;
+      for (let fn of previewers) {
+        if (fn(this, g, raw)) {
+          break;
         }
-      } catch (e) {
-        // Calling getOwnPropertyDescriptor with displayName might throw
-        // with "permission denied" errors for some functions.
-        dumpn(e);
       }
     }
 
+    this.threadActor._gripDepth--;
     return g;
   },
 
   /**
    * Releases this actor from the pool.
    */
   release: function () {
     if (this.registeredPool.objectActors) {
@@ -2959,25 +2967,27 @@ ObjectActor.prototype = {
 
   /**
    * Find the safe getter values for the current Debugger.Object, |this.obj|.
    *
    * @private
    * @param object aOwnProperties
    *        The object that holds the list of known ownProperties for
    *        |this.obj|.
+   * @param number [aLimit=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 (aOwnProperties)
+  _findSafeGetterValues: function (aOwnProperties, aLimit = 0)
   {
     let safeGetterValues = Object.create(null);
     let obj = this.obj;
-    let level = 0;
+    let level = 0, i = 0;
 
     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 ||
@@ -3009,19 +3019,25 @@ ObjectActor.prototype = {
           // return undefined and should be ignored.
           if (getterValue !== undefined) {
             safeGetterValues[name] = {
               getterValue: this.threadActor.createValueGrip(getterValue),
               getterPrototypeLevel: level,
               enumerable: desc.enumerable,
               writable: level == 0 ? desc.writable : true,
             };
+            if (aLimit && ++i == aLimit) {
+              break;
+            }
           }
         }
       }
+      if (aLimit && i == aLimit) {
+        break;
+      }
 
       obj = obj.proto;
       level++;
     }
 
     return safeGetterValues;
   },
 
@@ -3038,17 +3054,25 @@ ObjectActor.prototype = {
    */
   _findSafeGetters: function (aObject)
   {
     if (aObject._safeGetters) {
       return aObject._safeGetters;
     }
 
     let getters = new Set();
-    for (let name of aObject.getOwnPropertyNames()) {
+    let names = [];
+    try {
+      names = aObject.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 = aObject.getOwnPropertyDescriptor(name);
       } catch (e) {
         // Calling getOwnPropertyDescriptor on wrapped native prototypes is not
         // allowed (bug 560072).
       }
       if (!desc || desc.value !== undefined || !("get" in desc)) {
@@ -3103,36 +3127,43 @@ ObjectActor.prototype = {
     return { from: this.actorID,
              displayString: this.threadActor.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 aName
    *        The property that the descriptor is generated for.
+   * @param boolean [aOnlyEnumerable]
+   *        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 aOnlyEnumerable=true.
    */
-  _propertyDescriptor: function (aName) {
+  _propertyDescriptor: function (aName, aOnlyEnumerable) {
     let desc;
     try {
       desc = this.obj.getOwnPropertyDescriptor(aName);
     } 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) {
+    if (!desc || aOnlyEnumerable && !desc.enumerable) {
       return undefined;
     }
 
     let retval = {
       configurable: desc.configurable,
       enumerable: desc.enumerable
     };
 
@@ -3228,16 +3259,575 @@ ObjectActor.prototype.requestTypes = {
   "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames,
   "decompile": ObjectActor.prototype.onDecompile,
   "release": ObjectActor.prototype.onRelease,
   "scope": ObjectActor.prototype.onScope,
 };
 
 
 /**
+ * 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 two
+ * arguments:
+ *   - the ObjectActor instance 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 = {
+  Function: [function({obj, threadActor}, aGrip) {
+    if (obj.name) {
+      aGrip.name = obj.name;
+    }
+
+    if (obj.displayName) {
+      aGrip.displayName = obj.displayName.substr(0, 500);
+    }
+
+    if (obj.parameterNames) {
+      aGrip.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) {
+      aGrip.userDisplayName = threadActor.createValueGrip(userDisplayName.value);
+    }
+
+    return true;
+  }],
+
+  RegExp: [function({obj, threadActor}, aGrip) {
+    // 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());
+    aGrip.displayString = threadActor.createValueGrip(str);
+    return true;
+  }],
+
+  Date: [function({obj, threadActor}, aGrip) {
+    if (!obj.proto || obj.proto.class != "Date") {
+      return false;
+    }
+
+    let time = Date.prototype.getTime.call(obj.unsafeDereference());
+
+    aGrip.preview = {
+      timestamp: threadActor.createValueGrip(time),
+    };
+    return true;
+  }],
+
+  Array: [function({obj, threadActor}, aGrip) {
+    let length = DevToolsUtils.getProperty(obj, "length");
+    if (typeof length != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "ArrayLike",
+      length: length,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let raw = obj.unsafeDereference();
+    let items = aGrip.preview.items = [];
+
+    for (let [i, value] of Array.prototype.entries.call(raw)) {
+      if (Object.hasOwnProperty.call(raw, i)) {
+        value = makeDebuggeeValueIfNeeded(obj, value);
+        items.push(threadActor.createValueGrip(value));
+      } else {
+        items.push(null);
+      }
+
+      if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
+        break;
+      }
+    }
+
+    return true;
+  }], // Array
+
+  Set: [function({obj, threadActor}, aGrip) {
+    let size = DevToolsUtils.getProperty(obj, "size");
+    if (typeof size != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "ArrayLike",
+      length: size,
+    };
+
+    // Avoid recursive object grips.
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let raw = obj.unsafeDereference();
+    let items = aGrip.preview.items = [];
+    for (let item of Set.prototype.values.call(raw)) {
+      item = makeDebuggeeValueIfNeeded(obj, item);
+      items.push(threadActor.createValueGrip(item));
+      if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
+        break;
+      }
+    }
+
+    return true;
+  }], // Set
+
+  Map: [function({obj, threadActor}, aGrip) {
+    let size = DevToolsUtils.getProperty(obj, "size");
+    if (typeof size != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "MapLike",
+      size: size,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let raw = obj.unsafeDereference();
+    let entries = aGrip.preview.entries = [];
+    for (let [key, value] of Map.prototype.entries.call(raw)) {
+      key = makeDebuggeeValueIfNeeded(obj, key);
+      value = makeDebuggeeValueIfNeeded(obj, value);
+      entries.push([threadActor.createValueGrip(key),
+                    threadActor.createValueGrip(value)]);
+      if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
+        break;
+      }
+    }
+
+    return true;
+  }], // Map
+
+  DOMStringMap: [function({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj) {
+      return false;
+    }
+
+    let keys = obj.getOwnPropertyNames();
+    aGrip.preview = {
+      kind: "MapLike",
+      size: keys.length,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let entries = aGrip.preview.entries = [];
+    for (let key of keys) {
+      let value = makeDebuggeeValueIfNeeded(obj, aRawObj[key]);
+      entries.push([key, threadActor.createValueGrip(value)]);
+      if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
+        break;
+      }
+    }
+
+    return true;
+  }], // DOMStringMap
+}; // DebuggerServer.ObjectActorPreviewers
+
+// Preview functions that do not rely on the object class.
+DebuggerServer.ObjectActorPreviewers.Object = [
+  function TypedArray({obj, threadActor}, aGrip) {
+    if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) {
+      return false;
+    }
+
+    let length = DevToolsUtils.getProperty(obj, "length");
+    if (typeof length != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "ArrayLike",
+      length: length,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let raw = obj.unsafeDereference();
+    let global = Cu.getGlobalForObject(DebuggerServer);
+    let classProto = global[obj.class].prototype;
+    let safeView = classProto.subarray.call(raw, 0, OBJECT_PREVIEW_MAX_ITEMS);
+    let items = aGrip.preview.items = [];
+    for (let i = 0; i < safeView.length; i++) {
+      items.push(safeView[i]);
+    }
+
+    return true;
+  },
+
+  function Error({obj, threadActor}, aGrip) {
+    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");
+        aGrip.preview = {
+          kind: "Error",
+          name: threadActor.createValueGrip(name),
+          message: threadActor.createValueGrip(msg),
+          stack: threadActor.createValueGrip(stack),
+          fileName: threadActor.createValueGrip(fileName),
+          lineNumber: threadActor.createValueGrip(lineNumber),
+          columnNumber: threadActor.createValueGrip(columnNumber),
+        };
+        return true;
+      default:
+        return false;
+    }
+  },
+
+  function CSSMediaRule({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSMediaRule)) {
+      return false;
+    }
+    aGrip.preview = {
+      kind: "ObjectWithText",
+      text: threadActor.createValueGrip(aRawObj.conditionText),
+    };
+    return true;
+  },
+
+  function CSSStyleRule({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleRule)) {
+      return false;
+    }
+    aGrip.preview = {
+      kind: "ObjectWithText",
+      text: threadActor.createValueGrip(aRawObj.selectorText),
+    };
+    return true;
+  },
+
+  function ObjectWithURL({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj ||
+        !(aRawObj instanceof Ci.nsIDOMCSSImportRule ||
+          aRawObj instanceof Ci.nsIDOMCSSStyleSheet ||
+          aRawObj instanceof Ci.nsIDOMLocation ||
+          aRawObj instanceof Ci.nsIDOMWindow)) {
+      return false;
+    }
+
+    let url;
+    if (aRawObj instanceof Ci.nsIDOMWindow) {
+      url = aRawObj.location.href;
+    } else {
+      url = aRawObj.href;
+    }
+
+    aGrip.preview = {
+      kind: "ObjectWithURL",
+      url: threadActor.createValueGrip(url),
+    };
+
+    return true;
+  },
+
+  function ArrayLike({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj ||
+        obj.class != "DOMTokenList" &&
+        !(aRawObj instanceof Ci.nsIDOMMozNamedAttrMap ||
+          aRawObj instanceof Ci.nsIDOMCSSRuleList ||
+          aRawObj instanceof Ci.nsIDOMCSSValueList ||
+          aRawObj instanceof Ci.nsIDOMDOMStringList ||
+          aRawObj instanceof Ci.nsIDOMFileList ||
+          aRawObj instanceof Ci.nsIDOMFontFaceList ||
+          aRawObj instanceof Ci.nsIDOMMediaList ||
+          aRawObj instanceof Ci.nsIDOMNodeList ||
+          aRawObj instanceof Ci.nsIDOMStyleSheetList)) {
+      return false;
+    }
+
+    if (typeof aRawObj.length != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "ArrayLike",
+      length: aRawObj.length,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let items = aGrip.preview.items = [];
+
+    for (let i = 0; i < aRawObj.length &&
+                    items.length < OBJECT_PREVIEW_MAX_ITEMS; i++) {
+      let value = makeDebuggeeValueIfNeeded(obj, aRawObj[i]);
+      items.push(threadActor.createValueGrip(value));
+    }
+
+    return true;
+  }, // ArrayLike
+
+  function CSSStyleDeclaration({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "MapLike",
+      size: aRawObj.length,
+    };
+
+    let entries = aGrip.preview.entries = [];
+
+    for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS &&
+                    i < aRawObj.length; i++) {
+      let prop = aRawObj[i];
+      let value = aRawObj.getPropertyValue(prop);
+      entries.push([prop, threadActor.createValueGrip(value)]);
+    }
+
+    return true;
+  },
+
+  function DOMNode({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMNode)) {
+      return false;
+    }
+
+    let preview = aGrip.preview = {
+      kind: "DOMNode",
+      nodeType: aRawObj.nodeType,
+      nodeName: aRawObj.nodeName,
+    };
+
+    if (aRawObj instanceof Ci.nsIDOMDocument) {
+      preview.location = threadActor.createValueGrip(aRawObj.location.href);
+    } else if (aRawObj instanceof Ci.nsIDOMDocumentFragment) {
+      preview.childNodesLength = aRawObj.childNodes.length;
+
+      if (threadActor._gripDepth < 2) {
+        preview.childNodes = [];
+        for (let node of aRawObj.childNodes) {
+          let actor = threadActor.createValueGrip(obj.makeDebuggeeValue(node));
+          preview.childNodes.push(actor);
+          if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) {
+            break;
+          }
+        }
+      }
+    } else if (aRawObj instanceof Ci.nsIDOMElement) {
+      // Add preview for DOM element attributes.
+      if (aRawObj instanceof Ci.nsIDOMHTMLElement) {
+        preview.nodeName = preview.nodeName.toLowerCase();
+      }
+
+      let i = 0;
+      preview.attributes = {};
+      preview.attributesLength = aRawObj.attributes.length;
+      for (let attr of aRawObj.attributes) {
+        preview.attributes[attr.nodeName] = threadActor.createValueGrip(attr.value);
+        if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
+          break;
+        }
+      }
+    } else if (aRawObj instanceof Ci.nsIDOMAttr) {
+      preview.value = threadActor.createValueGrip(aRawObj.value);
+    } else if (aRawObj instanceof Ci.nsIDOMText ||
+               aRawObj instanceof Ci.nsIDOMComment) {
+      preview.textContent = threadActor.createValueGrip(aRawObj.textContent);
+    }
+
+    return true;
+  }, // DOMNode
+
+  function DOMEvent({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMEvent)) {
+      return false;
+    }
+
+    let preview = aGrip.preview = {
+      kind: "DOMEvent",
+      type: aRawObj.type,
+      properties: Object.create(null),
+    };
+
+    if (threadActor._gripDepth < 2) {
+      let target = obj.makeDebuggeeValue(aRawObj.target);
+      preview.target = threadActor.createValueGrip(target);
+    }
+
+    let props = [];
+    if (aRawObj instanceof Ci.nsIDOMMouseEvent) {
+      props.push("buttons", "clientX", "clientY", "layerX", "layerY");
+    } else if (aRawObj instanceof Ci.nsIDOMKeyEvent) {
+      let modifiers = [];
+      if (aRawObj.altKey) {
+        modifiers.push("Alt");
+      }
+      if (aRawObj.ctrlKey) {
+        modifiers.push("Control");
+      }
+      if (aRawObj.metaKey) {
+        modifiers.push("Meta");
+      }
+      if (aRawObj.shiftKey) {
+        modifiers.push("Shift");
+      }
+      preview.eventKind = "key";
+      preview.modifiers = modifiers;
+
+      props.push("key", "charCode", "keyCode");
+    } else if (aRawObj instanceof Ci.nsIDOMTransitionEvent ||
+               aRawObj instanceof Ci.nsIDOMAnimationEvent) {
+      props.push("animationName", "pseudoElement");
+    } else if (aRawObj instanceof Ci.nsIDOMClipboardEvent) {
+      props.push("clipboardData");
+    }
+
+    // Add event-specific properties.
+    for (let prop of props) {
+      let value = aRawObj[prop];
+      if (value && (typeof value == "object" || typeof value == "function")) {
+        // Skip properties pointing to objects.
+        if (threadActor._gripDepth > 1) {
+          continue;
+        }
+        value = obj.makeDebuggeeValue(value);
+      }
+      preview.properties[prop] = threadActor.createValueGrip(value);
+    }
+
+    // Add any properties we find on the event object.
+    if (!props.length) {
+      let i = 0;
+      for (let prop in aRawObj) {
+        let value = aRawObj[prop];
+        if (prop == "target" || prop == "type" || value === null ||
+            typeof value == "function") {
+          continue;
+        }
+        if (value && typeof value == "object") {
+          if (threadActor._gripDepth > 1) {
+            continue;
+          }
+          value = obj.makeDebuggeeValue(value);
+        }
+        preview.properties[prop] = threadActor.createValueGrip(value);
+        if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
+          break;
+        }
+      }
+    }
+
+    return true;
+  }, // DOMEvent
+
+  function DOMException({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMDOMException)) {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "DOMException",
+      name: threadActor.createValueGrip(aRawObj.name),
+      message: threadActor.createValueGrip(aRawObj.message),
+      code: threadActor.createValueGrip(aRawObj.code),
+      result: threadActor.createValueGrip(aRawObj.result),
+      filename: threadActor.createValueGrip(aRawObj.filename),
+      lineNumber: threadActor.createValueGrip(aRawObj.lineNumber),
+      columnNumber: threadActor.createValueGrip(aRawObj.columnNumber),
+    };
+
+    return true;
+  },
+
+  function GenericObject(aObjectActor, aGrip) {
+    let {obj, threadActor} = aObjectActor;
+    if (aGrip.preview || aGrip.displayString || threadActor._gripDepth > 1) {
+      return false;
+    }
+
+    let i = 0, names = [];
+    let preview = aGrip.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;
+
+    for (let name of names) {
+      let desc = aObjectActor._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 = aObjectActor.
+                                 _findSafeGetterValues(preview.ownProperties,
+                                                       OBJECT_PREVIEW_MAX_ITEMS - i);
+    }
+
+    return true;
+  }, // GenericObject
+]; // DebuggerServer.ObjectActorPreviewers.Object
+
+/**
  * Creates a pause-scoped actor for the specified object.
  * @see ObjectActor
  */
 function PauseScopedObjectActor()
 {
   ObjectActor.apply(this, arguments);
 }
 
@@ -4512,8 +5102,28 @@ function findCssSelector(ele) {
 function positionInNodeList(element, nodeList) {
   for (var i = 0; i < nodeList.length; i++) {
     if (element === nodeList[i]) {
       return i;
     }
   }
   return -1;
 }
+
+/**
+ * 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;
+}
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -56,16 +56,17 @@ function WebConsoleActor(aConnection, aP
   this.conn.addActorPool(this._actorPool);
 
   this._prefs = {};
 
   this.dbg = new Debugger();
 
   this._protoChains = new Map();
   this._netEvents = new Map();
+  this._gripDepth = 0;
 
   this._onObserverNotification = this._onObserverNotification.bind(this);
   if (this.parentActor.isRootActor) {
     Services.obs.addObserver(this._onObserverNotification,
                              "last-pb-context-exited", false);
   }
 }
 
@@ -76,16 +77,23 @@ WebConsoleActor.prototype =
   /**
    * Debugger instance.
    *
    * @see jsdebugger.jsm
    */
   dbg: null,
 
   /**
+   * This is used by the ObjectActor to keep track of the depth of grip() calls.
+   * @private
+   * @type number
+   */
+  _gripDepth: null,
+
+  /**
    * Actor pool for all of the actors we send to the client.
    * @private
    * @type object
    * @see ActorPool
    */
   _actorPool: null,
 
   /**