Mermaid diagram "Copy failed: The operation is insecure" on Linux

This commit is contained in:
Yuri Karamian
2026-07-10 14:09:56 +02:00
parent c4d1f12b2e
commit 54352a03d8
+32 -7
View File
@@ -1259,15 +1259,25 @@
async function svgToPngBlob(svgEl: SVGElement, scale = 2): Promise<Blob> { async function svgToPngBlob(svgEl: SVGElement, scale = 2): Promise<Blob> {
const clone = svgEl.cloneNode(true) as SVGElement; const clone = svgEl.cloneNode(true) as SVGElement;
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
if (!clone.getAttribute('xmlns:xlink')) clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
// Sanitize: remove external resource references that taint canvas on WebKitGTK ("The operation is insecure")
clone.querySelectorAll('image').forEach((img) => {
const href = img.getAttribute('href') || img.getAttribute('xlink:href') || '';
if (href.startsWith('http') || href.startsWith('//') || href.startsWith('data:')) {
img.remove();
}
});
const svgString = new XMLSerializer().serializeToString(clone); const svgString = new XMLSerializer().serializeToString(clone);
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }); // Use data URL instead of blob URL — blob: URLs taint canvas on some WebKitGTK versions
const url = URL.createObjectURL(svgBlob); const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgString);
try {
const img = new window.Image(); const img = new window.Image();
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
img.onload = () => resolve(); img.onload = () => resolve();
img.onerror = () => reject(new Error('SVG image load failed')); img.onerror = () => reject(new Error('SVG image load failed'));
img.src = url; img.src = dataUrl;
}); });
const bbox = svgEl.getBoundingClientRect(); const bbox = svgEl.getBoundingClientRect();
const width = Math.max(Math.round(bbox.width || 800), 100); const width = Math.max(Math.round(bbox.width || 800), 100);
@@ -1284,9 +1294,6 @@
return await new Promise<Blob>((resolve, reject) => { return await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((b) => b ? resolve(b) : reject(new Error('toBlob failed')), 'image/png'); canvas.toBlob((b) => b ? resolve(b) : reject(new Error('toBlob failed')), 'image/png');
}); });
} finally {
URL.revokeObjectURL(url);
}
} }
function flashToast(container: HTMLElement, msg: string) { function flashToast(container: HTMLElement, msg: string) {
@@ -1309,9 +1316,22 @@
flashToast(container, 'Copied'); flashToast(container, 'Copied');
} catch (e: any) { } catch (e: any) {
console.error('[Mermaid] copy failed', e); console.error('[Mermaid] copy failed', e);
// SecurityError fallback: canvas tainted by WebKitGTK — copy SVG source as text instead
if (e instanceof DOMException && (e.name === 'SecurityError' || e.code === 18)) {
try {
const clone = svgEl.cloneNode(true) as SVGElement;
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
const svgString = new XMLSerializer().serializeToString(clone);
await navigator.clipboard.writeText(svgString);
flashToast(container, 'Copied SVG source (PNG blocked by browser security)');
} catch (e2: any) {
flashToast(container, 'Copy failed: ' + (e2?.message || String(e2)));
}
} else {
flashToast(container, 'Copy failed: ' + (e?.message || String(e))); flashToast(container, 'Copy failed: ' + (e?.message || String(e)));
} }
} }
}
async function saveDiagram(container: HTMLElement) { async function saveDiagram(container: HTMLElement) {
const svgEl = container.querySelector('svg') as SVGElement | null; const svgEl = container.querySelector('svg') as SVGElement | null;
@@ -1339,9 +1359,14 @@
flashToast(container, 'Saved'); flashToast(container, 'Saved');
} catch (e: any) { } catch (e: any) {
console.error('[Mermaid] save failed', e); console.error('[Mermaid] save failed', e);
// SecurityError fallback: canvas tainted by WebKitGTK — tell user to use SVG format
if (e instanceof DOMException && (e.name === 'SecurityError' || e.code === 18)) {
flashToast(container, 'PNG blocked by browser security — try SVG format');
} else {
flashToast(container, 'Save failed: ' + (e?.message || String(e))); flashToast(container, 'Save failed: ' + (e?.message || String(e)));
} }
} }
}
function addToolbar(container: HTMLElement, source: string) { function addToolbar(container: HTMLElement, source: string) {
const toolbar = document.createElement('div'); const toolbar = document.createElement('div');