diff options
Diffstat (limited to 'devtools/client/jsonview/converter-child.js')
-rw-r--r-- | devtools/client/jsonview/converter-child.js | 345 |
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, "<") + .replace(/>/g, ">") : ""; + }, + + 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 +}; |