From 1119b477d80798dc0f887c03406235556ebc209e Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:22:25 +0100 Subject: [PATCH 1/6] 6592: Added check for media uri pattern --- e2e/slide-media-sync.spec.js | 16 ++++++++++++++++ src/components/slide/slide-media-utils.js | 19 ++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/e2e/slide-media-sync.spec.js b/e2e/slide-media-sync.spec.js index bbcb9c51..5e736066 100644 --- a/e2e/slide-media-sync.spec.js +++ b/e2e/slide-media-sync.spec.js @@ -123,4 +123,20 @@ test.describe("Slide media sync", () => { // contacts is an array of objects, not strings — objects are skipped expect(result).not.toContain("/v2/media/2"); }); + + test("It does not include non-media string arrays from content", () => { + // Regression: previously any array-of-strings could be treated as media, + // e.g. tags/categories/etc. Only actual media IRIs should be returned. + const content = { + images: ["/v2/media/1"], + tags: ["news", "sports"], + }; + const mediaFields = []; // rely on top-level scan + + const result = rebuildMediaFromContent(content, mediaFields); + + expect(result).toEqual(["/v2/media/1"]); + expect(result).not.toContain("news"); + expect(result).not.toContain("sports"); + }); }); diff --git a/src/components/slide/slide-media-utils.js b/src/components/slide/slide-media-utils.js index 70d4df9e..b0706e16 100644 --- a/src/components/slide/slide-media-utils.js +++ b/src/components/slide/slide-media-utils.js @@ -15,6 +15,11 @@ export default function rebuildMediaFromContent(content, mediaFields) { const media = []; const fieldsToScan = new Set(mediaFields); + const isMediaIri = (value) => + typeof value === "string" && + !value.startsWith("TEMP--") && + value.includes("/v2/media/"); + // Also scan top-level content keys to catch media fields not yet // tracked via handleMedia (e.g. on first edit of another field). if (content && typeof content === "object") { @@ -23,13 +28,13 @@ export default function rebuildMediaFromContent(content, mediaFields) { fieldsToScan.forEach((fieldName) => { const fieldData = get(content, fieldName); - if (Array.isArray(fieldData)) { - fieldData.forEach((mediaId) => { - if (typeof mediaId === "string" && !mediaId.startsWith("TEMP--")) { - media.push(mediaId); - } - }); - } + if (!Array.isArray(fieldData)) return; + + fieldData.forEach((candidate) => { + if (isMediaIri(candidate)) { + media.push(candidate); + } + }); }); return [...new Set(media)]; From 1d33b7b835e32f29d7eac20d0863dc334986a5ea Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:29:45 +0100 Subject: [PATCH 2/6] 6592: Replaced includes with regex --- src/components/slide/slide-media-utils.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/slide/slide-media-utils.js b/src/components/slide/slide-media-utils.js index b0706e16..558cf2b5 100644 --- a/src/components/slide/slide-media-utils.js +++ b/src/components/slide/slide-media-utils.js @@ -15,13 +15,15 @@ export default function rebuildMediaFromContent(content, mediaFields) { const media = []; const fieldsToScan = new Set(mediaFields); + const mediaIriRegex = /\/v2\/media\/.+/; + const isMediaIri = (value) => typeof value === "string" && !value.startsWith("TEMP--") && - value.includes("/v2/media/"); + mediaIriRegex.test(value); - // Also scan top-level content keys to catch media fields not yet - // tracked via handleMedia (e.g. on first edit of another field). + // Also, scan top-level content keys to catch media fields not yet + // tracked via handleMedia (e.g. on the first edit of another field). if (content && typeof content === "object") { Object.keys(content).forEach((key) => fieldsToScan.add(key)); } @@ -37,5 +39,7 @@ export default function rebuildMediaFromContent(content, mediaFields) { }); }); + console.log("media", [...new Set(media)]); + return [...new Set(media)]; } From 1a1715379fa968b40a8b30d2ff86b76c31d83f99 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:32:02 +0100 Subject: [PATCH 3/6] 6592: Removed console log --- src/components/slide/slide-media-utils.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/slide/slide-media-utils.js b/src/components/slide/slide-media-utils.js index 558cf2b5..e737f051 100644 --- a/src/components/slide/slide-media-utils.js +++ b/src/components/slide/slide-media-utils.js @@ -39,7 +39,5 @@ export default function rebuildMediaFromContent(content, mediaFields) { }); }); - console.log("media", [...new Set(media)]); - return [...new Set(media)]; } From 665f0a8b232dea6df653248c97c79dd2a6bd0c38 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 2 Mar 2026 08:14:28 +0100 Subject: [PATCH 4/6] 6592: Changed to handle nested media fields in content --- e2e/slide-media-sync.spec.js | 9 +++---- src/components/slide/slide-media-utils.js | 30 +++++++++++++++++------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/e2e/slide-media-sync.spec.js b/e2e/slide-media-sync.spec.js index 5e736066..00b76a0b 100644 --- a/e2e/slide-media-sync.spec.js +++ b/e2e/slide-media-sync.spec.js @@ -112,7 +112,7 @@ test.describe("Slide media sync", () => { images: ["/v2/media/1"], title: "Some text", separator: true, - contacts: [{ name: "John", image: ["/v2/media/2"] }], + contacts: [{ name: "John", image: ["/v2/media/2"], tags: ["news"] }], }; const mediaFields = []; @@ -120,13 +120,12 @@ test.describe("Slide media sync", () => { // images field is picked up via top-level scan expect(result).toContain("/v2/media/1"); - // contacts is an array of objects, not strings — objects are skipped - expect(result).not.toContain("/v2/media/2"); + expect(result).toContain("/v2/media/2"); + expect(result).not.toContain("news"); }); test("It does not include non-media string arrays from content", () => { - // Regression: previously any array-of-strings could be treated as media, - // e.g. tags/categories/etc. Only actual media IRIs should be returned. + // Only actual media IRIs should be returned. const content = { images: ["/v2/media/1"], tags: ["news", "sports"], diff --git a/src/components/slide/slide-media-utils.js b/src/components/slide/slide-media-utils.js index e737f051..3be19c8c 100644 --- a/src/components/slide/slide-media-utils.js +++ b/src/components/slide/slide-media-utils.js @@ -22,6 +22,28 @@ export default function rebuildMediaFromContent(content, mediaFields) { !value.startsWith("TEMP--") && mediaIriRegex.test(value); + const collectMediaFromValue = (value, seen = new Set()) => { + if (value === null || value === undefined) return; + + if (typeof value === "string") { + if (isMediaIri(value)) media.push(value); + return; + } + + if (typeof value !== "object") return; + + // Avoid potential circular references (defensive; content is usually JSON) + if (seen.has(value)) return; + seen.add(value); + + if (Array.isArray(value)) { + value.forEach((item) => collectMediaFromValue(item, seen)); + return; + } + + Object.values(value).forEach((item) => collectMediaFromValue(item, seen)); + }; + // Also, scan top-level content keys to catch media fields not yet // tracked via handleMedia (e.g. on the first edit of another field). if (content && typeof content === "object") { @@ -30,13 +52,7 @@ export default function rebuildMediaFromContent(content, mediaFields) { fieldsToScan.forEach((fieldName) => { const fieldData = get(content, fieldName); - if (!Array.isArray(fieldData)) return; - - fieldData.forEach((candidate) => { - if (isMediaIri(candidate)) { - media.push(candidate); - } - }); + collectMediaFromValue(fieldData); }); return [...new Set(media)]; From ef9292be17ddbdba30c044ec3f07e7c0c5181efc Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:52:10 +0100 Subject: [PATCH 5/6] 6592: Added comments --- src/components/slide/slide-media-utils.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/slide/slide-media-utils.js b/src/components/slide/slide-media-utils.js index 3be19c8c..67bfb0f4 100644 --- a/src/components/slide/slide-media-utils.js +++ b/src/components/slide/slide-media-utils.js @@ -23,24 +23,31 @@ export default function rebuildMediaFromContent(content, mediaFields) { mediaIriRegex.test(value); const collectMediaFromValue = (value, seen = new Set()) => { + // 1) Ignore empty values early (nothing to scan) if (value === null || value === undefined) return; + // 2) If it's a string, it might be a media IRI; validate and collect it if (typeof value === "string") { if (isMediaIri(value)) media.push(value); return; } + // 3) If it's not an object (e.g. number/boolean/function), it cannot contain nested media if (typeof value !== "object") return; - // Avoid potential circular references (defensive; content is usually JSON) + // 4) Defensive guard against circular references: + // - JSON content won't have cycles, but runtime objects might. + // - If we've seen this object/array already, stop to avoid infinite recursion. if (seen.has(value)) return; seen.add(value); + // 5) If it's an array, scan each element (elements can be strings, objects, or more arrays) if (Array.isArray(value)) { value.forEach((item) => collectMediaFromValue(item, seen)); return; } + // 6) Otherwise it's a plain object: scan its property values recursively Object.values(value).forEach((item) => collectMediaFromValue(item, seen)); }; From f5972c6a5576ce7eff511a2155bee778588bb097 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:09:00 +0100 Subject: [PATCH 6/6] 6592: Added test for infinite recursion protection --- e2e/slide-media-sync.spec.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/e2e/slide-media-sync.spec.js b/e2e/slide-media-sync.spec.js index 00b76a0b..65962b50 100644 --- a/e2e/slide-media-sync.spec.js +++ b/e2e/slide-media-sync.spec.js @@ -138,4 +138,15 @@ test.describe("Slide media sync", () => { expect(result).not.toContain("news"); expect(result).not.toContain("sports"); }); + + test("It avoids infinite recursion when content contains circular references", () => { + const circular = { images: ["/v2/media/1"] }; + circular.self = circular; // create an explicit cycle + + // If we didn't track `seen`, this would crash. + expect(() => rebuildMediaFromContent(circular, [])).not.toThrow(); + + const result = rebuildMediaFromContent(circular, []); + expect(result).toContain("/v2/media/1"); + }); });