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.
This commit is contained in:
Benjamin Altpeter 2022-02-13 21:34:39 +01:00
parent 2efd075803
commit cd8b2ee53a
No known key found for this signature in database
GPG key ID: 1775A9EFE112EDEF
7 changed files with 314 additions and 8 deletions

60
package-lock.json generated
View file

@ -76,6 +76,7 @@
"process": "^0.11.10", "process": "^0.11.10",
"protobufjs": "^6.11.3", "protobufjs": "^6.11.3",
"qr-image": "^3.2.0", "qr-image": "^3.2.0",
"qs": "^6.10.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"scryptsy": "^2.1.0", "scryptsy": "^2.1.0",
"snackbarjs": "^1.1.0", "snackbarjs": "^1.1.0",
@ -10587,6 +10588,14 @@
"node": ">=0.10.0" "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": { "node_modules/object-is": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
@ -11794,12 +11803,17 @@
"integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=" "integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug="
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.5.3", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dev": true, "dependencies": {
"side-channel": "^1.0.4"
},
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/querystring": { "node_modules/querystring": {
@ -12540,6 +12554,19 @@
"node": ">=0.8.0" "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": { "node_modules/signal-exit": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@ -22380,6 +22407,11 @@
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true "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": { "object-is": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
@ -23295,10 +23327,12 @@
"integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=" "integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug="
}, },
"qs": { "qs": {
"version": "6.5.3", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dev": true "requires": {
"side-channel": "^1.0.4"
}
}, },
"querystring": { "querystring": {
"version": "0.2.0", "version": "0.2.0",
@ -23889,6 +23923,16 @@
"integrity": "sha512-C2FisSSW8S6TIYHHiMHN0NqzdjWfTekdMpA2FJTbRWnQMLO1RRIXEB9eVZYOlofYmjZA7fY3ChoFu09MeI3wlQ==", "integrity": "sha512-C2FisSSW8S6TIYHHiMHN0NqzdjWfTekdMpA2FJTbRWnQMLO1RRIXEB9eVZYOlofYmjZA7fY3ChoFu09MeI3wlQ==",
"dev": true "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": { "signal-exit": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",

View file

@ -152,6 +152,7 @@
"process": "^0.11.10", "process": "^0.11.10",
"protobufjs": "^6.11.3", "protobufjs": "^6.11.3",
"qr-image": "^3.2.0", "qr-image": "^3.2.0",
"qs": "^6.10.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"scryptsy": "^2.1.0", "scryptsy": "^2.1.0",
"snackbarjs": "^1.1.0", "snackbarjs": "^1.1.0",

View file

@ -39,6 +39,8 @@
"From HTML Entity", "From HTML Entity",
"URL Encode", "URL Encode",
"URL Decode", "URL Decode",
"Query String Encode",
"Query String Decode",
"Escape Unicode Characters", "Escape Unicode Characters",
"Unescape Unicode Characters", "Unescape Unicode Characters",
"Normalise Unicode", "Normalise Unicode",

View file

@ -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.<br><br>e.g. <code>a=b&c=1</code> becomes <code>{&quot;a&quot;: &quot;b&quot;, &quot;c&quot;: &quot;1&quot;}</code>";
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 (<code>a.b=c</code>)?",
type: "boolean",
value: false,
},
{
name: "Allow comma arrays (<code>a=b,c</code>)?",
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;

View file

@ -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.<br><br>e.g. <code>{&quot;a&quot;: &quot;b&quot;, &quot;c&quot;: 1}</code> becomes <code>a=b&c=1</code>";
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;

View file

@ -130,6 +130,7 @@ import "./tests/FletcherChecksum.mjs";
import "./tests/CMAC.mjs"; import "./tests/CMAC.mjs";
import "./tests/AESKeyWrap.mjs"; import "./tests/AESKeyWrap.mjs";
import "./tests/Rabbit.mjs"; import "./tests/Rabbit.mjs";
import "./tests/QueryString.mjs";
// Cannot test operations that use the File type yet // Cannot test operations that use the File type yet
// import "./tests/SplitColourChannels.mjs"; // import "./tests/SplitColourChannels.mjs";

View file

@ -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] },
],
},
]);