From cd8b2ee53abca2c6b64a7efc7bcb89c77469189a Mon Sep 17 00:00:00 2001 From: Benjamin Altpeter Date: Sun, 13 Feb 2022 21:34:39 +0100 Subject: [PATCH] Add decode and encode operations for URL query strings These allow converting between URL query strings and JSON. They are based on the qs NPM package. --- package-lock.json | 60 +++++++++-- package.json | 1 + src/core/config/Categories.json | 2 + src/core/operations/QueryStringDecode.mjs | 74 +++++++++++++ src/core/operations/QueryStringEncode.mjs | 64 ++++++++++++ tests/operations/index.mjs | 1 + tests/operations/tests/QueryString.mjs | 120 ++++++++++++++++++++++ 7 files changed, 314 insertions(+), 8 deletions(-) create mode 100644 src/core/operations/QueryStringDecode.mjs create mode 100644 src/core/operations/QueryStringEncode.mjs create mode 100644 tests/operations/tests/QueryString.mjs diff --git a/package-lock.json b/package-lock.json index 8b5641c9e1..1fd748744d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "process": "^0.11.10", "protobufjs": "^6.11.3", "qr-image": "^3.2.0", + "qs": "^6.10.3", "reflect-metadata": "^0.1.13", "scryptsy": "^2.1.0", "snackbarjs": "^1.1.0", @@ -10587,6 +10588,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-is": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", @@ -11794,12 +11803,17 @@ "integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=" }, "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true, + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/querystring": { @@ -12540,6 +12554,19 @@ "node": ">=0.8.0" } }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -22380,6 +22407,11 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, "object-is": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", @@ -23295,10 +23327,12 @@ "integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=" }, "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } }, "querystring": { "version": "0.2.0", @@ -23889,6 +23923,16 @@ "integrity": "sha512-C2FisSSW8S6TIYHHiMHN0NqzdjWfTekdMpA2FJTbRWnQMLO1RRIXEB9eVZYOlofYmjZA7fY3ChoFu09MeI3wlQ==", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", diff --git a/package.json b/package.json index fe93fe52b1..7ed1278c0e 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "process": "^0.11.10", "protobufjs": "^6.11.3", "qr-image": "^3.2.0", + "qs": "^6.10.3", "reflect-metadata": "^0.1.13", "scryptsy": "^2.1.0", "snackbarjs": "^1.1.0", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 075e8d6662..71bac0157d 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -39,6 +39,8 @@ "From HTML Entity", "URL Encode", "URL Decode", + "Query String Encode", + "Query String Decode", "Escape Unicode Characters", "Unescape Unicode Characters", "Normalise Unicode", diff --git a/src/core/operations/QueryStringDecode.mjs b/src/core/operations/QueryStringDecode.mjs new file mode 100644 index 0000000000..97fbbe7b20 --- /dev/null +++ b/src/core/operations/QueryStringDecode.mjs @@ -0,0 +1,74 @@ +/** + * @author Benjamin Altpeter [hi@bn.al] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import qs from "qs"; +import Operation from "../Operation.mjs"; + +/** + * Query String Decode operation + */ +class QueryStringDecode extends Operation { + /** + * QueryStringDecode constructor + */ + constructor() { + super(); + + this.name = "Query String Decode"; + this.module = "URL"; + this.description = + "Converts URL query strings into a JSON representation.

e.g. a=b&c=1 becomes {"a": "b", "c": "1"}"; + this.infoURL = "https://wikipedia.org/wiki/Query_string"; + this.inputType = "string"; + this.outputType = "JSON"; + this.args = [ + { + name: "Depth", + type: "number", + value: 5, + }, + { + name: "Parameter limit", + type: "number", + value: 1000, + }, + { + name: "Delimiter", + type: "string", + value: "&", + }, + { + name: "Allow dot notation (a.b=c)?", + type: "boolean", + value: false, + }, + { + name: "Allow comma arrays (a=b,c)?", + type: "boolean", + value: false, + }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {JSON} + */ + run(input, args) { + const [depth, parameterLimit, delimiter, allowDots, comma] = args; + return qs.parse(input, { + depth, + delimiter, + parameterLimit, + allowDots, + comma, + ignoreQueryPrefix: true, + }); + } +} + +export default QueryStringDecode; diff --git a/src/core/operations/QueryStringEncode.mjs b/src/core/operations/QueryStringEncode.mjs new file mode 100644 index 0000000000..27c841e407 --- /dev/null +++ b/src/core/operations/QueryStringEncode.mjs @@ -0,0 +1,64 @@ +/** + * @author Benjamin Altpeter [hi@bn.al] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import qs from "qs"; +import Operation from "../Operation.mjs"; + +/** + * Query String Encode operation + */ +class QueryStringEncode extends Operation { + /** + * QueryStringEncode constructor + */ + constructor() { + super(); + + this.name = "Query String Encode"; + this.module = "URL"; + this.description = + "Converts JSON objects into a URL query string representation.

e.g. {"a": "b", "c": 1} becomes a=b&c=1"; + this.infoURL = "https://wikipedia.org/wiki/Query_string"; + this.inputType = "JSON"; + this.outputType = "string"; + this.args = [ + { + name: "Array format", + type: "option", + value: ["brackets", "indices", "repeat", "comma"], + defaultIndex: 0, + }, + { + name: "Object format", + type: "option", + value: ["brackets", "dots"], + defaultIndex: 0, + }, + { + name: "Delimiter", + type: "string", + value: "&", + }, + ]; + } + + /** + * @param {JSON} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [arrayFormat, objectFormat, delimiter] = args; + return qs.stringify(input, { + arrayFormat, + delimiter, + allowDots: objectFormat === "dots", + encode: false, + }); + } +} + +export default QueryStringEncode; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 7a3361f24c..b77f7fcac0 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -130,6 +130,7 @@ import "./tests/FletcherChecksum.mjs"; import "./tests/CMAC.mjs"; import "./tests/AESKeyWrap.mjs"; import "./tests/Rabbit.mjs"; +import "./tests/QueryString.mjs"; // Cannot test operations that use the File type yet // import "./tests/SplitColourChannels.mjs"; diff --git a/tests/operations/tests/QueryString.mjs b/tests/operations/tests/QueryString.mjs new file mode 100644 index 0000000000..74e21f9041 --- /dev/null +++ b/tests/operations/tests/QueryString.mjs @@ -0,0 +1,120 @@ +/** + * Query String tests. + * + * @author Benjamin Altpeter [hi@bn.al] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +/** + * Small helper for JSON.stringify() with the correct settings. + * @param {any} obj An object to stringify + * @returns A stringified version of the object, indented by four spaces. + */ +const json = (obj) => JSON.stringify(obj, null, 4); + +TestRegister.addTests([ + { + name: "Query String Decode simple example (defaults)", + input: "?a=b&c=1&d=e;f=g", + expectedOutput: json({ + a: "b", + c: "1", + d: "e;f=g", + }), + recipeConfig: [ + { op: "Query String Decode", args: [5, 1000, "&", false, false] }, + ], + }, + { + name: "Query String Decode arrays and objects (defaults)", + input: "a[]=b&a[2]=b&c[d]=e&f=g,h&i.j=k", + expectedOutput: json({ + a: ["b", "b"], + c: { + d: "e", + }, + f: "g,h", + "i.j": "k", + }), + recipeConfig: [ + { op: "Query String Decode", args: [5, 1000, "&", false, false] }, + ], + }, + { + name: "Query String Decode arrays and objects (extended)", + input: "a[]=b&a[2]=b&c[d]=e&f=g,h&i.j=k", + expectedOutput: json({ + a: ["b", "b"], + c: { + d: "e", + }, + f: ["g", "h"], + i: { + j: "k", + }, + }), + recipeConfig: [ + { op: "Query String Decode", args: [5, 1000, "&", true, true] }, + ], + }, + { + name: "Query String Decode delimiter", + input: "a=b;c=d", + expectedOutput: json({ + a: "b", + c: "d", + }), + recipeConfig: [ + { op: "Query String Decode", args: [5, 1000, ";", false, false] }, + ], + }, + { + name: "Query String Decode depth (default)", + input: "a[b][c][d][e][f][g][h]=5", + expectedOutput: json({ + a: { + b: { + c: { + d: { + e: { + f: { + "[g][h]": "5", + }, + }, + }, + }, + }, + }, + }), + recipeConfig: [ + { op: "Query String Decode", args: [5, 1000, "&", false, false] }, + ], + }, + { + name: "Query String Decode depth (higher)", + input: "a[b][c][d][e][f][g][h]=5", + expectedOutput: json({ + a: { + b: { + c: { + d: { + e: { + f: { + g: { + h: "5", + }, + }, + }, + }, + }, + }, + }, + }), + recipeConfig: [ + { op: "Query String Decode", args: [7, 1000, "&", false, false] }, + ], + }, +]);