summaryrefslogtreecommitdiffstats
path: root/devtools/client/jsonview/converter-child.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/jsonview/converter-child.js')
-rw-r--r--devtools/client/jsonview/converter-child.js345
1 files changed, 345 insertions, 0 deletions
diff --git a/devtools/client/jsonview/converter-child.js b/devtools/client/jsonview/converter-child.js
new file mode 100644
index 000000000..61aa0c9a3
--- /dev/null
+++ b/devtools/client/jsonview/converter-child.js
@@ -0,0 +1,345 @@
+/* -*- indent-tabs-mode: nil; 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 {Cc, Ci, components} = require("chrome");
+const Services = require("Services");
+const {Class} = require("sdk/core/heritage");
+const {Unknown} = require("sdk/platform/xpcom");
+const xpcom = require("sdk/platform/xpcom");
+const Events = require("sdk/dom/events");
+const Clipboard = require("sdk/clipboard");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+ "devtools/shared/webconsole/network-helper");
+loader.lazyRequireGetter(this, "JsonViewUtils",
+ "devtools/client/jsonview/utils");
+
+const childProcessMessageManager =
+ Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+
+// Amount of space that will be allocated for the stream's backing-store.
+// Must be power of 2. Used to copy the data stream in onStopRequest.
+const SEGMENT_SIZE = Math.pow(2, 17);
+
+const JSON_VIEW_MIME_TYPE = "application/vnd.mozilla.json.view";
+const CONTRACT_ID = "@mozilla.org/streamconv;1?from=" +
+ JSON_VIEW_MIME_TYPE + "&to=*/*";
+const CLASS_ID = "{d8c9acee-dec5-11e4-8c75-1681e6b88ec1}";
+
+// Localization
+let jsonViewStrings = Services.strings.createBundle(
+ "chrome://devtools/locale/jsonview.properties");
+
+/**
+ * This object detects 'application/vnd.mozilla.json.view' content type
+ * and converts it into a JSON Viewer application that allows simple
+ * JSON inspection.
+ *
+ * Inspired by JSON View: https://github.com/bhollis/jsonview/
+ */
+let Converter = Class({
+ extends: Unknown,
+
+ interfaces: [
+ "nsIStreamConverter",
+ "nsIStreamListener",
+ "nsIRequestObserver"
+ ],
+
+ get wrappedJSObject() {
+ return this;
+ },
+
+ /**
+ * 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 transcodes the data into a UTF-8 string
+ * 4. onStopRequest gets the collected data and converts it,
+ * spits it 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) {
+ // From https://developer.mozilla.org/en/Reading_textual_data
+ let is = Cc["@mozilla.org/intl/converter-input-stream;1"]
+ .createInstance(Ci.nsIConverterInputStream);
+ is.init(inputStream, this.charset, -1,
+ Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+ // Seed it with something positive
+ while (count) {
+ let str = {};
+ let bytesRead = is.readString(count, str);
+ if (!bytesRead) {
+ break;
+ }
+ count -= bytesRead;
+ this.data += str.value;
+ }
+ },
+
+ onStartRequest: function (request, context) {
+ this.data = "";
+ this.uri = request.QueryInterface(Ci.nsIChannel).URI.spec;
+
+ // Sets the charset if it is available. (For documents loaded from the
+ // filesystem, this is not set.)
+ this.charset =
+ request.QueryInterface(Ci.nsIChannel).contentCharset || "UTF-8";
+
+ this.channel = request;
+ this.channel.contentType = "text/html";
+ this.channel.contentCharset = "UTF-8";
+ // 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.
+ this.channel.loadInfo.resetPrincipalsToNullPrincipal();
+
+ this.listener.onStartRequest(this.channel, context);
+ },
+
+ /**
+ * This should go something like this:
+ * 1. Make sure we have a unicode string.
+ * 2. Convert it to a Javascript object.
+ * 2.1 Removes the callback
+ * 3. Convert that to HTML? Or XUL?
+ * 4. Spit it back out at the listener
+ */
+ onStopRequest: function (request, context, statusCode) {
+ let headers = {
+ response: [],
+ request: []
+ };
+
+ let win = NetworkHelper.getWindowForRequest(request);
+
+ let Locale = {
+ $STR: key => {
+ try {
+ return jsonViewStrings.GetStringFromName(key);
+ } catch (err) {
+ console.error(err);
+ return undefined;
+ }
+ }
+ };
+
+ JsonViewUtils.exportIntoContentScope(win, Locale, "Locale");
+
+ Events.once(win, "DOMContentLoaded", event => {
+ win.addEventListener("contentMessage",
+ this.onContentMessage.bind(this), false, true);
+ });
+
+ // 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 outputDoc = "";
+
+ try {
+ headers = JSON.stringify(headers);
+ outputDoc = this.toHTML(this.data, headers, this.uri);
+ } catch (e) {
+ console.error("JSON Viewer ERROR " + e);
+ outputDoc = this.toErrorPage(e, this.data, this.uri);
+ }
+
+ let storage = Cc["@mozilla.org/storagestream;1"]
+ .createInstance(Ci.nsIStorageStream);
+
+ storage.init(SEGMENT_SIZE, 0xffffffff, null);
+ let out = storage.getOutputStream(0);
+
+ let binout = Cc["@mozilla.org/binaryoutputstream;1"]
+ .createInstance(Ci.nsIBinaryOutputStream);
+
+ binout.setOutputStream(out);
+ binout.writeUtf8Z(outputDoc);
+ binout.close();
+
+ // We need to trim 4 bytes off the front (this could be underlying bug).
+ let trunc = 4;
+ let instream = storage.newInputStream(trunc);
+
+ // Pass the data to the main content listener
+ this.listener.onDataAvailable(this.channel, context, instream, 0,
+ instream.available());
+
+ this.listener.onStopRequest(this.channel, context, statusCode);
+
+ this.listener = null;
+ },
+
+ htmlEncode: function (t) {
+ return t !== null ? t.toString()
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;") : "";
+ },
+
+ toHTML: function (json, headers, title) {
+ let themeClassName = "theme-" + JsonViewUtils.getCurrentTheme();
+ let clientBaseUrl = "resource://devtools/client/";
+ let baseUrl = clientBaseUrl + "jsonview/";
+ let themeVarsUrl = clientBaseUrl + "themes/variables.css";
+ let commonUrl = clientBaseUrl + "themes/common.css";
+ let toolbarsUrl = clientBaseUrl + "themes/toolbars.css";
+
+ let os;
+ let platform = Services.appinfo.OS;
+ if (platform.startsWith("WINNT")) {
+ os = "win";
+ } else if (platform.startsWith("Darwin")) {
+ os = "mac";
+ } else {
+ os = "linux";
+ }
+
+ return "<!DOCTYPE html>\n" +
+ "<html platform=\"" + os + "\" class=\"" + themeClassName + "\">" +
+ "<head><title>" + this.htmlEncode(title) + "</title>" +
+ "<base href=\"" + this.htmlEncode(baseUrl) + "\">" +
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"" +
+ themeVarsUrl + "\">" +
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"" +
+ commonUrl + "\">" +
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"" +
+ toolbarsUrl + "\">" +
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"css/main.css\">" +
+ "<script data-main=\"viewer-config\" src=\"lib/require.js\"></script>" +
+ "</head><body>" +
+ "<div id=\"content\"></div>" +
+ "<div id=\"json\">" + this.htmlEncode(json) + "</div>" +
+ "<div id=\"headers\">" + this.htmlEncode(headers) + "</div>" +
+ "</body></html>";
+ },
+
+ toErrorPage: function (error, data, uri) {
+ // Escape unicode nulls
+ data = data.replace("\u0000", "\uFFFD");
+
+ let errorInfo = error + "";
+
+ let output = "<div id=\"error\">" + "error parsing";
+ if (errorInfo.message) {
+ output += "<div class=\"errormessage\">" + errorInfo.message + "</div>";
+ }
+
+ output += "</div><div id=\"json\">" + this.highlightError(data,
+ errorInfo.line, errorInfo.column) + "</div>";
+
+ return "<!DOCTYPE html>\n" +
+ "<html><head><title>" + this.htmlEncode(uri + " - Error") + "</title>" +
+ "<base href=\"" + this.htmlEncode(this.data.url()) + "\">" +
+ "</head><body>" +
+ output +
+ "</body></html>";
+ },
+
+ // Chrome <-> Content communication
+
+ onContentMessage: function (e) {
+ // Do not handle events from different documents.
+ let win = NetworkHelper.getWindowForRequest(this.channel);
+ if (win != e.target) {
+ return;
+ }
+
+ let value = e.detail.value;
+ switch (e.detail.type) {
+ case "copy":
+ Clipboard.set(value, "text");
+ break;
+
+ case "copy-headers":
+ this.copyHeaders(value);
+ break;
+
+ case "save":
+ childProcessMessageManager.sendAsyncMessage(
+ "devtools:jsonview:save", value);
+ }
+ },
+
+ copyHeaders: function (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;
+ }
+
+ value += eol;
+
+ let requestHeaders = headers.request;
+ for (let i = 0; i < requestHeaders.length; i++) {
+ let header = requestHeaders[i];
+ value += header.name + ": " + header.value + eol;
+ }
+
+ Clipboard.set(value, "text");
+ }
+});
+
+// Stream converter component definition
+let service = xpcom.Service({
+ id: components.ID(CLASS_ID),
+ contract: CONTRACT_ID,
+ Component: Converter,
+ register: false,
+ unregister: false
+});
+
+function register() {
+ if (!xpcom.isRegistered(service)) {
+ xpcom.register(service);
+ return true;
+ }
+ return false;
+}
+
+function unregister() {
+ if (xpcom.isRegistered(service)) {
+ xpcom.unregister(service);
+ return true;
+ }
+ return false;
+}
+
+exports.JsonViewService = {
+ register: register,
+ unregister: unregister
+};