From a849e50325bfb7cb68b79cefe9b5ff6fdc5c06aa Mon Sep 17 00:00:00 2001 From: Karsten Hassel Date: Sun, 12 Apr 2026 16:09:58 +0200 Subject: [PATCH 1/2] config endpoint must handle functions in module configs --- js/main.js | 14 ++++++++++++- js/server.js | 14 +++++++++++-- tests/configs/config_functions.js | 33 ++++++++++++++++++++++++++++++ tests/e2e/config_functions_spec.js | 16 +++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 tests/configs/config_functions.js create mode 100644 tests/e2e/config_functions_spec.js diff --git a/js/main.js b/js/main.js index 0e20c41e82..776c5d4e11 100644 --- a/js/main.js +++ b/js/main.js @@ -475,7 +475,19 @@ const MM = (function () { const loadConfig = async function () { try { const res = await fetch(new URL("config/", `${location.origin}${config.basePath}`)); - config = JSON.parse(await res.text()); + + config = JSON.parse(await res.text(), (key, value) => { + if (typeof value === "string") { + // checks for classic function OR arrow function + const isFunction = value.includes("function") || value.includes("=>"); + + if (isFunction) { + // eval() often needs brackets around + return eval(`(${value})`); + } + } + return value; + }); } catch (error) { Log.error("Unable to retrieve config", error); } diff --git a/js/server.js b/js/server.js index 081e90892c..0700089bc4 100644 --- a/js/server.js +++ b/js/server.js @@ -111,12 +111,22 @@ function Server (configObj) { const getStartup = (req, res) => res.send(startUp); const getConfig = (req, res) => { + let obj; if (config.hideConfigSecrets) { - res.send(configObj.redactedConf); + obj = configObj.redactedConf; } else { - res.send(configObj.fullConf); + obj = configObj.fullConf; } + const jsonString = JSON.stringify(obj, (key, value) => { + if (typeof value === "function") { + return value.toString(); + } + return value; + }); + res.set("Content-Type", "application/json"); + res.send(jsonString); }; + app.get("/config", (req, res) => getConfig(req, res)); app.get("/cors", async (req, res) => await cors(req, res)); diff --git a/tests/configs/config_functions.js b/tests/configs/config_functions.js new file mode 100644 index 0000000000..8e9a791eb6 --- /dev/null +++ b/tests/configs/config_functions.js @@ -0,0 +1,33 @@ +/*eslint object-shorthand: ["error", "always", { "methodsIgnorePattern": "^roundToInt2$" }]*/ + +let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({ + modules: [ + { + module: "clock", + position: "middle_center", + config: { + moduleFunctions: { + roundToInt1: (value) => { + try { + return Math.round(parseFloat(value)); + } catch { + return value; + } + }, + roundToInt2: function (value) { + try { + return Math.round(parseFloat(value)); + } catch { + return value; + } + } + } + } + } + ] +}); + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/e2e/config_functions_spec.js b/tests/e2e/config_functions_spec.js new file mode 100644 index 0000000000..d89f8f59ad --- /dev/null +++ b/tests/e2e/config_functions_spec.js @@ -0,0 +1,16 @@ +const helpers = require("./helpers/global-setup"); + +describe("config with module function", () => { + beforeAll(async () => { + await helpers.startApplication("tests/configs/config_functions.js"); + }); + + afterAll(async () => { + await helpers.stopApplication(); + }); + + it("config should resolve module functions", () => { + expect(config.modules[0].config.moduleFunctions.roundToInt1(13.3)).toBe(13); + expect(config.modules[0].config.moduleFunctions.roundToInt2(13.3)).toBe(13); + }); +}); From 242ecfedd99d7852163123a22c9af0d1b5994c63 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:36:39 +0200 Subject: [PATCH 2/2] fix(config): use tagged function serialization Replace heuristic string detection for function revival with an explicit __mmFunction tag to avoid false positives for plain text containing "function" or "=>". Add comments clarifying the server/client serialization contract and extend e2e coverage to ensure normal strings are not revived. --- js/main.js | 15 ++++++++------- js/server.js | 13 ++++++------- tests/configs/config_functions.js | 4 +++- tests/e2e/config_functions_spec.js | 5 +++++ 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/js/main.js b/js/main.js index 776c5d4e11..73d6075f3b 100644 --- a/js/main.js +++ b/js/main.js @@ -476,14 +476,15 @@ const MM = (function () { try { const res = await fetch(new URL("config/", `${location.origin}${config.basePath}`)); + // The server tags functions as { __mmFunction: "" } because + // JSON.stringify can't serialise live functions. This reviver turns + // those tagged objects back into callable functions. config = JSON.parse(await res.text(), (key, value) => { - if (typeof value === "string") { - // checks for classic function OR arrow function - const isFunction = value.includes("function") || value.includes("=>"); - - if (isFunction) { - // eval() often needs brackets around - return eval(`(${value})`); + if (value && typeof value === "object" && typeof value.__mmFunction === "string") { + try { + return new Function(`return (${value.__mmFunction})`)(); + } catch { + Log.warn(`Failed to revive function for config key "${key}".`); } } return value; diff --git a/js/server.js b/js/server.js index 0700089bc4..f8a51e714d 100644 --- a/js/server.js +++ b/js/server.js @@ -111,15 +111,14 @@ function Server (configObj) { const getStartup = (req, res) => res.send(startUp); const getConfig = (req, res) => { - let obj; - if (config.hideConfigSecrets) { - obj = configObj.redactedConf; - } else { - obj = configObj.fullConf; - } + const obj = config.hideConfigSecrets ? configObj.redactedConf : configObj.fullConf; + // Functions can't survive JSON.stringify, so we wrap them in a + // tagged object { __mmFunction: "" }. The client-side + // JSON reviver in main.js recognises this tag and reconstructs + // the live function from the source string. const jsonString = JSON.stringify(obj, (key, value) => { if (typeof value === "function") { - return value.toString(); + return { __mmFunction: value.toString() }; } return value; }); diff --git a/tests/configs/config_functions.js b/tests/configs/config_functions.js index 8e9a791eb6..27e265f14d 100644 --- a/tests/configs/config_functions.js +++ b/tests/configs/config_functions.js @@ -21,7 +21,9 @@ let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory( return value; } } - } + }, + stringWithArrow: "a => b is not a function", + stringWithFunction: "this function keyword is just text" } } ] diff --git a/tests/e2e/config_functions_spec.js b/tests/e2e/config_functions_spec.js index d89f8f59ad..b40361cdcb 100644 --- a/tests/e2e/config_functions_spec.js +++ b/tests/e2e/config_functions_spec.js @@ -13,4 +13,9 @@ describe("config with module function", () => { expect(config.modules[0].config.moduleFunctions.roundToInt1(13.3)).toBe(13); expect(config.modules[0].config.moduleFunctions.roundToInt2(13.3)).toBe(13); }); + + it("config should not revive plain strings containing arrow or function keywords", () => { + expect(config.modules[0].config.stringWithArrow).toBe("a => b is not a function"); + expect(config.modules[0].config.stringWithFunction).toBe("this function keyword is just text"); + }); });