From fe4c3138c8694311b30dc72e35be3507f8a9020e Mon Sep 17 00:00:00 2001 From: Duong Phu Dong Date: Mon, 25 May 2026 19:21:04 +0700 Subject: [PATCH 1/2] fix(diagrams): avoid rendering mermaid svg as dom --- app/dashboard/diagrams/text/[id]/page.tsx | 72 ++++++++++++----------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/app/dashboard/diagrams/text/[id]/page.tsx b/app/dashboard/diagrams/text/[id]/page.tsx index 5c02ef6..de0d44c 100644 --- a/app/dashboard/diagrams/text/[id]/page.tsx +++ b/app/dashboard/diagrams/text/[id]/page.tsx @@ -22,33 +22,6 @@ interface Diagram { category?: string } -function sanitizeMermaidSvg(svg: string) { - const parser = new DOMParser() - const doc = parser.parseFromString(svg, "image/svg+xml") - const disallowedTags = new Set(["script", "foreignObject"]) - const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT) - const toRemove: Element[] = [] - - while (walker.nextNode()) { - const element = walker.currentNode as Element - if (disallowedTags.has(element.tagName)) { - toRemove.push(element) - continue - } - - for (const attr of Array.from(element.attributes)) { - const name = attr.name.toLowerCase() - const value = attr.value.trim().toLowerCase() - if (name.startsWith("on") || value.startsWith("javascript:")) { - element.removeAttribute(attr.name) - } - } - } - - toRemove.forEach((element) => element.remove()) - return doc.documentElement -} - function toViewDiagram(row: DBDiagram): Diagram { const payload = row.data && typeof row.data === "object" ? row.data : {} return { @@ -75,9 +48,27 @@ export default function TextDiagramEditorPage() { const [isPreviewMode, setIsPreviewMode] = useState(false) const [savedMessage, setSavedMessage] = useState(false) const [error, setError] = useState(null) - const previewRef = useRef(null) + const [previewUrl, setPreviewUrl] = useState(null) + const previewUrlRef = useRef(null) const [showDocumentation, setShowDocumentation] = useState(false) + const replacePreviewUrl = useCallback((url: string | null) => { + if (previewUrlRef.current) { + URL.revokeObjectURL(previewUrlRef.current) + } + + previewUrlRef.current = url + setPreviewUrl(url) + }, []) + + useEffect(() => { + return () => { + if (previewUrlRef.current) { + URL.revokeObjectURL(previewUrlRef.current) + } + } + }, []) + useEffect(() => { const initMermaid = async () => { const { default: mermaid } = await import("mermaid") @@ -132,24 +123,25 @@ export default function TextDiagramEditorPage() { }, [diagramId, router]) const renderDiagram = useCallback(async () => { - if (!previewRef.current || !textContent) return + if (!textContent) return try { setError(null) - previewRef.current.replaceChildren() const { default: mermaid } = await import("mermaid") const id = `mermaid-${Date.now()}` const { svg } = await mermaid.render(id, textContent) - previewRef.current.appendChild(sanitizeMermaidSvg(svg)) + const blob = new Blob([svg], { type: "image/svg+xml" }) + replacePreviewUrl(URL.createObjectURL(blob)) } catch (err: any) { + replacePreviewUrl(null) setError(err.message || "Failed to render diagram") console.error("Mermaid render error:", err) } - }, [textContent]) + }, [replacePreviewUrl, textContent]) useEffect(() => { - if (isPreviewMode && textContent && previewRef.current) { + if (isPreviewMode && textContent) { void renderDiagram() } }, [isPreviewMode, textContent, renderDiagram]) @@ -409,7 +401,19 @@ export default function TextDiagramEditorPage() {
{error}
) : ( -
+
+ {previewUrl ? ( + // Mermaid preview uses a generated object URL, so Next Image optimization does not apply. + // eslint-disable-next-line @next/next/no-img-element + {diagram?.name + ) : ( +

Render the diagram to preview it.

+ )} +
)}
From 1de44ca05c4b84b037dd050d150737ed437e16c5 Mon Sep 17 00:00:00 2001 From: Duong Phu Dong Date: Mon, 25 May 2026 19:21:12 +0700 Subject: [PATCH 2/2] fix(i18n): harden translation merge keys --- lib/config/i18n.ts | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/lib/config/i18n.ts b/lib/config/i18n.ts index 003aa9f..2d8c381 100644 --- a/lib/config/i18n.ts +++ b/lib/config/i18n.ts @@ -1763,10 +1763,19 @@ export function getTranslations(lang: Language): Translations { const clone = JSON.parse(JSON.stringify(base)) as Translations - const merge = (target: Record, source: Record) => { - const blockedKeys = new Set(["__proto__", "constructor", "prototype"]) + const isUnsafeMergeKey = (key: string) => + key === "__proto__" || key === "constructor" || key === "prototype" + + const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value) + + const merge = (target: Record, source: Record) => { Object.keys(source).forEach((key) => { - if (blockedKeys.has(key)) { + if ( + isUnsafeMergeKey(key) || + !Object.prototype.hasOwnProperty.call(source, key) || + !Object.prototype.hasOwnProperty.call(target, key) + ) { return } @@ -1776,22 +1785,20 @@ export function getTranslations(lang: Language): Translations { } const targetValue = target[key] - if ( - typeof targetValue === "object" && - targetValue !== null && - !Array.isArray(targetValue) && - typeof value === "object" && - value !== null && - !Array.isArray(value) - ) { - merge(targetValue, value as Record) + if (isRecord(targetValue) && isRecord(value)) { + merge(targetValue, value) } else { - target[key] = value + Object.defineProperty(target, key, { + value, + configurable: true, + enumerable: true, + writable: true, + }) } }) } - merge(clone as unknown as Record, overrides as Record) + merge(clone as unknown as Record, overrides as Record) return clone }