Bug 609872 - Ability to execute code in sub-documents (iframes/frames); r=past
authorMihai Sucan <mihai.sucan@gmail.com>
Tue, 25 Feb 2014 22:02:58 +0200
changeset 170953 9be1d1bc11fc9107a675e78ef48d6cad106d1aae
parent 170952 71cfb5244ca044b67c0851b5f74e4c7fd9ad15d6
child 170954 63059742d136ddc7e54e156df9c155b92f91eb8b
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewerspast
bugs609872
milestone30.0a1
Bug 609872 - Ability to execute code in sub-documents (iframes/frames); r=past
browser/devtools/webconsole/test/browser.ini
browser/devtools/webconsole/test/browser_webconsole_cd_iframe.js
browser/devtools/webconsole/test/test-bug-609872-cd-iframe-child.html
browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html
browser/locales/en-US/chrome/browser/devtools/webconsole.properties
toolkit/devtools/server/actors/webbrowser.js
toolkit/devtools/server/actors/webconsole.js
toolkit/devtools/webconsole/test/chrome.ini
toolkit/devtools/webconsole/test/test_jsterm_cd_iframe.html
toolkit/devtools/webconsole/utils.js
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -102,16 +102,18 @@ support-files =
   test_bug_770099_violation.html^headers^
   test-autocomplete-in-stackframe.html
   testscript.js
   test-bug_923281_console_log_filter.html
   test-bug_923281_test1.js
   test-bug_923281_test2.js
   test-bug_939783_console_trace_duplicates.html
   test-bug-952277-highlight-nodes-in-vview.html
+  test-bug-609872-cd-iframe-parent.html
+  test-bug-609872-cd-iframe-child.html
 
 [browser_bug664688_sandbox_update_after_navigation.js]
 [browser_bug_638949_copy_link_location.js]
 [browser_bug_862916_console_dir_and_filter_off.js]
 [browser_bug_865288_repeat_different_objects.js]
 [browser_bug_865871_variables_view_close_on_esc_key.js]
 [browser_bug_869003_inspect_cross_domain_object.js]
 [browser_bug_871156_ctrlw_close_tab.js]
@@ -262,8 +264,9 @@ run-if = os == "mac"
 [browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js]
 [browser_webconsole_output_01.js]
 [browser_webconsole_output_02.js]
 [browser_webconsole_output_03.js]
 [browser_webconsole_output_04.js]
 [browser_webconsole_output_events.js]
 [browser_console_variables_view_highlighter.js]
 [browser_webconsole_console_trace_duplicates.js]
+[browser_webconsole_cd_iframe.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_cd_iframe.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the cd() jsterm helper function works as expected. See bug 609872.
+
+function test() {
+  let hud;
+
+  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html";
+
+  const parentMessages = [{
+    name: "document.title in parent iframe",
+    text: "bug 609872 - iframe parent",
+    category: CATEGORY_OUTPUT,
+  }, {
+    name: "paragraph content",
+    text: "p: test for bug 609872 - iframe parent",
+    category: CATEGORY_OUTPUT,
+  }, {
+    name: "object content",
+    text: "obj: parent!",
+    category: CATEGORY_OUTPUT,
+  }];
+
+  const childMessages = [{
+    name: "document.title in child iframe",
+    text: "bug 609872 - iframe child",
+    category: CATEGORY_OUTPUT,
+  }, {
+    name: "paragraph content",
+    text: "p: test for bug 609872 - iframe child",
+    category: CATEGORY_OUTPUT,
+  }, {
+    name: "object content",
+    text: "obj: child!",
+    category: CATEGORY_OUTPUT,
+  }];
+
+  Task.spawn(runner).then(finishTest);
+
+  function* runner() {
+    const {tab} = yield loadTab(TEST_URI);
+    hud = yield openConsole(tab);
+
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+    info("cd() into the iframe using a selector");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd('iframe')");
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: childMessages });
+
+    info("cd() out of the iframe, reset to default window");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd()");
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+    info("call cd() with unexpected arguments");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd(document)");
+
+    yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        text: "Cannot cd()",
+        category: CATEGORY_OUTPUT,
+        severity: SEVERITY_ERROR,
+      }],
+    });
+
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd('p')");
+
+    yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        text: "Cannot cd()",
+        category: CATEGORY_OUTPUT,
+        severity: SEVERITY_ERROR,
+      }],
+    });
+
+    info("cd() into the iframe using an iframe DOM element");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd($('iframe'))");
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: childMessages });
+
+    info("cd(window.parent)");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd(window.parent)");
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+    yield closeConsole(tab);
+  }
+
+  function executeWindowTest() {
+    hud.jsterm.execute("document.title");
+    hud.jsterm.execute("'p: ' + document.querySelector('p').textContent");
+    hud.jsterm.execute("'obj: ' + window.foobarBug609872");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-child.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>test for bug 609872 - iframe child</title>
+    <!-- Any copyright is dedicated to the Public Domain.
+         http://creativecommons.org/publicdomain/zero/1.0/ -->
+  </head>
+  <body>
+    <p>test for bug 609872 - iframe child</p>
+    <script>window.foobarBug609872 = 'child!';</script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>test for bug 609872 - iframe parent</title>
+    <!-- Any copyright is dedicated to the Public Domain.
+         http://creativecommons.org/publicdomain/zero/1.0/ -->
+  </head>
+  <body>
+    <p>test for bug 609872 - iframe parent</p>
+    <script>window.foobarBug609872 = 'parent!';</script>
+    <iframe src="test-bug-609872-cd-iframe-child.html"></iframe>
+  </body>
+</html>
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -208,8 +208,12 @@ emptyPropertiesList=No properties to dis
 # LOCALIZATION NOTE (messageRepeats.tooltip2): the tooltip text that is displayed
 # when you hover the red bubble that shows how many times a message is repeated
 # in the web console output.
 # This is a semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 number of message repeats
 # example: 3 repeats
 messageRepeats.tooltip2=#1 repeat;#1 repeats
+
+# LOCALIZATION NOTE (cdFunctionInvalidArgument): the text that is displayed when
+# cd() is invoked with an invalid argument.
+cdFunctionInvalidArgument=Cannot cd() to the given window. Invalid argument.
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -1040,16 +1040,18 @@ BrowserAddonActor.prototype = {
  * is resumed before the navigation begins.
  *
  * @param BrowserTabActor aBrowserTabActor
  *        The tab actor associated with this listener.
  */
 function DebuggerProgressListener(aBrowserTabActor) {
   this._tabActor = aBrowserTabActor;
   this._tabActor._tabbrowser.addProgressListener(this);
+  let EventEmitter = devtools.require("devtools/shared/event-emitter");
+  EventEmitter.decorate(this);
 }
 
 DebuggerProgressListener.prototype = {
   onStateChange:
   makeInfallible(function DPL_onStateChange(aProgress, aRequest, aFlag, aStatus) {
     let isStart = aFlag & Ci.nsIWebProgressListener.STATE_START;
     let isStop = aFlag & Ci.nsIWebProgressListener.STATE_STOP;
     let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
@@ -1067,38 +1069,42 @@ DebuggerProgressListener.prototype = {
       // Proceed normally only if the debuggee is not paused.
       if (this._tabActor.threadActor.state == "paused") {
         aRequest.suspend();
         this._tabActor.threadActor.onResume();
         this._tabActor.threadActor.dbg.enabled = false;
         this._tabActor._pendingNavigation = aRequest;
       }
 
-      this._tabActor.threadActor.disableAllBreakpoints();
-      this._tabActor.conn.send({
+      let packet = {
         from: this._tabActor.actorID,
         type: "tabNavigated",
         url: aRequest.URI.spec,
         nativeConsoleAPI: true,
         state: "start"
-      });
+      };
+      this._tabActor.threadActor.disableAllBreakpoints();
+      this._tabActor.conn.send(packet);
+      this.emit("will-navigate", packet);
     } else if (isStop) {
       if (this._tabActor.threadActor.state == "running") {
         this._tabActor.threadActor.dbg.enabled = true;
       }
 
       let window = this._tabActor.window;
-      this._tabActor.conn.send({
+      let packet = {
         from: this._tabActor.actorID,
         type: "tabNavigated",
         url: this._tabActor.url,
         title: this._tabActor.title,
         nativeConsoleAPI: this._tabActor.hasNativeConsoleAPI(window),
         state: "stop"
-      });
+      };
+      this._tabActor.conn.send(packet);
+      this.emit("navigate", packet);
     }
   }, "DebuggerProgressListener.prototype.onStateChange"),
 
   /**
    * Destroy the progress listener instance.
    */
   destroy: function DPL_destroy() {
     if (this._tabActor._tabbrowser.removeProgressListener) {
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -54,20 +54,20 @@ function WebConsoleActor(aConnection, aP
 
   this._actorPool = new ActorPool(this.conn);
   this.conn.addActorPool(this._actorPool);
 
   this._prefs = {};
 
   this.dbg = new Debugger();
 
-  this._protoChains = new Map();
   this._netEvents = new Map();
   this._gripDepth = 0;
 
+  this._onWillNavigate = this._onWillNavigate.bind(this);
   this._onObserverNotification = this._onObserverNotification.bind(this);
   if (this.parentActor.isRootActor) {
     Services.obs.addObserver(this._onObserverNotification,
                              "last-pb-context-exited", false);
   }
 }
 
 WebConsoleActor.l10n = new WebConsoleUtils.l10n("chrome://global/locale/console.properties");
@@ -108,26 +108,16 @@ WebConsoleActor.prototype =
    * created with sendHTTPRequest.
    *
    * @private
    * @type Map
    */
   _netEvents: null,
 
   /**
-   * A cache of prototype chains for objects that have received a
-   * prototypeAndProperties request.
-   *
-   * @private
-   * @type Map
-   * @see dbg-script-actors.js, ThreadActor._protoChains
-   */
-  _protoChains: null,
-
-  /**
    * The debugger server connection instance.
    * @type object
    */
   conn: null,
 
   /**
    * The window we work with.
    * @type nsIDOMWindow
@@ -204,16 +194,41 @@ WebConsoleActor.prototype =
   /**
    * A weak reference to the last chrome window we used to work with.
    *
    * @private
    * @type nsIWeakReference
    */
   _lastChromeWindow: null,
 
+  // The evalWindow is used at the scope for JS evaluation.
+  _evalWindow: null,
+  get evalWindow() {
+    return this._evalWindow || this.window;
+  },
+
+  set evalWindow(aWindow) {
+    this._evalWindow = aWindow;
+
+    if (!this._progressListenerActive && this.parentActor._progressListener) {
+      this.parentActor._progressListener.once("will-navigate", this._onWillNavigate);
+      this._progressListenerActive = true;
+    }
+  },
+
+  /**
+   * Flag used to track if we are listening for events from the progress
+   * listener of the tab actor. We use the progress listener to clear
+   * this.evalWindow on page navigation.
+   *
+   * @private
+   * @type boolean
+   */
+  _progressListenerActive: false,
+
   /**
    * The ConsoleServiceListener instance.
    * @type object
    */
   consoleServiceListener: null,
 
   /**
    * The ConsoleAPIListener instance.
@@ -290,18 +305,19 @@ WebConsoleActor.prototype =
     }
     this.conn.removeActorPool(this._actorPool);
     if (this.parentActor.isRootActor) {
       Services.obs.removeObserver(this._onObserverNotification,
                                   "last-pb-context-exited");
     }
     this._actorPool = null;
 
+    this._jstermHelpersCache = null;
+    this._evalWindow = null;
     this._netEvents.clear();
-    this._protoChains.clear();
     this.dbg.enabled = false;
     this.dbg = null;
     this.conn = null;
   },
 
   /**
    * Create and return an environment actor that corresponds to the provided
    * Debugger.Environment. This is a straightforward clone of the ThreadActor's
@@ -720,17 +736,17 @@ WebConsoleActor.prototype =
       }
       else {
         Cu.reportError("Web Console Actor: the frame actor was not found: " +
                        frameActorId);
       }
     }
     // This is the general case (non-paused debugger)
     else {
-      dbgObject = this.dbg.makeGlobalObjectReference(this.window);
+      dbgObject = this.dbg.makeGlobalObjectReference(this.evalWindow);
     }
 
     let result = JSPropertyProvider(dbgObject, environment, aRequest.text,
                                     aRequest.cursor, frameActorId) || {};
     let matches = result.matches || [];
     let reqText = aRequest.text.substr(0, aRequest.cursor);
 
     // We consider '$' as alphanumerc because it is used in the names of some
@@ -817,22 +833,23 @@ WebConsoleActor.prototype =
    * @return object
    *         The same object as |this|, but with an added |sandbox| property.
    *         The sandbox holds methods and properties that can be used as
    *         bindings during JS evaluation.
    */
   _getJSTermHelpers: function WCA__getJSTermHelpers(aDebuggerGlobal)
   {
     let helpers = {
-      window: this.window,
+      window: this.evalWindow,
       chromeWindow: this.chromeWindow.bind(this),
       makeDebuggeeValue: aDebuggerGlobal.makeDebuggeeValue.bind(aDebuggerGlobal),
       createValueGrip: this.createValueGrip.bind(this),
       sandbox: Object.create(null),
       helperResult: null,
+      consoleActor: this,
     };
     JSTermHelpers(helpers);
 
     // Make sure the helpers can be used during eval.
     for (let name in helpers.sandbox) {
       let desc = Object.getOwnPropertyDescriptor(helpers.sandbox, name);
       if (desc.get || desc.set) {
         continue;
@@ -919,22 +936,22 @@ WebConsoleActor.prototype =
 
     // If we've been given a frame actor in whose scope we should evaluate the
     // expression, be sure to use that frame's Debugger (that is, the JavaScript
     // debugger's Debugger) for the whole operation, not the console's Debugger.
     // (One Debugger will treat a different Debugger's Debugger.Object instances
     // as ordinary objects, not as references to be followed, so mixing
     // debuggers causes strange behaviors.)
     let dbg = frame ? frameActor.threadActor.dbg : this.dbg;
-    let dbgWindow = dbg.makeGlobalObjectReference(this.window);
+    let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow);
 
     // If we have an object to bind to |_self|, create a Debugger.Object
     // referring to that object, belonging to dbg.
     let bindSelf = null;
-    let dbgWindow = dbg.makeGlobalObjectReference(this.window);
+    let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow);
     if (aOptions.bindObjectActor) {
       let objActor = this.getActorByID(aOptions.bindObjectActor);
       if (objActor) {
         let jsObj = objActor.obj.unsafeDereference();
         // If we use the makeDebuggeeValue method of jsObj's own global, then
         // we'll get a D.O that sees jsObj as viewed from its own compartment -
         // that is, without wrappers. The evalWithBindings call will then wrap
         // jsObj appropriately for the evaluation compartment.
@@ -1284,17 +1301,27 @@ WebConsoleActor.prototype =
     switch (aTopic) {
       case "last-pb-context-exited":
         this.conn.send({
           from: this.actorID,
           type: "lastPrivateContextExited",
         });
         break;
     }
-  }
+  },
+
+  /**
+   * The "will-navigate" progress listener. This is used to clear the current
+   * eval scope.
+   */
+  _onWillNavigate: function WCA__onWillNavigate()
+  {
+    this._evalWindow = null;
+    this._progressListenerActive = false;
+  },
 };
 
 WebConsoleActor.prototype.requestTypes =
 {
   startListeners: WebConsoleActor.prototype.onStartListeners,
   stopListeners: WebConsoleActor.prototype.onStopListeners,
   getCachedMessages: WebConsoleActor.prototype.onGetCachedMessages,
   evaluateJS: WebConsoleActor.prototype.onEvaluateJS,
--- a/toolkit/devtools/webconsole/test/chrome.ini
+++ b/toolkit/devtools/webconsole/test/chrome.ini
@@ -16,8 +16,9 @@ support-files =
 [test_network_longstring.html]
 [test_network_post.html]
 [test_nsiconsolemessage.html]
 [test_object_actor.html]
 [test_object_actor_native_getters.html]
 [test_object_actor_native_getters_lenient_this.html]
 [test_page_errors.html]
 [test_throw.html]
+[test_jsterm_cd_iframe.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/test_jsterm_cd_iframe.html
@@ -0,0 +1,154 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for the cd() function</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript;version=1.8" src="common.js"></script>
+  <!-- Any copyright is dedicated to the Public Domain.
+     - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the cd() function</p>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+
+let gState;
+
+function startTest()
+{
+  removeEventListener("load", startTest);
+
+  attachConsole([], onAttach, true);
+}
+
+function onAttach(aState, aResponse)
+{
+  top.foobarObject = Object.create(null);
+  top.foobarObject.bug609872 = "parent";
+
+  window.foobarObject = Object.create(null);
+  window.foobarObject.bug609872 = "child";
+
+  gState = aState;
+
+  let tests = [doCheckParent, doCdIframe, doCheckIframe, doCdParent,
+      doCheckParent2];
+  runTests(tests, testEnd);
+}
+
+function doCheckParent()
+{
+  info("check parent window");
+  gState.client.evaluateJS("window.foobarObject.bug609872",
+      onFooObjectFromParent);
+}
+
+function onFooObjectFromParent(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "window.foobarObject.bug609872",
+    result: "parent",
+  });
+
+  ok(!aResponse.exception, "no eval exception");
+  ok(!aResponse.helperResult, "no helper result");
+
+  nextTest();
+}
+
+function doCdIframe()
+{
+  info("test cd('iframe')");
+  gState.client.evaluateJS("cd('iframe')", onCdIframe);
+}
+
+function onCdIframe(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "cd('iframe')",
+    result: { type: "undefined" },
+    helperResult: { type: "cd" },
+  });
+
+  ok(!aResponse.exception, "no eval exception");
+
+  nextTest();
+}
+
+function doCheckIframe()
+{
+  info("check foobarObject from the iframe");
+  gState.client.evaluateJS("window.foobarObject.bug609872",
+                           onFooObjectFromIframe);
+}
+
+function onFooObjectFromIframe(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "window.foobarObject.bug609872",
+    result: "child",
+  });
+
+  ok(!aResponse.exception, "no js eval exception");
+  ok(!aResponse.helperResult, "no helper result");
+
+  nextTest();
+}
+
+function doCdParent()
+{
+  info("test cd() back to parent");
+  gState.client.evaluateJS("cd()", onCdParent);
+}
+
+function onCdParent(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "cd()",
+    result: { type: "undefined" },
+    helperResult: { type: "cd" },
+  });
+
+  ok(!aResponse.exception, "no eval exception");
+
+  nextTest();
+}
+
+function doCheckParent2()
+{
+  gState.client.evaluateJS("window.foobarObject.bug609872",
+                           onFooObjectFromParent2);
+}
+
+function onFooObjectFromParent2(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "window.foobarObject.bug609872",
+    result: "parent",
+  });
+
+  ok(!aResponse.exception, "no eval exception");
+  ok(!aResponse.helperResult, "no helper result");
+
+  nextTest();
+}
+
+function testEnd()
+{
+  closeDebugger(gState, function() {
+    gState = null;
+    SimpleTest.finish();
+  });
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -1508,16 +1508,50 @@ function JSTermHelpers(aOwner)
    * Opens a help window in MDN.
    */
   aOwner.sandbox.help = function JSTH_help()
   {
     aOwner.helperResult = { type: "help" };
   };
 
   /**
+   * Change the JS evaluation scope.
+   *
+   * @param DOMElement|string|window aWindow
+   *        The window object to use for eval scope. This can be a string that
+   *        is used to perform document.querySelector(), to find the iframe that
+   *        you want to cd() to. A DOMElement can be given as well, the
+   *        .contentWindow property is used. Lastly, you can directly pass
+   *        a window object. If you call cd() with no arguments, the current
+   *        eval scope is cleared back to its default (the top window).
+   */
+  aOwner.sandbox.cd = function JSTH_cd(aWindow)
+  {
+    if (!aWindow) {
+      aOwner.consoleActor.evalWindow = null;
+      aOwner.helperResult = { type: "cd" };
+      return;
+    }
+
+    if (typeof aWindow == "string") {
+      aWindow = aOwner.window.document.querySelector(aWindow);
+    }
+    if (aWindow instanceof Ci.nsIDOMElement && aWindow.contentWindow) {
+      aWindow = aWindow.contentWindow;
+    }
+    if (!(aWindow instanceof Ci.nsIDOMWindow)) {
+      aOwner.helperResult = { type: "error", message: "cdFunctionInvalidArgument" };
+      return;
+    }
+
+    aOwner.consoleActor.evalWindow = aWindow;
+    aOwner.helperResult = { type: "cd" };
+  };
+
+  /**
    * Inspects the passed aObject. This is done by opening the PropertyPanel.
    *
    * @param object aObject
    *        Object to inspect.
    */
   aOwner.sandbox.inspect = function JSTH_inspect(aObject)
   {
     let dbgObj = aOwner.makeDebuggeeValue(aObject);