Issue #1138 - Part 1: refactor the JSON Viewer stream converter

Mozilla Bug 1367894
This commit is contained in:
yami 2019-07-30 15:27:59 +02:00 committed by Roy Tam
commit 8f1e1f40c7
4 changed files with 153 additions and 194 deletions

View file

@ -23,10 +23,6 @@ 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=*/*";
@ -61,9 +57,9 @@ let Converter = Class({
* 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
* 3. onDataAvailable spits it back to the listener
* 4. onStopRequest spits it back to the listener and initializes
* the JSON Viewer
* 5. convert does nothing, it's just the synchronous version
* of asyncConvertData
*/
@ -76,60 +72,52 @@ let Converter = Class({
},
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;
}
this.listener.onDataAvailable(...arguments);
},
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";
// 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);
// 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";
// 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();
request.loadInfo.resetPrincipalsToNullPrincipal();
this.listener.onStartRequest(this.channel, context);
this.listener.onStartRequest(request, 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 {
@ -141,12 +129,10 @@ let Converter = Class({
}
};
JsonViewUtils.exportIntoContentScope(win, Locale, "Locale");
Events.once(win, "DOMContentLoaded", event => {
win.addEventListener("contentMessage",
this.onContentMessage.bind(this), false, true);
});
let headers = {
response: [],
request: []
};
// The request doesn't have to be always nsIHttpChannel
// (e.g. in case of data: URLs)
@ -156,7 +142,6 @@ let Converter = Class({
headers.response.push({name: name, value: value});
}
});
request.visitRequestHeaders({
visitHeader: function (name, value) {
headers.request.push({name: name, value: value});
@ -164,156 +149,125 @@ let Converter = Class({
});
}
let outputDoc = "";
let win = NetworkHelper.getWindowForRequest(request);
JsonViewUtils.exportIntoContentScope(win, Locale, "Locale");
JsonViewUtils.exportIntoContentScope(win, headers, "headers");
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());
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 = 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");
}
});
// Chrome <-> Content communication
function onContentMessage(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":
copyString(win, value);
break;
case "copy-headers":
copyHeaders(win, value);
break;
case "save":
// 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;
}
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),

View file

@ -28,8 +28,7 @@ pre {
font-family: var(--monospace-font-family);
}
#json,
#headers {
#json {
display: none;
}
@ -44,3 +43,9 @@ body.theme-dark {
.theme-dark pre {
background-color: var(--theme-body-background);
}
/******************************************************************************/
/* Fixes for quirks mode */
table {
font: inherit;
}

View file

@ -12,7 +12,6 @@ define(function (require, exports, module) {
const { MainTabbedArea } = createFactories(require("./components/main-tabbed-area"));
const json = document.getElementById("json");
const headers = document.getElementById("headers");
let jsonData;
@ -27,13 +26,12 @@ define(function (require, exports, module) {
jsonText: json.textContent,
jsonPretty: null,
json: jsonData,
headers: JSON.parse(headers.textContent),
headers: window.headers,
tabActive: 0,
prettified: false
};
json.remove();
headers.remove();
/**
* Application actions/commands. This list implements all commands

View file

@ -96,6 +96,8 @@ exports.exportIntoContentScope = function (win, obj, defineAs) {
Cu.exportFunction(propValue, clone, {
defineAs: propName
});
} else {
clone[propName] = Cu.cloneInto(propValue, win);
}
}
};