Skip to content

Commit d3a3ad9

Browse files
config endpoint must handle functions in module configs (#4106)
Fixes #4105 ```bash In JavaScript, standard JSON does not support functions. If you use JSON.stringify() on an object containing functions, those functions will be omitted (if they are object properties) or changed to null (if they are in an array). ``` --------- Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
1 parent 61870ae commit d3a3ad9

4 files changed

Lines changed: 84 additions & 6 deletions

File tree

js/main.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,20 @@ const MM = (function () {
475475
const loadConfig = async function () {
476476
try {
477477
const res = await fetch(new URL("config/", `${location.origin}${config.basePath}`));
478-
config = JSON.parse(await res.text());
478+
479+
// The server tags functions as { __mmFunction: "<source>" } because
480+
// JSON.stringify can't serialise live functions. This reviver turns
481+
// those tagged objects back into callable functions.
482+
config = JSON.parse(await res.text(), (key, value) => {
483+
if (value && typeof value === "object" && typeof value.__mmFunction === "string") {
484+
try {
485+
return new Function(`return (${value.__mmFunction})`)();
486+
} catch {
487+
Log.warn(`Failed to revive function for config key "${key}".`);
488+
}
489+
}
490+
return value;
491+
});
479492
} catch (error) {
480493
Log.error("Unable to retrieve config", error);
481494
}

js/server.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,21 @@ function Server (configObj) {
111111
const getStartup = (req, res) => res.send(startUp);
112112

113113
const getConfig = (req, res) => {
114-
if (config.hideConfigSecrets) {
115-
res.send(configObj.redactedConf);
116-
} else {
117-
res.send(configObj.fullConf);
118-
}
114+
const obj = config.hideConfigSecrets ? configObj.redactedConf : configObj.fullConf;
115+
// Functions can't survive JSON.stringify, so we wrap them in a
116+
// tagged object { __mmFunction: "<source>" }. The client-side
117+
// JSON reviver in main.js recognises this tag and reconstructs
118+
// the live function from the source string.
119+
const jsonString = JSON.stringify(obj, (key, value) => {
120+
if (typeof value === "function") {
121+
return { __mmFunction: value.toString() };
122+
}
123+
return value;
124+
});
125+
res.set("Content-Type", "application/json");
126+
res.send(jsonString);
119127
};
128+
120129
app.get("/config", (req, res) => getConfig(req, res));
121130

122131
app.get("/cors", async (req, res) => await cors(req, res));

tests/configs/config_functions.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*eslint object-shorthand: ["error", "always", { "methodsIgnorePattern": "^roundToInt2$" }]*/
2+
3+
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
4+
modules: [
5+
{
6+
module: "clock",
7+
position: "middle_center",
8+
config: {
9+
moduleFunctions: {
10+
roundToInt1: (value) => {
11+
try {
12+
return Math.round(parseFloat(value));
13+
} catch {
14+
return value;
15+
}
16+
},
17+
roundToInt2: function (value) {
18+
try {
19+
return Math.round(parseFloat(value));
20+
} catch {
21+
return value;
22+
}
23+
}
24+
},
25+
stringWithArrow: "a => b is not a function",
26+
stringWithFunction: "this function keyword is just text"
27+
}
28+
}
29+
]
30+
});
31+
32+
/*************** DO NOT EDIT THE LINE BELOW ***************/
33+
if (typeof module !== "undefined") {
34+
module.exports = config;
35+
}

tests/e2e/config_functions_spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const helpers = require("./helpers/global-setup");
2+
3+
describe("config with module function", () => {
4+
beforeAll(async () => {
5+
await helpers.startApplication("tests/configs/config_functions.js");
6+
});
7+
8+
afterAll(async () => {
9+
await helpers.stopApplication();
10+
});
11+
12+
it("config should resolve module functions", () => {
13+
expect(config.modules[0].config.moduleFunctions.roundToInt1(13.3)).toBe(13);
14+
expect(config.modules[0].config.moduleFunctions.roundToInt2(13.3)).toBe(13);
15+
});
16+
17+
it("config should not revive plain strings containing arrow or function keywords", () => {
18+
expect(config.modules[0].config.stringWithArrow).toBe("a => b is not a function");
19+
expect(config.modules[0].config.stringWithFunction).toBe("this function keyword is just text");
20+
});
21+
});

0 commit comments

Comments
 (0)