summaryrefslogtreecommitdiffstats
path: root/devtools/client/jsonview/converter-child.js
diff options
context:
space:
mode:
authoryami <34216515+kn-yami@users.noreply.github.com>2019-07-30 16:09:52 +0200
committeryami <34216515+kn-yami@users.noreply.github.com>2019-07-30 16:09:52 +0200
commitd80d5688e6177d81513aa5b4d55cb479220c85b0 (patch)
treee3073fdb5c7ee459636591b69dab8720e2529d61 /devtools/client/jsonview/converter-child.js
parentf635feec77ad32c3e1251a91a145ebee185fee31 (diff)
downloadUXP-d80d5688e6177d81513aa5b4d55cb479220c85b0.tar
UXP-d80d5688e6177d81513aa5b4d55cb479220c85b0.tar.gz
UXP-d80d5688e6177d81513aa5b4d55cb479220c85b0.tar.lz
UXP-d80d5688e6177d81513aa5b4d55cb479220c85b0.tar.xz
UXP-d80d5688e6177d81513aa5b4d55cb479220c85b0.zip
Issue #1138 - Part 3: avoid quirks mode in JSON Viewer
Mozilla Bug 1368899
Diffstat (limited to 'devtools/client/jsonview/converter-child.js')
-rw-r--r--devtools/client/jsonview/converter-child.js262
1 files changed, 142 insertions, 120 deletions
diff --git a/devtools/client/jsonview/converter-child.js b/devtools/client/jsonview/converter-child.js
index cf93161af..1be342474 100644
--- a/devtools/client/jsonview/converter-child.js
+++ b/devtools/client/jsonview/converter-child.js
@@ -58,8 +58,7 @@ let Converter = Class({
* 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 and initializes
- * the JSON Viewer
+ * 4. onStopRequest spits it back to the listener
* 5. convert does nothing, it's just the synchronous version
* of asyncConvertData
*/
@@ -76,98 +75,168 @@ let Converter = Class({
},
onStartRequest: function (request, context) {
- this.channel = request;
+ // 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";
- // 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);
+ // JSON enforces UTF-8 charset (see bug 741776).
+ request.contentCharset = "UTF-8";
- // 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";
+ // 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);
- },
-
- onStopRequest: function (request, context, statusCode) {
- let Locale = {
- $STR: key => {
- try {
- return jsonViewStrings.GetStringFromName(key);
- } catch (err) {
- console.error(err);
- return undefined;
- }
- }
- };
-
- 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});
- }
- });
- }
+ // Initialize stuff.
let win = NetworkHelper.getWindowForRequest(request);
- JsonViewUtils.exportIntoContentScope(win, Locale, "Locale");
- JsonViewUtils.exportIntoContentScope(win, headers, "headers");
-
+ exportData(win, request);
win.addEventListener("DOMContentLoaded", event => {
- win.addEventListener("contentMessage",
- onContentMessage.bind(this), false, true);
- loadJsonViewer(win.document);
+ win.addEventListener("contentMessage", onContentMessage, false, true);
}, {once: true});
- this.listener.onStopRequest(this.channel, context, statusCode);
+ // 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 = NetworkHelper.getWindowForRequest(this.channel);
+ let win = this;
if (win != e.target) {
return;
}
@@ -191,53 +260,6 @@ function onContentMessage(e) {
}
}
-// 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";