@file-viewer/renderer-drawing 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/diagram.js +6 -20
- package/dist/drawing.d.ts +8 -8
- package/dist/drawing.js +298 -110
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/optionalCapabilities.d.ts +3 -0
- package/dist/optionalCapabilities.js +8 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +32 -0
- package/file-viewer.capability.json +12 -0
- package/package.json +7 -2
package/dist/diagram.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createFileViewerTranslator } from '@file-viewer/core';
|
|
1
|
+
import { createFileViewerTranslator, assertFileViewerMermaidSourceHasNoExternalResources } from '@file-viewer/core';
|
|
2
2
|
import Panzoom, {} from '@panzoom/panzoom';
|
|
3
|
+
import { sanitizeDrawingSvg } from './sanitize.js';
|
|
3
4
|
const getOwnerWindow = (documentRef) => {
|
|
4
5
|
return documentRef.defaultView || (typeof window !== 'undefined' ? window : undefined);
|
|
5
6
|
};
|
|
@@ -29,34 +30,19 @@ const normalizePlantumlServer = (documentRef, value) => {
|
|
|
29
30
|
return normalized;
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
|
-
const sanitizeSvg = (documentRef, svg, t) => {
|
|
33
|
-
const parsed = new DOMParser().parseFromString(svg, 'image/svg+xml');
|
|
34
|
-
const parseError = parsed.querySelector('parsererror');
|
|
35
|
-
if (parseError) {
|
|
36
|
-
throw new Error(parseError.textContent || t('drawing.error.svgParseFailed'));
|
|
37
|
-
}
|
|
38
|
-
parsed.querySelectorAll('script,iframe,object,embed').forEach(node => node.remove());
|
|
39
|
-
parsed.querySelectorAll('*').forEach(node => {
|
|
40
|
-
for (const attribute of Array.from(node.attributes)) {
|
|
41
|
-
if (/^on/i.test(attribute.name)) {
|
|
42
|
-
node.removeAttribute(attribute.name);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
const svgNode = parsed.documentElement;
|
|
47
|
-
return documentRef.importNode(svgNode, true);
|
|
48
|
-
};
|
|
49
33
|
const renderMermaidSvg = async (documentRef, text, theme, t) => {
|
|
34
|
+
assertFileViewerMermaidSourceHasNoExternalResources(text);
|
|
50
35
|
const mermaidModule = await import('mermaid');
|
|
51
36
|
const mermaid = mermaidModule.default;
|
|
52
37
|
const id = `file-viewer-mermaid-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
53
38
|
mermaid.initialize({
|
|
54
39
|
startOnLoad: false,
|
|
55
40
|
securityLevel: 'strict',
|
|
41
|
+
htmlLabels: false,
|
|
56
42
|
theme: isDarkTheme(documentRef, theme) ? 'dark' : 'default'
|
|
57
43
|
});
|
|
58
44
|
const rendered = await mermaid.render(id, text);
|
|
59
|
-
return
|
|
45
|
+
return sanitizeDrawingSvg(documentRef, rendered.svg, t('drawing.error.svgParseFailed'));
|
|
60
46
|
};
|
|
61
47
|
const appendSvgText = (documentRef, parent, text, x, dy, weight = '500') => {
|
|
62
48
|
const line = documentRef.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
|
@@ -138,7 +124,7 @@ const renderPlantumlSvg = async (documentRef, text, options, t) => {
|
|
|
138
124
|
if (!response.ok) {
|
|
139
125
|
throw new Error(t('drawing.error.plantumlRenderFailed', { status: response.status }));
|
|
140
126
|
}
|
|
141
|
-
return
|
|
127
|
+
return sanitizeDrawingSvg(documentRef, await response.text(), t('drawing.error.svgParseFailed'));
|
|
142
128
|
}
|
|
143
129
|
catch (error) {
|
|
144
130
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
package/dist/drawing.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { FileRenderContext, FileViewerRenderedInstance } from '@file-viewer/core';
|
|
2
|
-
declare
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
export declare const normalizeDrawioText: (documentRef: Document, value: string | null) => string;
|
|
3
|
+
/**
|
|
4
|
+
* diagrams.net is a large third-party runtime. Its same-document renderer may
|
|
5
|
+
* interpret cell labels and style resource keys as HTML or URLs, so sanitize
|
|
6
|
+
* the direct mxGraphModel before it reaches that runtime. Compressed/opaque
|
|
7
|
+
* models fail closed here and use the inert fallback instead.
|
|
8
|
+
*/
|
|
9
|
+
export declare const sanitizeOfficialDrawioXml: (documentRef: Document, value: string, invalidMessage?: string) => string;
|
|
10
10
|
export default function renderDrawing(buffer: ArrayBuffer, target: HTMLDivElement, type?: string, context?: FileRenderContext): Promise<FileViewerRenderedInstance>;
|
package/dist/drawing.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createFileViewerTranslator, createFileViewerZoomChangeEmitter, registerFileViewerZoomProvider, readFileViewerText, resolveFileViewerDrawioViewerScriptUrl, resolveFileViewerRuntimeAssetBaseUrl,
|
|
1
|
+
import { createFileViewerTranslator, createFileViewerZoomChangeEmitter, registerFileViewerZoomProvider, readFileViewerText, resolveFileViewerDrawioViewerScriptUrl, resolveFileViewerRuntimeAssetBaseUrl, unregisterFileViewerZoomProvider, } from '@file-viewer/core';
|
|
2
|
+
import { isFileViewerDrawioOfficialViewerEnabled } from './optionalCapabilities.js';
|
|
2
3
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
3
4
|
const EXCALIDRAW_OFFICIAL_TIMEOUT = 6000;
|
|
4
5
|
const DRAWIO_OFFICIAL_TIMEOUT = 6000;
|
|
5
|
-
const diagramsViewerPromises = new WeakMap();
|
|
6
6
|
const drawingStyle = `
|
|
7
7
|
.drawing-viewer{display:flex;height:100%;min-height:360px;flex-direction:column;background:#edf2f7;color:#172033}
|
|
8
8
|
.drawing-toolbar{position:sticky;top:0;z-index:2;display:flex;min-height:46px;align-items:center;justify-content:space-between;gap:16px;padding:8px 14px;border-bottom:1px solid rgba(148,163,184,.35);background:rgba(248,250,252,.92);backdrop-filter:blur(12px)}
|
|
@@ -17,7 +17,7 @@ const drawingStyle = `
|
|
|
17
17
|
.drawing-scroll{height:100%;overflow:auto;padding:22px}
|
|
18
18
|
.drawing-canvas{width:100%;min-height:420px;transition:transform .18s ease,zoom .18s ease}
|
|
19
19
|
.drawing-canvas .drawing-svg,.drawing-canvas svg{display:block;max-width:100%;height:auto;margin:0 auto;border-radius:10px;background:#fff;box-shadow:0 18px 42px rgba(15,23,42,.12)}
|
|
20
|
-
.drawing-canvas .drawing-mxgraph{min-height:420px;overflow:hidden;border-radius:10px;background:#fff;box-shadow:0 18px 42px rgba(15,23,42,.12)}
|
|
20
|
+
.drawing-canvas .drawing-mxgraph{display:block;width:100%;min-height:420px;overflow:hidden;border:0;border-radius:10px;background:#fff;box-shadow:0 18px 42px rgba(15,23,42,.12)}
|
|
21
21
|
.drawing-diagram-shell{display:flex;min-height:100%;align-items:center;justify-content:center;overflow:hidden;border-radius:10px;background:linear-gradient(135deg,#f8fafc,#eef6f4);box-shadow:0 18px 42px rgba(15,23,42,.12)}
|
|
22
22
|
.drawing-diagram-pan{display:inline-flex;min-width:240px;min-height:180px;align-items:center;justify-content:center;padding:32px;cursor:grab;touch-action:none}
|
|
23
23
|
.drawing-diagram-pan:active{cursor:grabbing}
|
|
@@ -86,87 +86,99 @@ const resolveDirectoryUrl = (url) => {
|
|
|
86
86
|
return slashIndex >= 0 ? url.slice(0, slashIndex + 1) : '';
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
const escapeHtmlAttribute = (value) => value
|
|
90
|
+
.replace(/&/g, '&')
|
|
91
|
+
.replace(/"/g, '"')
|
|
92
|
+
.replace(/</g, '<')
|
|
93
|
+
.replace(/>/g, '>');
|
|
94
|
+
const createDrawioSandboxNonce = (ownerWindow) => {
|
|
95
|
+
const bytes = new Uint8Array(18);
|
|
96
|
+
ownerWindow.crypto.getRandomValues(bytes);
|
|
97
|
+
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
98
|
+
};
|
|
99
|
+
const resolveSafeDrawioViewerScript = (documentRef, scriptUrl) => {
|
|
100
|
+
const resolved = new URL(scriptUrl, documentRef.baseURI);
|
|
101
|
+
const documentUrl = new URL(documentRef.baseURI);
|
|
102
|
+
if (!/^https?:$/.test(resolved.protocol) || resolved.origin !== documentUrl.origin) {
|
|
103
|
+
throw new Error('The diagrams.net viewer script must use a same-origin HTTP(S) URL.');
|
|
93
104
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
return resolved;
|
|
106
|
+
};
|
|
107
|
+
const buildDrawioSandboxDocument = (xml, scriptUrl, nonce, channel) => {
|
|
108
|
+
const baseUrl = resolveDirectoryUrl(scriptUrl.href);
|
|
109
|
+
const assetRoot = baseUrl.replace(/\/$/, '');
|
|
110
|
+
const runtimeConfig = {
|
|
111
|
+
PROXY_URL: `${baseUrl}proxy`,
|
|
112
|
+
STYLE_PATH: `${baseUrl}styles`,
|
|
113
|
+
SHAPES_PATH: `${baseUrl}shapes`,
|
|
114
|
+
STENCIL_PATH: `${baseUrl}stencils`,
|
|
115
|
+
DRAW_MATH_URL: `${baseUrl}math4/es5`,
|
|
116
|
+
GRAPH_IMAGE_PATH: `${baseUrl}img`,
|
|
117
|
+
mxImageBasePath: `${baseUrl}mxgraph/images`,
|
|
118
|
+
mxBasePath: `${baseUrl}mxgraph/`,
|
|
119
|
+
mxLoadStylesheets: false,
|
|
120
|
+
DRAWIO_BASE_URL: assetRoot,
|
|
121
|
+
DRAWIO_LIGHTBOX_URL: assetRoot,
|
|
122
|
+
DRAWIO_SERVER_URL: baseUrl,
|
|
123
|
+
DRAWIO_VIEWER_URL: scriptUrl.href,
|
|
124
|
+
DRAWIO_LOG_URL: '',
|
|
125
|
+
EXPORT_URL: `${baseUrl}export`,
|
|
126
|
+
PLANT_URL: `${baseUrl}plant`,
|
|
127
|
+
VSS_CONVERT_URL: `${baseUrl}VsdConverter/api/converter`,
|
|
128
|
+
DRAWIO_GITLAB_URL: baseUrl,
|
|
129
|
+
DRAWIO_GITHUB_URL: baseUrl,
|
|
130
|
+
DRAWIO_GITHUB_API_URL: baseUrl,
|
|
131
|
+
RT_WEBSOCKET_URL: `${baseUrl}rt`,
|
|
132
|
+
NOTIFICATIONS_URL: `${baseUrl}notifications`,
|
|
100
133
|
};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const existingPromise = promiseMap.get(scriptUrl);
|
|
150
|
-
if (existingPromise) {
|
|
151
|
-
return existingPromise;
|
|
152
|
-
}
|
|
153
|
-
const nextPromise = new Promise((resolve, reject) => {
|
|
154
|
-
const existed = Array.from(documentRef.querySelectorAll('script[src]'))
|
|
155
|
-
.find(script => script.src === scriptUrl);
|
|
156
|
-
if (existed) {
|
|
157
|
-
existed.addEventListener('load', () => resolve(), { once: true });
|
|
158
|
-
existed.addEventListener('error', () => reject(new Error(t('drawing.error.viewerLoadFailed'))), { once: true });
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const script = documentRef.createElement('script');
|
|
162
|
-
script.src = scriptUrl;
|
|
163
|
-
script.async = true;
|
|
164
|
-
script.onload = () => resolve();
|
|
165
|
-
script.onerror = () => reject(new Error(t('drawing.error.viewerLoadFailed')));
|
|
166
|
-
documentRef.head.appendChild(script);
|
|
167
|
-
});
|
|
168
|
-
promiseMap.set(scriptUrl, nextPromise);
|
|
169
|
-
return nextPromise;
|
|
134
|
+
const graphConfig = {
|
|
135
|
+
xml,
|
|
136
|
+
toolbar: 'zoom layers lightbox',
|
|
137
|
+
nav: true,
|
|
138
|
+
resize: true,
|
|
139
|
+
'auto-fit': true,
|
|
140
|
+
'auto-crop': true,
|
|
141
|
+
'auto-origin': true,
|
|
142
|
+
'allow-zoom-in': true,
|
|
143
|
+
'allow-zoom-out': true,
|
|
144
|
+
border: 16,
|
|
145
|
+
highlight: '#0f766e',
|
|
146
|
+
};
|
|
147
|
+
const csp = [
|
|
148
|
+
"default-src 'none'",
|
|
149
|
+
`script-src 'nonce-${nonce}' ${baseUrl}`,
|
|
150
|
+
`style-src 'unsafe-inline' ${baseUrl}`,
|
|
151
|
+
`img-src data: blob: ${baseUrl}`,
|
|
152
|
+
`font-src data: ${baseUrl}`,
|
|
153
|
+
"media-src data: blob:",
|
|
154
|
+
"connect-src 'none'",
|
|
155
|
+
"object-src 'none'",
|
|
156
|
+
"base-uri 'none'",
|
|
157
|
+
"form-action 'none'",
|
|
158
|
+
].join('; ');
|
|
159
|
+
const bootstrap = `
|
|
160
|
+
Object.assign(window, ${JSON.stringify(runtimeConfig)});
|
|
161
|
+
window.__fileViewerDrawioSentinel = 0;
|
|
162
|
+
window.onDrawioViewerLoad = function () {
|
|
163
|
+
try {
|
|
164
|
+
GraphViewer.createViewerForElement(document.getElementById('graph'));
|
|
165
|
+
parent.postMessage({ fileViewerDrawio: ${JSON.stringify(channel)}, ok: true }, '*');
|
|
166
|
+
} catch (error) {
|
|
167
|
+
parent.postMessage({ fileViewerDrawio: ${JSON.stringify(channel)}, ok: false, message: String(error && error.message || error) }, '*');
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
`;
|
|
171
|
+
return `<!doctype html>
|
|
172
|
+
<html><head>
|
|
173
|
+
<meta charset="utf-8">
|
|
174
|
+
<meta http-equiv="Content-Security-Policy" content="${escapeHtmlAttribute(csp)}">
|
|
175
|
+
<meta name="referrer" content="no-referrer">
|
|
176
|
+
<style>html,body{margin:0;min-height:100%;background:#fff}#graph{max-width:100%;min-height:420px;border:1px solid transparent}</style>
|
|
177
|
+
<script nonce="${nonce}">${bootstrap}</script>
|
|
178
|
+
</head><body>
|
|
179
|
+
<div id="graph" class="mxgraph" data-mxgraph="${escapeHtmlAttribute(JSON.stringify(graphConfig))}"></div>
|
|
180
|
+
<script src="${escapeHtmlAttribute(scriptUrl.href)}"></script>
|
|
181
|
+
</body></html>`;
|
|
170
182
|
};
|
|
171
183
|
const runWithTimeout = async (task, timeout, message) => {
|
|
172
184
|
let timer;
|
|
@@ -507,19 +519,179 @@ const parseDrawioGeometry = (cell) => {
|
|
|
507
519
|
})),
|
|
508
520
|
};
|
|
509
521
|
};
|
|
510
|
-
const
|
|
522
|
+
const decodeDrawioEntities = (documentRef, value) => {
|
|
523
|
+
const Parser = documentRef.defaultView?.DOMParser || globalThis.DOMParser;
|
|
524
|
+
if (!Parser) {
|
|
525
|
+
return value;
|
|
526
|
+
}
|
|
527
|
+
// Escaping every literal '<' means document input can only become a text
|
|
528
|
+
// node in this inert parser. Named and numeric HTML entities still decode
|
|
529
|
+
// according to the browser's complete entity table.
|
|
530
|
+
const parsed = new Parser().parseFromString(value.replace(/</g, '<'), 'text/html');
|
|
531
|
+
return parsed.documentElement.textContent || '';
|
|
532
|
+
};
|
|
533
|
+
const stripDrawioMarkup = (value) => {
|
|
534
|
+
let output = '';
|
|
535
|
+
let cursor = 0;
|
|
536
|
+
while (cursor < value.length) {
|
|
537
|
+
if (value[cursor] !== '<') {
|
|
538
|
+
output += value[cursor];
|
|
539
|
+
cursor += 1;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
let quote = '';
|
|
543
|
+
let end = cursor + 1;
|
|
544
|
+
for (; end < value.length; end += 1) {
|
|
545
|
+
const character = value[end];
|
|
546
|
+
if (quote) {
|
|
547
|
+
if (character === quote) {
|
|
548
|
+
quote = '';
|
|
549
|
+
}
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (character === '"' || character === "'") {
|
|
553
|
+
quote = character;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (character === '>') {
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (end >= value.length) {
|
|
561
|
+
output += '<';
|
|
562
|
+
cursor += 1;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const tag = value.slice(cursor + 1, end).trim().toLowerCase();
|
|
566
|
+
if (/^\/?br(?:\s|\/|$)/.test(tag)) {
|
|
567
|
+
output += '\n';
|
|
568
|
+
}
|
|
569
|
+
cursor = end + 1;
|
|
570
|
+
}
|
|
571
|
+
return output;
|
|
572
|
+
};
|
|
573
|
+
export const normalizeDrawioText = (documentRef, value) => {
|
|
511
574
|
if (!value) {
|
|
512
575
|
return '';
|
|
513
576
|
}
|
|
514
|
-
|
|
515
|
-
helper.innerHTML = value;
|
|
516
|
-
return helper.value
|
|
517
|
-
.replace(/<br\s*\/?>/gi, '\n')
|
|
518
|
-
.replace(/<[^>]+>/g, '')
|
|
577
|
+
return stripDrawioMarkup(decodeDrawioEntities(documentRef, value))
|
|
519
578
|
.replace(/\u00a0/g, ' ')
|
|
520
579
|
.replace(/[ \t]+/g, ' ')
|
|
521
580
|
.trim();
|
|
522
581
|
};
|
|
582
|
+
const DRAWIO_FORBIDDEN_ELEMENTS = new Set([
|
|
583
|
+
'script',
|
|
584
|
+
'style',
|
|
585
|
+
'foreignobject',
|
|
586
|
+
'iframe',
|
|
587
|
+
'object',
|
|
588
|
+
'embed',
|
|
589
|
+
'form',
|
|
590
|
+
'link',
|
|
591
|
+
]);
|
|
592
|
+
const DRAWIO_RESOURCE_ATTRIBUTES = new Set([
|
|
593
|
+
'href',
|
|
594
|
+
'xlink:href',
|
|
595
|
+
'url',
|
|
596
|
+
'link',
|
|
597
|
+
'image',
|
|
598
|
+
'icon',
|
|
599
|
+
'src',
|
|
600
|
+
'srcset',
|
|
601
|
+
]);
|
|
602
|
+
const isControlCharacter = (character) => {
|
|
603
|
+
const codePoint = character.charCodeAt(0);
|
|
604
|
+
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
605
|
+
};
|
|
606
|
+
const stripControlCharacters = (value, replacement = '') => {
|
|
607
|
+
let normalized = '';
|
|
608
|
+
for (const character of value) {
|
|
609
|
+
normalized += isControlCharacter(character) ? replacement : character;
|
|
610
|
+
}
|
|
611
|
+
return normalized;
|
|
612
|
+
};
|
|
613
|
+
const containsDrawioActiveResource = (value) => {
|
|
614
|
+
const canonical = stripControlCharacters(value)
|
|
615
|
+
.replace(/\s+/g, '')
|
|
616
|
+
.toLowerCase();
|
|
617
|
+
return /(?:javascript|vbscript|data|file|blob|https?):|(?:^|[=("'])\/\/|\\\\/.test(canonical);
|
|
618
|
+
};
|
|
619
|
+
const sanitizeOfficialDrawioStyle = (value) => {
|
|
620
|
+
const safeEntries = [];
|
|
621
|
+
for (const [rawKey, rawValue] of parseDrawioStyle(value)) {
|
|
622
|
+
const key = rawKey.trim();
|
|
623
|
+
const styleValue = rawValue.trim();
|
|
624
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(key))
|
|
625
|
+
continue;
|
|
626
|
+
if (/(?:html|image|icon|link|href|url)/i.test(key))
|
|
627
|
+
continue;
|
|
628
|
+
if (containsDrawioActiveResource(styleValue))
|
|
629
|
+
continue;
|
|
630
|
+
if ([...styleValue].some(isControlCharacter) || /[<>"'`\\]/.test(styleValue) || /url\s*\(/i.test(styleValue))
|
|
631
|
+
continue;
|
|
632
|
+
safeEntries.push(`${key}=${styleValue}`);
|
|
633
|
+
}
|
|
634
|
+
safeEntries.push('html=0');
|
|
635
|
+
return `${safeEntries.join(';')};`;
|
|
636
|
+
};
|
|
637
|
+
const sanitizeOfficialDrawioText = (documentRef, value) => {
|
|
638
|
+
return normalizeDrawioText(documentRef, value)
|
|
639
|
+
.replace(/[<>&"'`]/g, ' ')
|
|
640
|
+
.split('')
|
|
641
|
+
.map(character => isControlCharacter(character) ? ' ' : character)
|
|
642
|
+
.join('')
|
|
643
|
+
.replace(/\s+/g, ' ')
|
|
644
|
+
.trim();
|
|
645
|
+
};
|
|
646
|
+
/**
|
|
647
|
+
* diagrams.net is a large third-party runtime. Its same-document renderer may
|
|
648
|
+
* interpret cell labels and style resource keys as HTML or URLs, so sanitize
|
|
649
|
+
* the direct mxGraphModel before it reaches that runtime. Compressed/opaque
|
|
650
|
+
* models fail closed here and use the inert fallback instead.
|
|
651
|
+
*/
|
|
652
|
+
export const sanitizeOfficialDrawioXml = (documentRef, value, invalidMessage = 'This Draw.io file has no safely renderable mxGraphModel.') => {
|
|
653
|
+
if (/<!\s*(?:doctype|entity)\b/i.test(value))
|
|
654
|
+
throw new Error(invalidMessage);
|
|
655
|
+
const Parser = documentRef.defaultView?.DOMParser || globalThis.DOMParser;
|
|
656
|
+
const Serializer = documentRef.defaultView?.XMLSerializer || globalThis.XMLSerializer;
|
|
657
|
+
if (!Parser || !Serializer)
|
|
658
|
+
throw new Error(invalidMessage);
|
|
659
|
+
const parsed = new Parser().parseFromString(value, 'application/xml');
|
|
660
|
+
if (parsed.querySelector('parsererror') || !parsed.querySelector('mxGraphModel')) {
|
|
661
|
+
throw new Error(invalidMessage);
|
|
662
|
+
}
|
|
663
|
+
for (const element of Array.from(parsed.querySelectorAll('*'))) {
|
|
664
|
+
const localName = element.localName.toLowerCase();
|
|
665
|
+
if (DRAWIO_FORBIDDEN_ELEMENTS.has(localName)) {
|
|
666
|
+
element.remove();
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
670
|
+
const name = attribute.name.toLowerCase();
|
|
671
|
+
if (name.startsWith('on') || DRAWIO_RESOURCE_ATTRIBUTES.has(name)) {
|
|
672
|
+
element.removeAttribute(attribute.name);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (name === 'style') {
|
|
676
|
+
element.setAttribute(attribute.name, sanitizeOfficialDrawioStyle(attribute.value));
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (name === 'value' || name === 'label' || name === 'tooltip' || name === 'title') {
|
|
680
|
+
element.setAttribute(attribute.name, sanitizeOfficialDrawioText(documentRef, attribute.value));
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (containsDrawioActiveResource(attribute.value)) {
|
|
684
|
+
element.removeAttribute(attribute.name);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const normalized = stripControlCharacters(attribute.value)
|
|
688
|
+
.replace(/[<>&"'`]/g, '')
|
|
689
|
+
.trim();
|
|
690
|
+
element.setAttribute(attribute.name, normalized);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return new Serializer().serializeToString(parsed);
|
|
694
|
+
};
|
|
523
695
|
const appendDrawioWrappedText = (documentRef, svg, text, x, y, width, height, fontSize, fill) => {
|
|
524
696
|
if (!text) {
|
|
525
697
|
return;
|
|
@@ -729,41 +901,57 @@ const renderDrawioFallback = (documentRef, text, target, t) => {
|
|
|
729
901
|
};
|
|
730
902
|
const renderOfficialDrawio = async (documentRef, text, target, scriptUrl, t) => {
|
|
731
903
|
const ownerWindow = documentRef.defaultView || (typeof window !== 'undefined' ? window : undefined);
|
|
732
|
-
|
|
733
|
-
await waitForFileViewerNextPaint(ownerWindow);
|
|
734
|
-
const host = createElement(documentRef, 'div', 'mxgraph drawing-mxgraph');
|
|
735
|
-
host.setAttribute('data-mxgraph', JSON.stringify({
|
|
736
|
-
xml: text,
|
|
737
|
-
toolbar: 'zoom layers lightbox',
|
|
738
|
-
nav: true,
|
|
739
|
-
resize: true,
|
|
740
|
-
'auto-fit': true,
|
|
741
|
-
'auto-crop': true,
|
|
742
|
-
'auto-origin': true,
|
|
743
|
-
'allow-zoom-in': true,
|
|
744
|
-
'allow-zoom-out': true,
|
|
745
|
-
border: 16,
|
|
746
|
-
highlight: '#0f766e',
|
|
747
|
-
}));
|
|
748
|
-
target.appendChild(host);
|
|
749
|
-
if (!ownerWindow?.GraphViewer) {
|
|
904
|
+
if (!ownerWindow) {
|
|
750
905
|
throw new Error(t('drawing.error.viewerInitFailed'));
|
|
751
906
|
}
|
|
752
|
-
|
|
907
|
+
const resolvedScriptUrl = resolveSafeDrawioViewerScript(documentRef, scriptUrl);
|
|
908
|
+
const sanitizedText = sanitizeOfficialDrawioXml(documentRef, text, t('drawing.error.drawioNoModel'));
|
|
909
|
+
const nonce = createDrawioSandboxNonce(ownerWindow);
|
|
910
|
+
const channel = `file-viewer-drawio-${nonce}`;
|
|
911
|
+
const frame = createElement(documentRef, 'iframe', 'drawing-mxgraph');
|
|
912
|
+
frame.title = 'diagrams.net preview';
|
|
913
|
+
frame.referrerPolicy = 'no-referrer';
|
|
914
|
+
frame.sandbox.add('allow-scripts');
|
|
915
|
+
frame.srcdoc = buildDrawioSandboxDocument(sanitizedText, resolvedScriptUrl, nonce, channel);
|
|
916
|
+
await new Promise((resolve, reject) => {
|
|
917
|
+
const timer = setTimeout(() => fail(t('drawing.error.drawioTimeout')), DRAWIO_OFFICIAL_TIMEOUT);
|
|
918
|
+
const cleanup = () => {
|
|
919
|
+
ownerWindow.removeEventListener('message', onMessage);
|
|
920
|
+
clearTimeout(timer);
|
|
921
|
+
};
|
|
922
|
+
const fail = (message) => {
|
|
923
|
+
cleanup();
|
|
924
|
+
reject(new Error(message));
|
|
925
|
+
};
|
|
926
|
+
const onMessage = (event) => {
|
|
927
|
+
if (event.source !== frame.contentWindow || event.data?.fileViewerDrawio !== channel)
|
|
928
|
+
return;
|
|
929
|
+
if (event.data?.ok === true) {
|
|
930
|
+
cleanup();
|
|
931
|
+
resolve();
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
fail(event.data?.message || t('drawing.error.viewerInitFailed'));
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
ownerWindow.addEventListener('message', onMessage);
|
|
938
|
+
frame.addEventListener('error', () => fail(t('drawing.error.viewerLoadFailed')), { once: true });
|
|
939
|
+
target.appendChild(frame);
|
|
940
|
+
});
|
|
753
941
|
markRendered(target, 'official');
|
|
754
942
|
};
|
|
755
943
|
const renderDrawio = async (documentRef, text, target, options, t) => {
|
|
756
|
-
|
|
944
|
+
const preferOfficial = options?.preferOfficial ?? isFileViewerDrawioOfficialViewerEnabled();
|
|
945
|
+
if (!preferOfficial) {
|
|
757
946
|
renderDrawioFallback(documentRef, text, target, t);
|
|
758
947
|
return;
|
|
759
948
|
}
|
|
760
949
|
const scriptUrl = resolveDrawingViewerScriptUrl(options, documentRef);
|
|
761
950
|
try {
|
|
762
|
-
await
|
|
951
|
+
await renderOfficialDrawio(documentRef, text, target, scriptUrl, t);
|
|
763
952
|
}
|
|
764
953
|
catch (error) {
|
|
765
954
|
console.warn(error);
|
|
766
|
-
deleteDiagramsViewerPromise(documentRef, scriptUrl);
|
|
767
955
|
delete target.dataset.drawingRendered;
|
|
768
956
|
target.replaceChildren();
|
|
769
957
|
renderDrawioFallback(documentRef, text, target, t);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FileRenderHandler, type FileViewerRenderedInstance, type FileViewerRendererPlugin, type RendererDefinition } from '@file-viewer/core';
|
|
2
|
+
export { disableFileViewerDrawioOfficialViewer, enableFileViewerDrawioOfficialViewer, isFileViewerDrawioOfficialViewerEnabled, } from './optionalCapabilities.js';
|
|
2
3
|
export declare const drawingRendererDefinition: RendererDefinition;
|
|
3
4
|
export declare const renderFileViewerDrawing: FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>;
|
|
4
5
|
export declare const drawingRenderer: FileViewerRendererPlugin<FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>>;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_RENDERER_DEFINITIONS, } from '@file-viewer/core';
|
|
2
|
+
export { disableFileViewerDrawioOfficialViewer, enableFileViewerDrawioOfficialViewer, isFileViewerDrawioOfficialViewerEnabled, } from './optionalCapabilities.js';
|
|
2
3
|
const drawingDefinition = DEFAULT_RENDERER_DEFINITIONS.find(definition => definition.id === 'drawing');
|
|
3
4
|
if (!drawingDefinition) {
|
|
4
5
|
throw new Error('@file-viewer/renderer-drawing could not locate the core drawing renderer definition.');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
let drawioOfficialViewerEnabled = false;
|
|
2
|
+
export const enableFileViewerDrawioOfficialViewer = () => {
|
|
3
|
+
drawioOfficialViewerEnabled = true;
|
|
4
|
+
};
|
|
5
|
+
export const disableFileViewerDrawioOfficialViewer = () => {
|
|
6
|
+
drawioOfficialViewerEnabled = false;
|
|
7
|
+
};
|
|
8
|
+
export const isFileViewerDrawioOfficialViewerEnabled = () => drawioOfficialViewerEnabled;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sanitizeDrawingSvg: (documentRef: Document, svg: string, invalidMessage?: string) => SVGSVGElement;
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import createDOMPurify from 'dompurify';
|
|
2
|
+
import { sanitizeFileViewerSvgResources } from '@file-viewer/core';
|
|
3
|
+
const purifierByDocument = new WeakMap();
|
|
4
|
+
const getPurifier = (documentRef) => {
|
|
5
|
+
const cached = purifierByDocument.get(documentRef);
|
|
6
|
+
if (cached)
|
|
7
|
+
return cached;
|
|
8
|
+
const windowRef = documentRef.defaultView;
|
|
9
|
+
if (!windowRef)
|
|
10
|
+
return null;
|
|
11
|
+
const purifier = createDOMPurify(windowRef);
|
|
12
|
+
if (!purifier.isSupported)
|
|
13
|
+
return null;
|
|
14
|
+
purifierByDocument.set(documentRef, purifier);
|
|
15
|
+
return purifier;
|
|
16
|
+
};
|
|
17
|
+
export const sanitizeDrawingSvg = (documentRef, svg, invalidMessage = 'Unable to parse SVG safely.') => {
|
|
18
|
+
const purifier = getPurifier(documentRef);
|
|
19
|
+
if (!purifier)
|
|
20
|
+
throw new Error(invalidMessage);
|
|
21
|
+
const fragment = purifier.sanitize(svg, {
|
|
22
|
+
RETURN_DOM_FRAGMENT: true,
|
|
23
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
24
|
+
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'foreignObject'],
|
|
25
|
+
FORBID_ATTR: ['srcdoc'],
|
|
26
|
+
});
|
|
27
|
+
sanitizeFileViewerSvgResources(fragment);
|
|
28
|
+
const root = fragment.querySelector('svg');
|
|
29
|
+
if (!root)
|
|
30
|
+
throw new Error(invalidMessage);
|
|
31
|
+
return documentRef.importNode(root, true);
|
|
32
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../ecosystem/capability-manifest.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"id": "drawing",
|
|
5
|
+
"packageName": "@file-viewer/renderer-drawing",
|
|
6
|
+
"rendererIds": ["drawing"],
|
|
7
|
+
"formats": ["excalidraw", "drawio", "dio", "mermaid", "mmd", "plantuml", "puml"],
|
|
8
|
+
"assets": { "rendererIds": [] },
|
|
9
|
+
"license": { "spdx": "Apache-2.0", "policy": "permissive" },
|
|
10
|
+
"weight": "heavy",
|
|
11
|
+
"profiles": ["all"]
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@file-viewer/renderer-drawing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone Draw.io, Excalidraw, Mermaid, and PlantUML renderer plugin for File Viewer with pan/zoom support.",
|
|
@@ -51,13 +51,18 @@
|
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"dist",
|
|
54
|
+
"file-viewer.capability.json",
|
|
54
55
|
"README.md",
|
|
55
56
|
"README.en.md",
|
|
56
57
|
"LICENSE"
|
|
57
58
|
],
|
|
59
|
+
"fileViewer": {
|
|
60
|
+
"capabilityManifest": "./file-viewer.capability.json"
|
|
61
|
+
},
|
|
58
62
|
"dependencies": {
|
|
59
|
-
"@file-viewer/core": "
|
|
63
|
+
"@file-viewer/core": "3.0.0",
|
|
60
64
|
"@panzoom/panzoom": "^4.6.2",
|
|
65
|
+
"dompurify": "3.4.13",
|
|
61
66
|
"mermaid": "^11.16.1",
|
|
62
67
|
"plantuml-encoder": "^1.4.0",
|
|
63
68
|
"roughjs": "^4.6.6"
|