mirror of
https://github.com/gchq/CyberChef.git
synced 2024-11-16 08:58:30 +01:00
'JSON Beautify' operation now supports formatting, collapsing and syntax highlighting. Closes #203.
This commit is contained in:
parent
4274e8f3a2
commit
5349115b94
6 changed files with 304 additions and 41 deletions
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -45,6 +45,7 @@
|
||||||
"js-crc": "^0.2.0",
|
"js-crc": "^0.2.0",
|
||||||
"js-sha3": "^0.8.0",
|
"js-sha3": "^0.8.0",
|
||||||
"jsesc": "^3.0.2",
|
"jsesc": "^3.0.2",
|
||||||
|
"json5": "^2.2.1",
|
||||||
"jsonpath": "^1.1.1",
|
"jsonpath": "^1.1.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
|
@ -9489,7 +9490,6 @@
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
},
|
},
|
||||||
|
@ -23014,8 +23014,7 @@
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"jsonpath": {
|
"jsonpath": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
|
|
|
@ -122,6 +122,7 @@
|
||||||
"js-crc": "^0.2.0",
|
"js-crc": "^0.2.0",
|
||||||
"js-sha3": "^0.8.0",
|
"js-sha3": "^0.8.0",
|
||||||
"jsesc": "^3.0.2",
|
"jsesc": "^3.0.2",
|
||||||
|
"json5": "^2.2.1",
|
||||||
"jsonpath": "^1.1.1",
|
"jsonpath": "^1.1.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
|
|
|
@ -5,8 +5,10 @@
|
||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import vkbeautify from "vkbeautify";
|
import JSON5 from "json5";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON Beautify operation
|
* JSON Beautify operation
|
||||||
|
@ -21,19 +23,25 @@ class JSONBeautify extends Operation {
|
||||||
|
|
||||||
this.name = "JSON Beautify";
|
this.name = "JSON Beautify";
|
||||||
this.module = "Code";
|
this.module = "Code";
|
||||||
this.description = "Indents and prettifies JavaScript Object Notation (JSON) code.";
|
this.description = "Indents and pretty prints JavaScript Object Notation (JSON) code.<br><br>Tags: json viewer, prettify, syntax highlighting";
|
||||||
this.inputType = "string";
|
this.inputType = "string";
|
||||||
this.outputType = "string";
|
this.outputType = "string";
|
||||||
|
this.presentType = "html";
|
||||||
this.args = [
|
this.args = [
|
||||||
{
|
{
|
||||||
"name": "Indent string",
|
name: "Indent string",
|
||||||
"type": "binaryShortString",
|
type: "binaryShortString",
|
||||||
"value": " "
|
value: " "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Sort Object Keys",
|
name: "Sort Object Keys",
|
||||||
"type": "boolean",
|
type: "boolean",
|
||||||
"value": false
|
value: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Formatted",
|
||||||
|
type: "boolean",
|
||||||
|
value: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -44,35 +52,193 @@ class JSONBeautify extends Operation {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const [indentStr, sortBool] = args;
|
|
||||||
|
|
||||||
if (!input) return "";
|
if (!input) return "";
|
||||||
if (sortBool) {
|
|
||||||
input = JSON.stringify(JSONBeautify._sort(JSON.parse(input)));
|
const [indentStr, sortBool] = args;
|
||||||
|
let json = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
json = JSON5.parse(input);
|
||||||
|
} catch (err) {
|
||||||
|
throw new OperationError("Unable to parse input as JSON.\n" + err);
|
||||||
}
|
}
|
||||||
return vkbeautify.json(input, indentStr);
|
|
||||||
|
if (sortBool) json = sortKeys(json);
|
||||||
|
|
||||||
|
return JSON.stringify(json, null, indentStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort JSON representation of an object
|
* Adds various dynamic features to the JSON blob
|
||||||
*
|
*
|
||||||
* @author Phillip Nordwall [phillip.nordwall@gmail.com]
|
* @param {string} data
|
||||||
* @private
|
* @param {Object[]} args
|
||||||
* @param {object} o
|
* @returns {html}
|
||||||
* @returns {object}
|
|
||||||
*/
|
*/
|
||||||
static _sort(o) {
|
present(data, args) {
|
||||||
if (Array.isArray(o)) {
|
const formatted = args[2];
|
||||||
return o.map(JSONBeautify._sort);
|
if (!formatted) return Utils.escapeHtml(data);
|
||||||
} else if ("[object Object]" === Object.prototype.toString.call(o)) {
|
|
||||||
return Object.keys(o).sort().reduce(function(a, k) {
|
const json = JSON5.parse(data);
|
||||||
a[k] = JSONBeautify._sort(o[k]);
|
const options = {
|
||||||
return a;
|
withLinks: true,
|
||||||
}, {});
|
bigNumbers: true
|
||||||
|
};
|
||||||
|
let html = '<div class="json-document">';
|
||||||
|
|
||||||
|
if (isCollapsable(json)) {
|
||||||
|
const isArr = json instanceof Array;
|
||||||
|
html += '<details open class="json-details">' +
|
||||||
|
`<summary class="json-summary ${isArr ? "json-arr" : "json-obj"}"></summary>` +
|
||||||
|
json2html(json, options) +
|
||||||
|
"</details>";
|
||||||
|
} else {
|
||||||
|
html += json2html(json, options);
|
||||||
}
|
}
|
||||||
return o;
|
|
||||||
|
html += "</div>";
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort keys in a JSON object
|
||||||
|
*
|
||||||
|
* @author Phillip Nordwall [phillip.nordwall@gmail.com]
|
||||||
|
* @param {object} o
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function sortKeys(o) {
|
||||||
|
if (Array.isArray(o)) {
|
||||||
|
return o.map(sortKeys);
|
||||||
|
} else if ("[object Object]" === Object.prototype.toString.call(o)) {
|
||||||
|
return Object.keys(o).sort().reduce(function(a, k) {
|
||||||
|
a[k] = sortKeys(o[k]);
|
||||||
|
return a;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isCollapsable(arg) {
|
||||||
|
return arg instanceof Object && Object.keys(arg).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string looks like a URL, based on protocol
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isUrl(string) {
|
||||||
|
const protocols = ["http", "https", "ftp", "ftps"];
|
||||||
|
for (let i = 0; i < protocols.length; i++) {
|
||||||
|
if (string.startsWith(protocols[i] + "://")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a json object into html representation
|
||||||
|
*
|
||||||
|
* Adapted for CyberChef by @n1474335 from jQuery json-viewer
|
||||||
|
* @author Alexandre Bodelot <alexandre.bodelot@gmail.com>
|
||||||
|
* @link https://github.com/abodelot/jquery.json-viewer
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function json2html(json, options) {
|
||||||
|
let html = "";
|
||||||
|
if (typeof json === "string") {
|
||||||
|
// Escape tags and quotes
|
||||||
|
json = Utils.escapeHtml(json);
|
||||||
|
|
||||||
|
if (options.withLinks && isUrl(json)) {
|
||||||
|
html += `<a href="${json}" class="json-string" target="_blank">${json}</a>`;
|
||||||
|
} else {
|
||||||
|
// Escape double quotes in the rendered non-URL string.
|
||||||
|
json = json.replace(/"/g, "\\"");
|
||||||
|
html += `<span class="json-string">"${json}"</span>`;
|
||||||
|
}
|
||||||
|
} else if (typeof json === "number" || typeof json === "bigint") {
|
||||||
|
html += `<span class="json-literal">${json}</span>`;
|
||||||
|
} else if (typeof json === "boolean") {
|
||||||
|
html += `<span class="json-literal">${json}</span>`;
|
||||||
|
} else if (json === null) {
|
||||||
|
html += '<span class="json-literal">null</span>';
|
||||||
|
} else if (json instanceof Array) {
|
||||||
|
if (json.length > 0) {
|
||||||
|
html += '<span class="json-bracket">[</span><ol class="json-array">';
|
||||||
|
for (let i = 0; i < json.length; i++) {
|
||||||
|
html += "<li>";
|
||||||
|
|
||||||
|
// Add toggle button if item is collapsable
|
||||||
|
if (isCollapsable(json[i])) {
|
||||||
|
const isArr = json[i] instanceof Array;
|
||||||
|
html += '<details open class="json-details">' +
|
||||||
|
`<summary class="json-summary ${isArr ? "json-arr" : "json-obj"}"></summary>` +
|
||||||
|
json2html(json[i], options) +
|
||||||
|
"</details>";
|
||||||
|
} else {
|
||||||
|
html += json2html(json[i], options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add comma if item is not last
|
||||||
|
if (i < json.length - 1) {
|
||||||
|
html += '<span class="json-comma">,</span>';
|
||||||
|
}
|
||||||
|
html += "</li>";
|
||||||
|
}
|
||||||
|
html += '</ol><span class="json-bracket">]</span>';
|
||||||
|
} else {
|
||||||
|
html += '<span class="json-bracket">[]</span>';
|
||||||
|
}
|
||||||
|
} else if (typeof json === "object") {
|
||||||
|
// Optional support different libraries for big numbers
|
||||||
|
// json.isLosslessNumber: package lossless-json
|
||||||
|
// json.toExponential(): packages bignumber.js, big.js, decimal.js, decimal.js-light, others?
|
||||||
|
if (options.bigNumbers && (typeof json.toExponential === "function" || json.isLosslessNumber)) {
|
||||||
|
html += `<span class="json-literal">${json.toString()}</span>`;
|
||||||
|
} else {
|
||||||
|
let keyCount = Object.keys(json).length;
|
||||||
|
if (keyCount > 0) {
|
||||||
|
html += '<span class="json-brace">{</span><ul class="json-dict">';
|
||||||
|
for (const key in json) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
||||||
|
const safeKey = Utils.escapeHtml(key);
|
||||||
|
html += "<li>";
|
||||||
|
|
||||||
|
// Add toggle button if item is collapsable
|
||||||
|
if (isCollapsable(json[key])) {
|
||||||
|
const isArr = json[key] instanceof Array;
|
||||||
|
html += '<details open class="json-details">' +
|
||||||
|
`<summary class="json-summary ${isArr ? "json-arr" : "json-obj"}">${safeKey}<span class="json-colon">:</span> </summary>` +
|
||||||
|
json2html(json[key], options) +
|
||||||
|
"</details>";
|
||||||
|
} else {
|
||||||
|
html += safeKey + '<span class="json-colon">:</span> ' + json2html(json[key], options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add comma if item is not last
|
||||||
|
if (--keyCount > 0) {
|
||||||
|
html += '<span class="json-comma">,</span>';
|
||||||
|
}
|
||||||
|
html += "</li>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += '</ul><span class="json-brace">}</span>';
|
||||||
|
} else {
|
||||||
|
html += '<span class="json-brace">{}</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
export default JSONBeautify;
|
export default JSONBeautify;
|
||||||
|
|
|
@ -34,3 +34,6 @@
|
||||||
@import "./layout/_operations.css";
|
@import "./layout/_operations.css";
|
||||||
@import "./layout/_recipe.css";
|
@import "./layout/_recipe.css";
|
||||||
@import "./layout/_structure.css";
|
@import "./layout/_structure.css";
|
||||||
|
|
||||||
|
/* Operations */
|
||||||
|
@import "./operations/json.css";
|
||||||
|
|
78
src/web/stylesheets/operations/json.css
Normal file
78
src/web/stylesheets/operations/json.css
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* JSON styles
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2022
|
||||||
|
* @license Apache-2.0
|
||||||
|
*
|
||||||
|
* Adapted for CyberChef by @n1474335 from jQuery json-viewer
|
||||||
|
* @author Alexandre Bodelot <alexandre.bodelot@gmail.com>
|
||||||
|
* @link https://github.com/abodelot/jquery.json-viewer
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Root element */
|
||||||
|
.json-document {
|
||||||
|
padding: .5em 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Syntax highlighting for JSON objects */
|
||||||
|
ul.json-dict, ol.json-array {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0 0 0 1px;
|
||||||
|
border-left: 1px dotted #ccc;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
.json-string {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
.json-literal {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.json-brace,
|
||||||
|
.json-bracket,
|
||||||
|
.json-colon,
|
||||||
|
.json-comma {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapse */
|
||||||
|
.json-details {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.json-details[open] {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.json-summary {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display object and array brackets when closed */
|
||||||
|
.json-summary.json-obj::after {
|
||||||
|
color: gray;
|
||||||
|
content: "{ ... }"
|
||||||
|
}
|
||||||
|
.json-summary.json-arr::after {
|
||||||
|
color: gray;
|
||||||
|
content: "[ ... ]"
|
||||||
|
}
|
||||||
|
.json-details[open] > .json-summary.json-obj::after,
|
||||||
|
.json-details[open] > .json-summary.json-arr::after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show arrows, even in inline mode */
|
||||||
|
.json-summary::before {
|
||||||
|
content: "\25BC";
|
||||||
|
color: #c0c0c0;
|
||||||
|
margin-left: -12px;
|
||||||
|
margin-right: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
.json-summary:hover::before {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
.json-details[open] > .json-summary::before {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: [" ", false],
|
args: [" ", false, false],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -27,7 +27,7 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: [" ", false],
|
args: [" ", false, false],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -38,8 +38,12 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: [" ", false],
|
args: [" ", false, false],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
op: "HTML To Text",
|
||||||
|
args: []
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -49,7 +53,7 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: [" ", false],
|
args: [" ", false, false],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -60,7 +64,7 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: [" ", false],
|
args: [" ", false, false],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -71,7 +75,7 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: [" ", false],
|
args: [" ", false, false],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -82,7 +86,7 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: ["\t", false],
|
args: ["\t", false, false],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -93,8 +97,12 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: [" ", false],
|
args: [" ", false, false],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
op: "HTML To Text",
|
||||||
|
args: []
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -104,8 +112,12 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: ["\t", false],
|
args: ["\t", false, false],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
op: "HTML To Text",
|
||||||
|
args: []
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -115,8 +127,12 @@ TestRegister.addTests([
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JSON Beautify",
|
op: "JSON Beautify",
|
||||||
args: ["\t", true],
|
args: ["\t", true, false],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
op: "HTML To Text",
|
||||||
|
args: []
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
Loading…
Reference in a new issue