Bug 1213932 - Restrict ServiceWorker logging to windows that match navigator.serviceWorker.controller.scriptURL;r=baku,r=past
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 05 Nov 2015 12:03:31 -0800
changeset 306928 1ed402253ec6c0855605284c7e5ebece4acd3579
parent 306927 ee13258380bae3159244c7194304cd327f1d4ca0
child 306929 cc2459e470302ad28e2bb3e3ccc610eec87b7a1a
push id7224
push usercliu@mozilla.com
push dateThu, 05 Nov 2015 20:35:36 +0000
reviewersbaku, past
bugs1213932
milestone45.0a1
Bug 1213932 - Restrict ServiceWorker logging to windows that match navigator.serviceWorker.controller.scriptURL;r=baku,r=past
devtools/server/actors/webconsole.js
devtools/shared/webconsole/test/chrome.ini
devtools/shared/webconsole/test/helper_serviceworker.js
devtools/shared/webconsole/test/test_console_serviceworker.html
devtools/shared/webconsole/utils.js
dom/base/Console.cpp
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1529,18 +1529,17 @@ WebConsoleActor.prototype =
    * @return object
    *         The object that can be sent to the remote client.
    */
   prepareConsoleMessageForRemote:
   function WCA_prepareConsoleMessageForRemote(aMessage, aUseObjectGlobal = true)
   {
     let result = WebConsoleUtils.cloneObject(aMessage);
 
-    result.workerType = CONSOLE_WORKER_IDS.indexOf(result.innerID) == -1
-                          ? 'none' : result.innerID;
+    result.workerType = WebConsoleUtils.getWorkerType(result) || "none";
 
     delete result.wrappedJSObject;
     delete result.ID;
     delete result.innerID;
     delete result.consoleID;
 
     result.arguments = Array.map(aMessage.arguments || [], (aObj) => {
       let dbgObj = this.makeDebuggeeValue(aObj, aUseObjectGlobal);
--- a/devtools/shared/webconsole/test/chrome.ini
+++ b/devtools/shared/webconsole/test/chrome.ini
@@ -1,26 +1,28 @@
 [DEFAULT]
 tags = devtools
 skip-if = buildapp == 'b2g'
 support-files =
   common.js
   data.json
   data.json^headers^
+  helper_serviceworker.js
   network_requests_iframe.html
   sandboxed_iframe.html
   console-test-worker.js
 
 [test_basics.html]
 [test_bug819670_getter_throws.html]
 [test_cached_messages.html]
 [test_commands_other.html]
 [test_commands_registration.html]
 [test_consoleapi.html]
 [test_consoleapi_innerID.html]
+[test_console_serviceworker.html]
 [test_console_styling.html]
 [test_file_uri.html]
 [test_reflow.html]
 [test_jsterm.html]
 [test_jsterm_autocomplete.html]
 [test_jsterm_cd_iframe.html]
 [test_jsterm_last_result.html]
 [test_jsterm_queryselector.html]
new file mode 100644
--- /dev/null
+++ b/devtools/shared/webconsole/test/helper_serviceworker.js
@@ -0,0 +1,2 @@
+
+console.log("Hello from serviceworker");
new file mode 100644
--- /dev/null
+++ b/devtools/shared/webconsole/test/test_console_serviceworker.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for the Console API and Service Workers</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 Console API and Service Workers</p>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+
+let SERVICE_WORKER_URL = "https://example.com/chrome/devtools/shared/webconsole/test/helper_serviceworker.js";
+let FRAME_URL = "https://example.com/chrome/devtools/shared/webconsole/test/sandboxed_iframe.html";
+
+let swClosed = new Promise(() => {});
+let expectedConsoleCalls = [
+    {
+      level: "log",
+      filename: /helper_serviceworker/,
+      arguments: ["Hello from serviceworker"],
+    }
+];
+let consoleCalls = [];
+
+let startTest = Task.async(function*() {
+  removeEventListener("load", startTest);
+
+  yield new Promise(resolve => {
+    SpecialPowers.pushPrefEnv({"set": [
+      ["devtools.webconsole.filter.serviceworkers", true]
+    ]}, resolve);
+  });
+
+  attachConsoleToTab(["ConsoleAPI"], onAttach);
+});
+addEventListener("load", startTest);
+
+function onAttach(state, response) {
+  onConsoleAPICall = onConsoleAPICall.bind(null, state);
+  state.dbgClient.addListener("consoleAPICall", onConsoleAPICall);
+
+  info("Loading a ServiceWorker that will use console API");
+  swClosed = new Promise(resolve => {
+    let iframe = document.createElement("iframe");
+    iframe.onload = function() {
+      let win = iframe.contentWindow;
+      info("Registering the service worker");
+      win.navigator.serviceWorker.register(SERVICE_WORKER_URL).then(swr => {
+
+        info("Service worker registered.  Unregistering");
+        swr.unregister().then(() => {
+          resolve();
+        });
+      }, error => {
+        info("Error registering service worker: " + error);
+      });
+    };
+    iframe.src = FRAME_URL;
+
+    document.body.appendChild(iframe);
+  });
+}
+
+function onConsoleAPICall(state, type, packet) {
+  info("received message level: " + packet.message.level);
+  is(packet.from, state.actor, "console API call actor");
+
+  consoleCalls.push(packet.message);
+  if (consoleCalls.length != expectedConsoleCalls.length) {
+    return;
+  }
+
+  state.dbgClient.removeListener("consoleAPICall", onConsoleAPICall);
+
+  expectedConsoleCalls.forEach(function(aMessage, aIndex) {
+    info("checking received console call #" + aIndex);
+    checkConsoleAPICall(consoleCalls[aIndex], expectedConsoleCalls[aIndex]);
+  });
+
+  consoleCalls = [];
+
+  closeDebugger(state, function() {
+    swClosed.then(() => {
+      SimpleTest.finish();
+    });
+  });
+}
+</script>
+</body>
+</html>
--- a/devtools/shared/webconsole/utils.js
+++ b/devtools/shared/webconsole/utils.js
@@ -47,16 +47,27 @@ var WebConsoleUtils = {
   {
     let str = Cc["@mozilla.org/supports-string;1"].
               createInstance(Ci.nsISupportsString);
     str.data = aString;
     return str;
   },
 
   /**
+   * Given a message, return one of CONSOLE_WORKER_IDS if it matches
+   * one of those.
+   *
+   * @return string
+   */
+  getWorkerType: function(message) {
+    let id = message ? message.innerID : null;
+    return CONSOLE_WORKER_IDS[CONSOLE_WORKER_IDS.indexOf(id)] || null;
+  },
+
+  /**
    * Clone an object.
    *
    * @param object aObject
    *        The object you want cloned.
    * @param boolean aRecursive
    *        Tells if you want to dig deeper into the object, to clone
    *        recursively.
    * @param function [aFilter]
@@ -111,16 +122,39 @@ var WebConsoleUtils = {
     let style = win.getComputedStyle(aFrom);
     aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
     aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
     aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
     aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
   },
 
   /**
+   * Recursively gather a list of window locations given
+   * a top level window.
+   *
+   * @param nsIDOMWindow aWindow
+   * @return Array
+   *         list of window locations as strings
+   */
+  getLocationsForFrames: function(aWindow)
+  {
+    let location = aWindow.location.toString();
+    let locations = [location];
+
+    if (aWindow.frames) {
+      for (let i = 0; i < aWindow.frames.length; i++) {
+        let frame = aWindow.frames[i];
+        locations = locations.concat(this.getLocationsForFrames(frame));
+      }
+    }
+
+    return locations;
+  },
+
+  /**
    * Gets the ID of the inner window of this DOM window.
    *
    * @param nsIDOMWindow aWindow
    * @return integer
    *         Inner ID for the given aWindow.
    */
   getInnerWindowId: function WCU_getInnerWindowId(aWindow)
   {
@@ -930,31 +964,64 @@ ConsoleAPIListener.prototype =
     if (!this.owner) {
       return;
     }
 
     // Here, wrappedJSObject is not a security wrapper but a property defined
     // by the XPCOM component which allows us to unwrap the XPCOM interface and
     // access the underlying JSObject.
     let apiMessage = aMessage.wrappedJSObject;
-    if (this.window && CONSOLE_WORKER_IDS.indexOf(apiMessage.innerID) == -1) {
-      let msgWindow = Services.wm.getCurrentInnerWindowWithId(apiMessage.innerID);
-      if (!msgWindow || !isWindowIncluded(this.window, msgWindow)) {
-        // Not the same window!
-        return;
-      }
-    }
-    if (this.consoleID && apiMessage.consoleID != this.consoleID) {
+
+    if (!this.isMessageRelevant(apiMessage)) {
       return;
     }
 
     this.owner.onConsoleAPICall(apiMessage);
   },
 
   /**
+   * Given a message, return true if this window should show it and false
+   * if it should be ignored.
+   *
+   * @param message
+   *        The message from the Storage Service
+   * @return bool
+   *         Do we care about this message?
+   */
+  isMessageRelevant: function(message) {
+    let workerType = WebConsoleUtils.getWorkerType(message);
+
+    if (this.window && workerType === "ServiceWorker") {
+      // For messages from Service Workers, message.ID is the
+      // scope, which can be used to determine whether it's controlling
+      // a window.
+      let scope = message.ID;
+      let locations = WebConsoleUtils.getLocationsForFrames(this.window);
+
+      if (!locations.some(loc => loc.startsWith(scope))) {
+        return false;
+      }
+    }
+
+    if (this.window && !workerType) {
+      let msgWindow = Services.wm.getCurrentInnerWindowWithId(message.innerID);
+      if (!msgWindow || !isWindowIncluded(this.window, msgWindow)) {
+        // Not the same window!
+        return false;
+      }
+    }
+
+    if (this.consoleID && message.consoleID !== this.consoleID) {
+      return false;
+    }
+
+    return true;
+  },
+
+  /**
    * Get the cached messages for the current inner window and its (i)frames.
    *
    * @param boolean [aIncludePrivate=false]
    *        Tells if you want to also retrieve messages coming from private
    *        windows. Defaults to false.
    * @return array
    *         The array of cached messages.
    */
@@ -974,19 +1041,19 @@ ConsoleAPIListener.prototype =
         messages = messages.concat(ConsoleAPIStorage.getEvents(id));
       });
     }
 
     CONSOLE_WORKER_IDS.forEach((id) => {
       messages = messages.concat(ConsoleAPIStorage.getEvents(id));
     });
 
-    if (this.consoleID) {
-      messages = messages.filter((m) => m.consoleID == this.consoleID);
-    }
+    messages = messages.filter(msg => {
+      return this.isMessageRelevant(msg);
+    });
 
     if (aIncludePrivate) {
       return messages;
     }
 
     return messages.filter((m) => !m.private);
   },
 
--- a/dom/base/Console.cpp
+++ b/dom/base/Console.cpp
@@ -493,26 +493,30 @@ private:
     if (aOuterWindow) {
       mCallData->SetIDs(aOuterWindow->WindowID(), aInnerWindow->WindowID());
     } else {
       ConsoleStackEntry frame;
       if (mCallData->mTopStackFrame) {
         frame = *mCallData->mTopStackFrame;
       }
 
-      nsString id;
+      nsString id = frame.mFilename;
+      nsString innerID;
       if (mWorkerPrivate->IsSharedWorker()) {
-        id = NS_LITERAL_STRING("SharedWorker");
+        innerID = NS_LITERAL_STRING("SharedWorker");
       } else if (mWorkerPrivate->IsServiceWorker()) {
-        id = NS_LITERAL_STRING("ServiceWorker");
+        innerID = NS_LITERAL_STRING("ServiceWorker");
+        // Use scope as ID so the webconsole can decide if the message should
+        // show up per tab
+        id.AssignWithConversion(mWorkerPrivate->WorkerName());
       } else {
-        id = NS_LITERAL_STRING("Worker");
+        innerID = NS_LITERAL_STRING("Worker");
       }
 
-      mCallData->SetIDs(frame.mFilename, id);
+      mCallData->SetIDs(id, innerID);
     }
 
     // Now we could have the correct window (if we are not window-less).
     mClonedData.mParent = aInnerWindow;
 
     ProcessCallData(aCx);
     mCallData->CleanupJSObjects();