summaryrefslogtreecommitdiffstats
path: root/devtools/client/jsonview
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/jsonview')
-rw-r--r--devtools/client/jsonview/converter-child.js399
-rw-r--r--devtools/client/jsonview/css/general.css4
-rw-r--r--devtools/client/jsonview/css/main.css4
-rw-r--r--devtools/client/jsonview/json-viewer.js26
-rw-r--r--devtools/client/jsonview/utils.js2
5 files changed, 206 insertions, 229 deletions
diff --git a/devtools/client/jsonview/converter-child.js b/devtools/client/jsonview/converter-child.js
index 61aa0c9a3..65327c395 100644
--- a/devtools/client/jsonview/converter-child.js
+++ b/devtools/client/jsonview/converter-child.js
@@ -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,8 @@ 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
* 5. convert does nothing, it's just the synchronous version
* of asyncConvertData
*/
@@ -76,243 +71,221 @@ 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;
+ // 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";
- // 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";
+ // JSON enforces UTF-8 charset (see bug 741776).
+ request.contentCharset = "UTF-8";
+
+ // Changing the content type breaks saving functionality. Fix it.
+ fixSave(request);
- 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();
+ request.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: []
- };
+ // 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());
+ },
- 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 = "";
+ 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 {
- 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 header = request.getResponseHeader("Content-Type");
+ originalType = header.split(";")[0];
+ } catch (err) {
+ // Handled below
}
-
- 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, "&amp;")
- .replace(/"/g, "&quot;")
- .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";
+ } 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);
+}
- 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");
+// 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");
+}
- let errorInfo = error + "";
+// 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) + ">";
+}
- let output = "<div id=\"error\">" + "error parsing";
- if (errorInfo.message) {
- output += "<div class=\"errormessage\">" + errorInfo.message + "</div>";
- }
+// 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";
+ }
- output += "</div><div id=\"json\">" + this.highlightError(data,
- errorInfo.line, errorInfo.column) + "</div>";
+ 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"});
+}
- 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
+function onContentMessage(e) {
+ // Do not handle events from different documents.
+ let win = this;
+ if (win != e.target) {
+ return;
+ }
- // Chrome <-> Content communication
+ let value = e.detail.value;
+ switch (e.detail.type) {
+ case "copy":
+ copyString(win, value);
+ break;
- onContentMessage: function (e) {
- // Do not handle events from different documents.
- let win = NetworkHelper.getWindowForRequest(this.channel);
- if (win != e.target) {
- return;
- }
+ case "copy-headers":
+ copyHeaders(win, value);
+ break;
- let value = e.detail.value;
- switch (e.detail.type) {
- case "copy":
- Clipboard.set(value, "text");
- break;
+ case "save":
+ childProcessMessageManager.sendAsyncMessage(
+ "devtools:jsonview:save", value);
+ }
+}
- case "copy-headers":
- this.copyHeaders(value);
- break;
+function copyHeaders(win, headers) {
+ let value = "";
+ let eol = (Services.appinfo.OS !== "WINNT") ? "\n" : "\r\n";
- case "save":
- childProcessMessageManager.sendAsyncMessage(
- "devtools:jsonview:save", value);
- }
- },
+ let responseHeaders = headers.response;
+ for (let i = 0; i < responseHeaders.length; i++) {
+ let header = responseHeaders[i];
+ value += header.name + ": " + header.value + eol;
+ }
- copyHeaders: function (headers) {
- let value = "";
- let eol = (Services.appinfo.OS !== "WINNT") ? "\n" : "\r\n";
+ value += eol;
- let responseHeaders = headers.response;
- for (let i = 0; i < responseHeaders.length; i++) {
- let header = responseHeaders[i];
- value += header.name + ": " + header.value + eol;
- }
+ let requestHeaders = headers.request;
+ for (let i = 0; i < requestHeaders.length; i++) {
+ let header = requestHeaders[i];
+ value += header.name + ": " + header.value + eol;
+ }
- value += eol;
+ copyString(win, value);
+}
- let requestHeaders = headers.request;
- for (let i = 0; i < requestHeaders.length; i++) {
- let header = requestHeaders[i];
- value += header.name + ": " + header.value + eol;
- }
+function copyString(win, string) {
+ win.document.addEventListener("copy", event => {
+ event.clipboardData.setData("text/plain", string);
+ event.preventDefault();
+ }, {once: true});
- Clipboard.set(value, "text");
- }
-});
+ win.document.execCommand("copy", false, null);
+}
// Stream converter component definition
let service = xpcom.Service({
diff --git a/devtools/client/jsonview/css/general.css b/devtools/client/jsonview/css/general.css
index 0c68d65e7..d80720f4f 100644
--- a/devtools/client/jsonview/css/general.css
+++ b/devtools/client/jsonview/css/general.css
@@ -28,9 +28,9 @@ pre {
font-family: var(--monospace-font-family);
}
-#json,
-#headers {
+#json {
display: none;
+ white-space: pre-wrap;
}
/******************************************************************************/
diff --git a/devtools/client/jsonview/css/main.css b/devtools/client/jsonview/css/main.css
index 04f3cb87c..c75d7cea0 100644
--- a/devtools/client/jsonview/css/main.css
+++ b/devtools/client/jsonview/css/main.css
@@ -3,7 +3,9 @@
* 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";
diff --git a/devtools/client/jsonview/json-viewer.js b/devtools/client/jsonview/json-viewer.js
index d96081da2..38cb6d7ec 100644
--- a/devtools/client/jsonview/json-viewer.js
+++ b/devtools/client/jsonview/json-viewer.js
@@ -12,28 +12,28 @@ 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;
-
- try {
- jsonData = JSON.parse(json.textContent);
- } catch (err) {
- jsonData = err + "";
- }
// Application state object.
let input = {
jsonText: json.textContent,
jsonPretty: null,
- json: jsonData,
- headers: JSON.parse(headers.textContent),
+ headers: window.headers,
tabActive: 0,
prettified: false
};
+ // Remove BOM, if present.
+ if (input.jsonText.startsWith("\ufeff")) {
+ input.jsonText = input.jsonText.slice(1);
+ }
+
+ try {
+ input.json = JSON.parse(input.jsonText);
+ } catch (err) {
+ input.json = err;
+ }
+
json.remove();
- headers.remove();
/**
* Application actions/commands. This list implements all commands
@@ -61,7 +61,7 @@ define(function (require, exports, module) {
theApp.setState({jsonText: input.jsonText});
} else {
if (!input.jsonPretty) {
- input.jsonPretty = JSON.stringify(jsonData, null, " ");
+ input.jsonPretty = JSON.stringify(input.json, null, " ");
}
theApp.setState({jsonText: input.jsonPretty});
}
diff --git a/devtools/client/jsonview/utils.js b/devtools/client/jsonview/utils.js
index a70afdc68..6ab697c89 100644
--- a/devtools/client/jsonview/utils.js
+++ b/devtools/client/jsonview/utils.js
@@ -96,6 +96,8 @@ exports.exportIntoContentScope = function (win, obj, defineAs) {
Cu.exportFunction(propValue, clone, {
defineAs: propName
});
+ } else {
+ clone[propName] = Cu.cloneInto(propValue, win);
}
}
};