Skip to content

Commit c92a8d7

Browse files
Merge pull request #32 from lab68dev/fix-codeql-diagram-i18n-hardening
Fix diagram rendering and enhance translation merging
2 parents 215f566 + 1de44ca commit c92a8d7

2 files changed

Lines changed: 59 additions & 48 deletions

File tree

app/dashboard/diagrams/text/[id]/page.tsx

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,6 @@ interface Diagram {
2222
category?: string
2323
}
2424

25-
function sanitizeMermaidSvg(svg: string) {
26-
const parser = new DOMParser()
27-
const doc = parser.parseFromString(svg, "image/svg+xml")
28-
const disallowedTags = new Set(["script", "foreignObject"])
29-
const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT)
30-
const toRemove: Element[] = []
31-
32-
while (walker.nextNode()) {
33-
const element = walker.currentNode as Element
34-
if (disallowedTags.has(element.tagName)) {
35-
toRemove.push(element)
36-
continue
37-
}
38-
39-
for (const attr of Array.from(element.attributes)) {
40-
const name = attr.name.toLowerCase()
41-
const value = attr.value.trim().toLowerCase()
42-
if (name.startsWith("on") || value.startsWith("javascript:")) {
43-
element.removeAttribute(attr.name)
44-
}
45-
}
46-
}
47-
48-
toRemove.forEach((element) => element.remove())
49-
return doc.documentElement
50-
}
51-
5225
function toViewDiagram(row: DBDiagram): Diagram {
5326
const payload = row.data && typeof row.data === "object" ? row.data : {}
5427
return {
@@ -75,9 +48,27 @@ export default function TextDiagramEditorPage() {
7548
const [isPreviewMode, setIsPreviewMode] = useState(false)
7649
const [savedMessage, setSavedMessage] = useState(false)
7750
const [error, setError] = useState<string | null>(null)
78-
const previewRef = useRef<HTMLDivElement>(null)
51+
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
52+
const previewUrlRef = useRef<string | null>(null)
7953
const [showDocumentation, setShowDocumentation] = useState(false)
8054

55+
const replacePreviewUrl = useCallback((url: string | null) => {
56+
if (previewUrlRef.current) {
57+
URL.revokeObjectURL(previewUrlRef.current)
58+
}
59+
60+
previewUrlRef.current = url
61+
setPreviewUrl(url)
62+
}, [])
63+
64+
useEffect(() => {
65+
return () => {
66+
if (previewUrlRef.current) {
67+
URL.revokeObjectURL(previewUrlRef.current)
68+
}
69+
}
70+
}, [])
71+
8172
useEffect(() => {
8273
const initMermaid = async () => {
8374
const { default: mermaid } = await import("mermaid")
@@ -132,24 +123,25 @@ export default function TextDiagramEditorPage() {
132123
}, [diagramId, router])
133124

134125
const renderDiagram = useCallback(async () => {
135-
if (!previewRef.current || !textContent) return
126+
if (!textContent) return
136127

137128
try {
138129
setError(null)
139-
previewRef.current.replaceChildren()
140130

141131
const { default: mermaid } = await import("mermaid")
142132
const id = `mermaid-${Date.now()}`
143133
const { svg } = await mermaid.render(id, textContent)
144-
previewRef.current.appendChild(sanitizeMermaidSvg(svg))
134+
const blob = new Blob([svg], { type: "image/svg+xml" })
135+
replacePreviewUrl(URL.createObjectURL(blob))
145136
} catch (err: any) {
137+
replacePreviewUrl(null)
146138
setError(err.message || "Failed to render diagram")
147139
console.error("Mermaid render error:", err)
148140
}
149-
}, [textContent])
141+
}, [replacePreviewUrl, textContent])
150142

151143
useEffect(() => {
152-
if (isPreviewMode && textContent && previewRef.current) {
144+
if (isPreviewMode && textContent) {
153145
void renderDiagram()
154146
}
155147
}, [isPreviewMode, textContent, renderDiagram])
@@ -409,7 +401,19 @@ export default function TextDiagramEditorPage() {
409401
<pre className="text-sm whitespace-pre-wrap">{error}</pre>
410402
</div>
411403
) : (
412-
<div ref={previewRef} className="flex items-center justify-center min-h-full" />
404+
<div className="flex min-h-full items-center justify-center">
405+
{previewUrl ? (
406+
// Mermaid preview uses a generated object URL, so Next Image optimization does not apply.
407+
// eslint-disable-next-line @next/next/no-img-element
408+
<img
409+
src={previewUrl}
410+
alt={diagram?.name ? `${diagram.name} preview` : "Diagram preview"}
411+
className="max-h-full max-w-full object-contain"
412+
/>
413+
) : (
414+
<p className="text-sm text-muted-foreground">Render the diagram to preview it.</p>
415+
)}
416+
</div>
413417
)}
414418
</div>
415419
</div>

lib/config/i18n.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,10 +1763,19 @@ export function getTranslations(lang: Language): Translations {
17631763

17641764
const clone = JSON.parse(JSON.stringify(base)) as Translations
17651765

1766-
const merge = (target: Record<string, any>, source: Record<string, any>) => {
1767-
const blockedKeys = new Set(["__proto__", "constructor", "prototype"])
1766+
const isUnsafeMergeKey = (key: string) =>
1767+
key === "__proto__" || key === "constructor" || key === "prototype"
1768+
1769+
const isRecord = (value: unknown): value is Record<string, unknown> =>
1770+
typeof value === "object" && value !== null && !Array.isArray(value)
1771+
1772+
const merge = (target: Record<string, unknown>, source: Record<string, unknown>) => {
17681773
Object.keys(source).forEach((key) => {
1769-
if (blockedKeys.has(key)) {
1774+
if (
1775+
isUnsafeMergeKey(key) ||
1776+
!Object.prototype.hasOwnProperty.call(source, key) ||
1777+
!Object.prototype.hasOwnProperty.call(target, key)
1778+
) {
17701779
return
17711780
}
17721781

@@ -1776,22 +1785,20 @@ export function getTranslations(lang: Language): Translations {
17761785
}
17771786

17781787
const targetValue = target[key]
1779-
if (
1780-
typeof targetValue === "object" &&
1781-
targetValue !== null &&
1782-
!Array.isArray(targetValue) &&
1783-
typeof value === "object" &&
1784-
value !== null &&
1785-
!Array.isArray(value)
1786-
) {
1787-
merge(targetValue, value as Record<string, any>)
1788+
if (isRecord(targetValue) && isRecord(value)) {
1789+
merge(targetValue, value)
17881790
} else {
1789-
target[key] = value
1791+
Object.defineProperty(target, key, {
1792+
value,
1793+
configurable: true,
1794+
enumerable: true,
1795+
writable: true,
1796+
})
17901797
}
17911798
})
17921799
}
17931800

1794-
merge(clone as unknown as Record<string, any>, overrides as Record<string, any>)
1801+
merge(clone as unknown as Record<string, unknown>, overrides as Record<string, unknown>)
17951802
return clone
17961803
}
17971804

0 commit comments

Comments
 (0)