converter-child.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const {Cc, Ci, components} = require("chrome");
  7. const Services = require("Services");
  8. const {Class} = require("sdk/core/heritage");
  9. const {Unknown} = require("sdk/platform/xpcom");
  10. const xpcom = require("sdk/platform/xpcom");
  11. const Events = require("sdk/dom/events");
  12. const Clipboard = require("sdk/clipboard");
  13. loader.lazyRequireGetter(this, "NetworkHelper",
  14. "devtools/shared/webconsole/network-helper");
  15. loader.lazyRequireGetter(this, "JsonViewUtils",
  16. "devtools/client/jsonview/utils");
  17. const childProcessMessageManager =
  18. Cc["@mozilla.org/childprocessmessagemanager;1"]
  19. .getService(Ci.nsISyncMessageSender);
  20. const JSON_VIEW_MIME_TYPE = "application/vnd.mozilla.json.view";
  21. const CONTRACT_ID = "@mozilla.org/streamconv;1?from=" +
  22. JSON_VIEW_MIME_TYPE + "&to=*/*";
  23. const CLASS_ID = "{d8c9acee-dec5-11e4-8c75-1681e6b88ec1}";
  24. // Localization
  25. let jsonViewStrings = Services.strings.createBundle(
  26. "chrome://devtools/locale/jsonview.properties");
  27. /**
  28. * This object detects 'application/vnd.mozilla.json.view' content type
  29. * and converts it into a JSON Viewer application that allows simple
  30. * JSON inspection.
  31. *
  32. * Inspired by JSON View: https://github.com/bhollis/jsonview/
  33. */
  34. let Converter = Class({
  35. extends: Unknown,
  36. interfaces: [
  37. "nsIStreamConverter",
  38. "nsIStreamListener",
  39. "nsIRequestObserver"
  40. ],
  41. get wrappedJSObject() {
  42. return this;
  43. },
  44. /**
  45. * This component works as such:
  46. * 1. asyncConvertData captures the listener
  47. * 2. onStartRequest fires, initializes stuff, modifies the listener
  48. * to match our output type
  49. * 3. onDataAvailable spits it back to the listener
  50. * 4. onStopRequest spits it back to the listener
  51. * 5. convert does nothing, it's just the synchronous version
  52. * of asyncConvertData
  53. */
  54. convert: function (fromStream, fromType, toType, ctx) {
  55. return fromStream;
  56. },
  57. asyncConvertData: function (fromType, toType, listener, ctx) {
  58. this.listener = listener;
  59. },
  60. onDataAvailable: function (request, context, inputStream, offset, count) {
  61. this.listener.onDataAvailable(...arguments);
  62. },
  63. onStartRequest: function (request, context) {
  64. // Set the content type to HTML in order to parse the doctype, styles
  65. // and scripts, but later a <plaintext> element will switch the tokenizer
  66. // to the plaintext state in order to parse the JSON.
  67. request.QueryInterface(Ci.nsIChannel);
  68. request.contentType = "text/html";
  69. // JSON enforces UTF-8 charset (see bug 741776).
  70. request.contentCharset = "UTF-8";
  71. // Changing the content type breaks saving functionality. Fix it.
  72. fixSave(request);
  73. // Because content might still have a reference to this window,
  74. // force setting it to a null principal to avoid it being same-
  75. // origin with (other) content.
  76. request.loadInfo.resetPrincipalsToNullPrincipal();
  77. // Start the request.
  78. this.listener.onStartRequest(request, context);
  79. // Initialize stuff.
  80. let win = NetworkHelper.getWindowForRequest(request);
  81. exportData(win, request);
  82. win.addEventListener("DOMContentLoaded", event => {
  83. win.addEventListener("contentMessage", onContentMessage, false, true);
  84. }, {once: true});
  85. // Insert the initial HTML code.
  86. let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
  87. .createInstance(Ci.nsIScriptableUnicodeConverter);
  88. converter.charset = "UTF-8";
  89. let stream = converter.convertToInputStream(initialHTML(win.document));
  90. this.listener.onDataAvailable(request, context, stream, 0, stream.available());
  91. },
  92. onStopRequest: function (request, context, statusCode) {
  93. this.listener.onStopRequest(request, context, statusCode);
  94. this.listener = null;
  95. }
  96. });
  97. // Lets "save as" save the original JSON, not the viewer.
  98. // To save with the proper extension we need the original content type,
  99. // which has been replaced by application/vnd.mozilla.json.view
  100. function fixSave(request) {
  101. let originalType;
  102. if (request instanceof Ci.nsIHttpChannel) {
  103. try {
  104. let header = request.getResponseHeader("Content-Type");
  105. originalType = header.split(";")[0];
  106. } catch (err) {
  107. // Handled below
  108. }
  109. } else {
  110. let uri = request.QueryInterface(Ci.nsIChannel).URI.spec;
  111. let match = uri.match(/^data:(.*?)[,;]/);
  112. if (match) {
  113. originalType = match[1];
  114. }
  115. }
  116. const JSON_TYPES = ["application/json", "application/manifest+json"];
  117. if (!JSON_TYPES.includes(originalType)) {
  118. originalType = JSON_TYPES[0];
  119. }
  120. request.QueryInterface(Ci.nsIWritablePropertyBag);
  121. request.setProperty("contentType", originalType);
  122. }
  123. // Exports variables that will be accessed by the non-privileged scripts.
  124. function exportData(win, request) {
  125. let Locale = {
  126. $STR: key => {
  127. try {
  128. return jsonViewStrings.GetStringFromName(key);
  129. } catch (err) {
  130. console.error(err);
  131. return undefined;
  132. }
  133. }
  134. };
  135. JsonViewUtils.exportIntoContentScope(win, Locale, "Locale");
  136. let headers = {
  137. response: [],
  138. request: []
  139. };
  140. // The request doesn't have to be always nsIHttpChannel
  141. // (e.g. in case of data: URLs)
  142. if (request instanceof Ci.nsIHttpChannel) {
  143. request.visitResponseHeaders({
  144. visitHeader: function (name, value) {
  145. headers.response.push({name: name, value: value});
  146. }
  147. });
  148. request.visitRequestHeaders({
  149. visitHeader: function (name, value) {
  150. headers.request.push({name: name, value: value});
  151. }
  152. });
  153. }
  154. JsonViewUtils.exportIntoContentScope(win, headers, "headers");
  155. }
  156. // Serializes a qualifiedName and an optional set of attributes into an HTML
  157. // start tag. Be aware qualifiedName and attribute names are not validated.
  158. // Attribute values are escaped with escapingString algorithm in attribute mode
  159. // (https://html.spec.whatwg.org/multipage/syntax.html#escapingString).
  160. function startTag(qualifiedName, attributes = {}) {
  161. return Object.entries(attributes).reduce(function (prev, [attr, value]) {
  162. return prev + " " + attr + "=\"" +
  163. value.replace(/&/g, "&amp;")
  164. .replace(/\u00a0/g, "&nbsp;")
  165. .replace(/"/g, "&quot;") +
  166. "\"";
  167. }, "<" + qualifiedName) + ">";
  168. }
  169. // Builds an HTML string that will be used to load stylesheets and scripts,
  170. // and switch the parser to plaintext state.
  171. function initialHTML(doc) {
  172. let os;
  173. let platform = Services.appinfo.OS;
  174. if (platform.startsWith("WINNT")) {
  175. os = "win";
  176. } else if (platform.startsWith("Darwin")) {
  177. os = "mac";
  178. } else {
  179. os = "linux";
  180. }
  181. let base = doc.createElement("base");
  182. base.href = "resource://devtools/client/jsonview/";
  183. let style = doc.createElement("link");
  184. style.rel = "stylesheet";
  185. style.type = "text/css";
  186. style.href = "css/main.css";
  187. let script = doc.createElement("script");
  188. script.src = "lib/require.js";
  189. script.dataset.main = "viewer-config";
  190. script.defer = true;
  191. let head = doc.createElement("head");
  192. head.append(base, style, script);
  193. return "<!DOCTYPE html>\n" +
  194. startTag("html", {
  195. "platform": os,
  196. "class": "theme-" + JsonViewUtils.getCurrentTheme(),
  197. "dir": Services.locale.isAppLocaleRTL ? "rtl" : "ltr"
  198. }) +
  199. head.outerHTML +
  200. startTag("body") +
  201. startTag("div", {"id": "content"}) +
  202. startTag("plaintext", {"id": "json"});
  203. }
  204. // Chrome <-> Content communication
  205. function onContentMessage(e) {
  206. // Do not handle events from different documents.
  207. let win = this;
  208. if (win != e.target) {
  209. return;
  210. }
  211. let value = e.detail.value;
  212. switch (e.detail.type) {
  213. case "copy":
  214. copyString(win, value);
  215. break;
  216. case "copy-headers":
  217. copyHeaders(win, value);
  218. break;
  219. case "save":
  220. childProcessMessageManager.sendAsyncMessage(
  221. "devtools:jsonview:save", value);
  222. }
  223. }
  224. function copyHeaders(win, headers) {
  225. let value = "";
  226. let eol = (Services.appinfo.OS !== "WINNT") ? "\n" : "\r\n";
  227. let responseHeaders = headers.response;
  228. for (let i = 0; i < responseHeaders.length; i++) {
  229. let header = responseHeaders[i];
  230. value += header.name + ": " + header.value + eol;
  231. }
  232. value += eol;
  233. let requestHeaders = headers.request;
  234. for (let i = 0; i < requestHeaders.length; i++) {
  235. let header = requestHeaders[i];
  236. value += header.name + ": " + header.value + eol;
  237. }
  238. copyString(win, value);
  239. }
  240. function copyString(win, string) {
  241. win.document.addEventListener("copy", event => {
  242. event.clipboardData.setData("text/plain", string);
  243. event.preventDefault();
  244. }, {once: true});
  245. win.document.execCommand("copy", false, null);
  246. }
  247. // Stream converter component definition
  248. let service = xpcom.Service({
  249. id: components.ID(CLASS_ID),
  250. contract: CONTRACT_ID,
  251. Component: Converter,
  252. register: false,
  253. unregister: false
  254. });
  255. function register() {
  256. if (!xpcom.isRegistered(service)) {
  257. xpcom.register(service);
  258. return true;
  259. }
  260. return false;
  261. }
  262. function unregister() {
  263. if (xpcom.isRegistered(service)) {
  264. xpcom.unregister(service);
  265. return true;
  266. }
  267. return false;
  268. }
  269. exports.JsonViewService = {
  270. register: register,
  271. unregister: unregister
  272. };