123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- /* -*- 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, "&")
- .replace(/\u00a0/g, " ")
- .replace(/"/g, """) +
- "\"";
- }, "<" + 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
- };
|