Skip to content

Commit c8fc506

Browse files
committed
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.
1 parent ae1b12c commit c8fc506

File tree

7 files changed

+266
-7
lines changed

7 files changed

+266
-7
lines changed

package-lock.json

Lines changed: 4 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
"process": "^0.11.10",
149149
"protobufjs": "^6.11.2",
150150
"qr-image": "^3.2.0",
151+
"qs": "^6.10.3",
151152
"scryptsy": "^2.1.0",
152153
"snackbarjs": "^1.1.0",
153154
"sortablejs": "^1.14.0",

src/core/config/Categories.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"From HTML Entity",
3838
"URL Encode",
3939
"URL Decode",
40+
"Query String Encode",
41+
"Query String Decode",
4042
"Escape Unicode Characters",
4143
"Unescape Unicode Characters",
4244
"Normalise Unicode",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @author Benjamin Altpeter [[email protected]]
3+
* @copyright Crown Copyright 2022
4+
* @license Apache-2.0
5+
*/
6+
7+
import qs from "qs";
8+
import Operation from "../Operation.mjs";
9+
10+
/**
11+
* Query String Decode operation
12+
*/
13+
class QueryStringDecode extends Operation {
14+
/**
15+
* QueryStringDecode constructor
16+
*/
17+
constructor() {
18+
super();
19+
20+
this.name = "Query String Decode";
21+
this.module = "URL";
22+
this.description =
23+
"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>";
24+
this.infoURL = "https://wikipedia.org/wiki/Query_string";
25+
this.inputType = "string";
26+
this.outputType = "JSON";
27+
this.args = [
28+
{
29+
name: "Depth",
30+
type: "number",
31+
value: 5,
32+
},
33+
{
34+
name: "Parameter limit",
35+
type: "number",
36+
value: 1000,
37+
},
38+
{
39+
name: "Delimiter",
40+
type: "string",
41+
value: "&",
42+
},
43+
{
44+
name: "Allow dot notation (<code>a.b=c</code>)?",
45+
type: "boolean",
46+
value: false,
47+
},
48+
{
49+
name: "Allow comma arrays (<code>a=b,c</code>)?",
50+
type: "boolean",
51+
value: false,
52+
},
53+
];
54+
}
55+
56+
/**
57+
* @param {string} input
58+
* @param {Object[]} args
59+
* @returns {JSON}
60+
*/
61+
run(input, args) {
62+
const [depth, parameterLimit, delimiter, allowDots, comma] = args;
63+
return qs.parse(input, {
64+
depth,
65+
delimiter,
66+
parameterLimit,
67+
allowDots,
68+
comma,
69+
ignoreQueryPrefix: true,
70+
});
71+
}
72+
}
73+
74+
export default QueryStringDecode;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @author Benjamin Altpeter [[email protected]]
3+
* @copyright Crown Copyright 2022
4+
* @license Apache-2.0
5+
*/
6+
7+
import qs from "qs";
8+
import Operation from "../Operation.mjs";
9+
10+
/**
11+
* Query String Encode operation
12+
*/
13+
class QueryStringEncode extends Operation {
14+
/**
15+
* QueryStringEncode constructor
16+
*/
17+
constructor() {
18+
super();
19+
20+
this.name = "Query String Encode";
21+
this.module = "URL";
22+
this.description =
23+
"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>";
24+
this.infoURL = "https://wikipedia.org/wiki/Query_string";
25+
this.inputType = "JSON";
26+
this.outputType = "string";
27+
this.args = [
28+
{
29+
name: "Array format",
30+
type: "option",
31+
value: ["brackets", "indices", "repeat", "comma"],
32+
defaultIndex: 0,
33+
},
34+
{
35+
name: "Object format",
36+
type: "option",
37+
value: ["brackets", "dots"],
38+
defaultIndex: 0,
39+
},
40+
{
41+
name: "Delimiter",
42+
type: "string",
43+
value: "&",
44+
},
45+
];
46+
}
47+
48+
/**
49+
* @param {JSON} input
50+
* @param {Object[]} args
51+
* @returns {string}
52+
*/
53+
run(input, args) {
54+
const [arrayFormat, objectFormat, delimiter] = args;
55+
return qs.stringify(input, {
56+
arrayFormat,
57+
delimiter,
58+
allowDots: objectFormat === "dots",
59+
encode: false,
60+
});
61+
}
62+
}
63+
64+
export default QueryStringEncode;

tests/operations/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import "./tests/CBORDecode.mjs";
107107
import "./tests/JA3Fingerprint.mjs";
108108
import "./tests/JA3SFingerprint.mjs";
109109
import "./tests/HASSH.mjs";
110+
import "./tests/QueryString.mjs";
110111

111112

112113
// Cannot test operations that use the File type yet
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Query String tests.
3+
*
4+
* @author Benjamin Altpeter [[email protected]]
5+
* @copyright Crown Copyright 2022
6+
* @license Apache-2.0
7+
*/
8+
9+
import TestRegister from "../../lib/TestRegister.mjs";
10+
11+
/**
12+
* Small helper for JSON.stringify() with the correct settings.
13+
* @param {any} obj An object to stringify
14+
* @returns A stringified version of the object, indented by four spaces.
15+
*/
16+
const json = (obj) => JSON.stringify(obj, null, 4);
17+
18+
TestRegister.addTests([
19+
{
20+
name: "Query String Decode simple example (defaults)",
21+
input: "?a=b&c=1&d=e;f=g",
22+
expectedOutput: json({
23+
a: "b",
24+
c: "1",
25+
d: "e;f=g",
26+
}),
27+
recipeConfig: [
28+
{ op: "Query String Decode", args: [5, 1000, "&", false, false] },
29+
],
30+
},
31+
{
32+
name: "Query String Decode arrays and objects (defaults)",
33+
input: "a[]=b&a[2]=b&c[d]=e&f=g,h&i.j=k",
34+
expectedOutput: json({
35+
a: ["b", "b"],
36+
c: {
37+
d: "e",
38+
},
39+
f: "g,h",
40+
"i.j": "k",
41+
}),
42+
recipeConfig: [
43+
{ op: "Query String Decode", args: [5, 1000, "&", false, false] },
44+
],
45+
},
46+
{
47+
name: "Query String Decode arrays and objects (extended)",
48+
input: "a[]=b&a[2]=b&c[d]=e&f=g,h&i.j=k",
49+
expectedOutput: json({
50+
a: ["b", "b"],
51+
c: {
52+
d: "e",
53+
},
54+
f: ["g", "h"],
55+
i: {
56+
j: "k",
57+
},
58+
}),
59+
recipeConfig: [
60+
{ op: "Query String Decode", args: [5, 1000, "&", true, true] },
61+
],
62+
},
63+
{
64+
name: "Query String Decode delimiter",
65+
input: "a=b;c=d",
66+
expectedOutput: json({
67+
a: "b",
68+
c: "d",
69+
}),
70+
recipeConfig: [
71+
{ op: "Query String Decode", args: [5, 1000, ";", false, false] },
72+
],
73+
},
74+
{
75+
name: "Query String Decode depth (default)",
76+
input: "a[b][c][d][e][f][g][h]=5",
77+
expectedOutput: json({
78+
a: {
79+
b: {
80+
c: {
81+
d: {
82+
e: {
83+
f: {
84+
"[g][h]": "5",
85+
},
86+
},
87+
},
88+
},
89+
},
90+
},
91+
}),
92+
recipeConfig: [
93+
{ op: "Query String Decode", args: [5, 1000, "&", false, false] },
94+
],
95+
},
96+
{
97+
name: "Query String Decode depth (higher)",
98+
input: "a[b][c][d][e][f][g][h]=5",
99+
expectedOutput: json({
100+
a: {
101+
b: {
102+
c: {
103+
d: {
104+
e: {
105+
f: {
106+
g: {
107+
h: "5",
108+
},
109+
},
110+
},
111+
},
112+
},
113+
},
114+
},
115+
}),
116+
recipeConfig: [
117+
{ op: "Query String Decode", args: [7, 1000, "&", false, false] },
118+
],
119+
},
120+
]);

0 commit comments

Comments
 (0)