Bug 1368899 - Refactor the JSON Viewer stream converter to avoid quirks mode. r=Honza
authorOriol <oriol-bugzilla@hotmail.com>
Tue, 30 May 2017 20:23:00 -0400
changeset 410022 1d284663f05ddfc94fed8c44b552a383b9168610
parent 410021 c77e79c09f1e6adf173e09ef829c96e507dbbb0d
child 410023 ecad8cb9f20c5c4a3e768ebb96ec52917474680d
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1368899
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1368899 - Refactor the JSON Viewer stream converter to avoid quirks mode. r=Honza
devtools/client/jsonview/converter-child.js
devtools/client/jsonview/css/general.css
devtools/client/jsonview/css/main.css
--- a/devtools/client/jsonview/converter-child.js
+++ b/devtools/client/jsonview/converter-child.js
@@ -46,125 +46,195 @@ Converter.prototype = {
   },
 
   /**
    * This component works as such:
    * 1. asyncConvertData captures the listener
    * 2. onStartRequest fires, initializes stuff, modifies the listener
    *    to match our output type
    * 3. onDataAvailable spits it back to the listener
-   * 4. onStopRequest spits it back to the listener and initializes
-        the JSON Viewer
+   * 4. onStopRequest spits it back to the listener
    * 5. convert does nothing, it's just the synchronous version
    *    of asyncConvertData
    */
   convert: function (fromStream, fromType, toType, ctx) {
     return fromStream;
   },
 
   asyncConvertData: function (fromType, toType, listener, ctx) {
     this.listener = listener;
   },
 
   onDataAvailable: function (request, context, inputStream, offset, count) {
     this.listener.onDataAvailable(...arguments);
   },
 
   onStartRequest: function (request, context) {
-    this.channel = request;
+    // Set the content type to HTML in order to parse the doctype, styles
+    // and scripts, but later a <plaintext> element will switch the tokenizer
+    // to the plaintext state in order to parse the JSON.
+    request.QueryInterface(Ci.nsIChannel);
+    request.contentType = "text/html";
 
-    // Let "save as" save the original JSON, not the viewer.
-    // To save with the proper extension we need the original content type,
-    // which has been replaced by application/vnd.mozilla.json.view
-    let originalType;
-    if (request instanceof Ci.nsIHttpChannel) {
-      try {
-        let header = request.getResponseHeader("Content-Type");
-        originalType = header.split(";")[0];
-      } catch (err) {
-        // Handled below
-      }
-    } else {
-      let uri = request.QueryInterface(Ci.nsIChannel).URI.spec;
-      let match = uri.match(/^data:(.*?)[,;]/);
-      if (match) {
-        originalType = match[1];
-      }
-    }
-    const JSON_TYPES = ["application/json", "application/manifest+json"];
-    if (!JSON_TYPES.includes(originalType)) {
-      originalType = JSON_TYPES[0];
-    }
-    request.QueryInterface(Ci.nsIWritablePropertyBag);
-    request.setProperty("contentType", originalType);
+    // JSON enforces UTF-8 charset (see bug 741776).
+    request.contentCharset = "UTF-8";
 
-    // Parse source as JSON. This is like text/plain, but enforcing
-    // UTF-8 charset (see bug 741776).
-    request.QueryInterface(Ci.nsIChannel);
-    request.contentType = JSON_TYPES[0];
-    this.charset = request.contentCharset = "UTF-8";
+    // Changing the content type breaks saving functionality. Fix it.
+    fixSave(request);
 
     // Because content might still have a reference to this window,
     // force setting it to a null principal to avoid it being same-
     // origin with (other) content.
     request.loadInfo.resetPrincipalToInheritToNullPrincipal();
 
+    // Start the request.
     this.listener.onStartRequest(request, context);
+
+    // Initialize stuff.
+    let win = NetworkHelper.getWindowForRequest(request);
+    exportData(win, request);
+    win.addEventListener("DOMContentLoaded", event => {
+      win.addEventListener("contentMessage", onContentMessage, false, true);
+    }, {once: true});
+
+    // Insert the initial HTML code.
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                      .createInstance(Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+    let stream = converter.convertToInputStream(initialHTML(win.document));
+    this.listener.onDataAvailable(request, context, stream, 0, stream.available());
   },
 
   onStopRequest: function (request, context, statusCode) {
-    let Locale = {
-      $STR: key => {
-        try {
-          return jsonViewStrings.GetStringFromName(key);
-        } catch (err) {
-          console.error(err);
-          return undefined;
-        }
-      }
-    };
-
-    let headers = {
-      response: [],
-      request: []
-    };
-    // The request doesn't have to be always nsIHttpChannel
-    // (e.g. in case of data: URLs)
-    if (request instanceof Ci.nsIHttpChannel) {
-      request.visitResponseHeaders({
-        visitHeader: function (name, value) {
-          headers.response.push({name: name, value: value});
-        }
-      });
-      request.visitRequestHeaders({
-        visitHeader: function (name, value) {
-          headers.request.push({name: name, value: value});
-        }
-      });
-    }
-
-    let win = NetworkHelper.getWindowForRequest(request);
-    JsonViewUtils.exportIntoContentScope(win, Locale, "Locale");
-    JsonViewUtils.exportIntoContentScope(win, headers, "headers");
-
-    win.addEventListener("DOMContentLoaded", event => {
-      win.addEventListener("contentMessage",
-        onContentMessage.bind(this), false, true);
-      loadJsonViewer(win.document);
-    }, {once: true});
-
-    this.listener.onStopRequest(this.channel, context, statusCode);
+    this.listener.onStopRequest(request, context, statusCode);
     this.listener = null;
   }
 };
 
+// Lets "save as" save the original JSON, not the viewer.
+// To save with the proper extension we need the original content type,
+// which has been replaced by application/vnd.mozilla.json.view
+function fixSave(request) {
+  let originalType;
+  if (request instanceof Ci.nsIHttpChannel) {
+    try {
+      let header = request.getResponseHeader("Content-Type");
+      originalType = header.split(";")[0];
+    } catch (err) {
+      // Handled below
+    }
+  } else {
+    let uri = request.QueryInterface(Ci.nsIChannel).URI.spec;
+    let match = uri.match(/^data:(.*?)[,;]/);
+    if (match) {
+      originalType = match[1];
+    }
+  }
+  const JSON_TYPES = ["application/json", "application/manifest+json"];
+  if (!JSON_TYPES.includes(originalType)) {
+    originalType = JSON_TYPES[0];
+  }
+  request.QueryInterface(Ci.nsIWritablePropertyBag);
+  request.setProperty("contentType", originalType);
+}
+
+// Exports variables that will be accessed by the non-privileged scripts.
+function exportData(win, request) {
+  let Locale = {
+    $STR: key => {
+      try {
+        return jsonViewStrings.GetStringFromName(key);
+      } catch (err) {
+        console.error(err);
+        return undefined;
+      }
+    }
+  };
+  JsonViewUtils.exportIntoContentScope(win, Locale, "Locale");
+
+  let headers = {
+    response: [],
+    request: []
+  };
+  // The request doesn't have to be always nsIHttpChannel
+  // (e.g. in case of data: URLs)
+  if (request instanceof Ci.nsIHttpChannel) {
+    request.visitResponseHeaders({
+      visitHeader: function (name, value) {
+        headers.response.push({name: name, value: value});
+      }
+    });
+    request.visitRequestHeaders({
+      visitHeader: function (name, value) {
+        headers.request.push({name: name, value: value});
+      }
+    });
+  }
+  JsonViewUtils.exportIntoContentScope(win, headers, "headers");
+}
+
+// Serializes a qualifiedName and an optional set of attributes into an HTML
+// start tag. Be aware qualifiedName and attribute names are not validated.
+// Attribute values are escaped with escapingString algorithm in attribute mode
+// (https://html.spec.whatwg.org/multipage/syntax.html#escapingString).
+function startTag(qualifiedName, attributes = {}) {
+  return Object.entries(attributes).reduce(function (prev, [attr, value]) {
+    return prev + " " + attr + "=\"" +
+      value.replace(/&/g, "&amp;")
+           .replace(/\u00a0/g, "&nbsp;")
+           .replace(/"/g, "&quot;") +
+      "\"";
+  }, "<" + qualifiedName) + ">";
+}
+
+// Builds an HTML string that will be used to load stylesheets and scripts,
+// and switch the parser to plaintext state.
+function initialHTML(doc) {
+  let os;
+  let platform = Services.appinfo.OS;
+  if (platform.startsWith("WINNT")) {
+    os = "win";
+  } else if (platform.startsWith("Darwin")) {
+    os = "mac";
+  } else {
+    os = "linux";
+  }
+
+  let base = doc.createElement("base");
+  base.href = "resource://devtools/client/jsonview/";
+
+  let style = doc.createElement("link");
+  style.rel = "stylesheet";
+  style.type = "text/css";
+  style.href = "css/main.css";
+
+  let script = doc.createElement("script");
+  script.src = "lib/require.js";
+  script.dataset.main = "viewer-config";
+  script.defer = true;
+
+  let head = doc.createElement("head");
+  head.append(base, style, script);
+
+  return "<!DOCTYPE html>\n" +
+    startTag("html", {
+      "platform": os,
+      "class": "theme-" + JsonViewUtils.getCurrentTheme(),
+      "dir": Services.locale.isAppLocaleRTL ? "rtl" : "ltr"
+    }) +
+    head.outerHTML +
+    startTag("body") +
+    startTag("div", {"id": "content"}) +
+    startTag("plaintext", {"id": "json"});
+}
+
 // Chrome <-> Content communication
 function onContentMessage(e) {
   // Do not handle events from different documents.
-  let win = NetworkHelper.getWindowForRequest(this.channel);
+  let win = this;
   if (win != e.target) {
     return;
   }
 
   let value = e.detail.value;
   switch (e.detail.type) {
     case "copy":
       copyString(win, value);
@@ -178,63 +248,16 @@ function onContentMessage(e) {
       // The window ID is needed when the JSON Viewer is inside an iframe.
       let windowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
       childProcessMessageManager.sendAsyncMessage(
         "devtools:jsonview:save", {url: value, windowID: windowID});
   }
 }
 
-// Loads the JSON Viewer into a text/plain document
-function loadJsonViewer(doc) {
-  function addStyleSheet(url) {
-    let link = doc.createElement("link");
-    link.rel = "stylesheet";
-    link.type = "text/css";
-    link.href = url;
-    doc.head.appendChild(link);
-  }
-
-  let os;
-  let platform = Services.appinfo.OS;
-  if (platform.startsWith("WINNT")) {
-    os = "win";
-  } else if (platform.startsWith("Darwin")) {
-    os = "mac";
-  } else {
-    os = "linux";
-  }
-
-  doc.documentElement.setAttribute("platform", os);
-  doc.documentElement.dataset.contentType = doc.contentType;
-  doc.documentElement.classList.add("theme-" + JsonViewUtils.getCurrentTheme());
-  doc.documentElement.dir = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
-
-  let base = doc.createElement("base");
-  base.href = "resource://devtools/client/jsonview/";
-  doc.head.appendChild(base);
-
-  addStyleSheet("../themes/variables.css");
-  addStyleSheet("../themes/common.css");
-  addStyleSheet("../themes/toolbars.css");
-  addStyleSheet("css/main.css");
-
-  let json = doc.querySelector("pre");
-  json.id = "json";
-  let content = doc.createElement("div");
-  content.id = "content";
-  content.appendChild(json);
-  doc.body.appendChild(content);
-
-  let script = doc.createElement("script");
-  script.src = "lib/require.js";
-  script.dataset.main = "viewer-config";
-  doc.body.appendChild(script);
-}
-
 function copyHeaders(win, headers) {
   let value = "";
   let eol = (Services.appinfo.OS !== "WINNT") ? "\n" : "\r\n";
 
   let responseHeaders = headers.response;
   for (let i = 0; i < responseHeaders.length; i++) {
     let header = responseHeaders[i];
     value += header.name + ": " + header.value + eol;
--- a/devtools/client/jsonview/css/general.css
+++ b/devtools/client/jsonview/css/general.css
@@ -28,28 +28,22 @@ pre {
 }
 
 #content {
   display: flow-root;
 }
 
 #json {
   margin: 8px;
+  white-space: pre-wrap;
 }
 
 /******************************************************************************/
 /* Dark Theme */
 
 body.theme-dark {
   color: var(--theme-body-color);
   background-color: var(--theme-body-background);
 }
 
 .theme-dark pre {
   background-color: var(--theme-body-background);
 }
-
-/******************************************************************************/
-/* Fixes for quirks mode */
-
-table {
-  font: inherit;
-}
--- a/devtools/client/jsonview/css/main.css
+++ b/devtools/client/jsonview/css/main.css
@@ -1,14 +1,17 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
-@import "resource://devtools/client/shared/components/reps/reps.css";
+@import "resource://devtools/client/themes/variables.css";
+@import "resource://devtools/client/themes/common.css";
+@import "resource://devtools/client/themes/toolbars.css";
+
 @import "resource://devtools/client/shared/components/tree/tree-view.css";
 @import "resource://devtools/client/shared/components/tabs/tabs.css";
 
 @import "general.css";
 @import "search-box.css";
 @import "toolbar.css";
 @import "json-panel.css";
 @import "text-panel.css";
@@ -48,9 +51,8 @@
 /******************************************************************************/
 /* Theme Firebug */
 
 /* JSON View is using bigger font-size for the main tabs so,
   let's overwrite the default value. */
 .theme-firebug .tabs .tabs-navigation {
   font-size: 14px;
 }
-