Mypal/devtools/client/jsonview/converter-child.js

318 lines
9.3 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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);
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 spits it back to the listener
* 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) {
// 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";
// JSON enforces UTF-8 charset (see bug 741776).
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.resetPrincipalsToNullPrincipal();
// 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) {
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 = this;
if (win != e.target) {
return;
}
let value = e.detail.value;
switch (e.detail.type) {
case "copy":
copyString(win, value);
break;
case "copy-headers":
copyHeaders(win, value);
break;
case "save":
childProcessMessageManager.sendAsyncMessage(
"devtools:jsonview:save", value);
}
}
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;
}
value += eol;
let requestHeaders = headers.request;
for (let i = 0; i < requestHeaders.length; i++) {
let header = requestHeaders[i];
value += header.name + ": " + header.value + eol;
}
copyString(win, value);
}
function copyString(win, string) {
win.document.addEventListener("copy", event => {
event.clipboardData.setData("text/plain", string);
event.preventDefault();
}, {once: true});
win.document.execCommand("copy", false, null);
}
// 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
};