@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 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 sanitizeSvg(documentRef, rendered.svg, t);
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 sanitizeSvg(documentRef, await response.text(), t);
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 global {
3
- interface Window {
4
- GraphViewer?: {
5
- createViewerForElement: (element: HTMLElement, callback?: (viewer: unknown) => void) => unknown;
6
- processElements: (className?: string) => void;
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, waitForFileViewerNextPaint, unregisterFileViewerZoomProvider, } from '@file-viewer/core';
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 configureOfflineDiagramsViewerAssets = (documentRef, scriptUrl) => {
90
- const ownerWindow = documentRef.defaultView || (typeof window !== 'undefined' ? window : undefined);
91
- if (!ownerWindow) {
92
- return;
89
+ const escapeHtmlAttribute = (value) => value
90
+ .replace(/&/g, '&amp;')
91
+ .replace(/"/g, '&quot;')
92
+ .replace(/</g, '&lt;')
93
+ .replace(/>/g, '&gt;');
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
- const viewerWindow = ownerWindow;
95
- const baseUrl = resolveDirectoryUrl(scriptUrl);
96
- const setDefault = (key, value) => {
97
- if (viewerWindow[key] === undefined || viewerWindow[key] === '') {
98
- viewerWindow[key] = value;
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
- // viewer-static.min.js still contains public diagrams.net defaults. Setting
102
- // these before the script executes keeps every secondary asset on the same
103
- // self-hosted directory as the viewer script.
104
- setDefault('PROXY_URL', `${baseUrl}proxy`);
105
- setDefault('STYLE_PATH', `${baseUrl}styles`);
106
- setDefault('SHAPES_PATH', `${baseUrl}shapes`);
107
- setDefault('STENCIL_PATH', `${baseUrl}stencils`);
108
- setDefault('DRAW_MATH_URL', `${baseUrl}math4/es5`);
109
- setDefault('GRAPH_IMAGE_PATH', `${baseUrl}img`);
110
- setDefault('mxImageBasePath', `${baseUrl}mxgraph/images`);
111
- setDefault('mxBasePath', `${baseUrl}mxgraph/`);
112
- setDefault('mxLoadStylesheets', false);
113
- setDefault('DRAWIO_BASE_URL', baseUrl.replace(/\/$/, ''));
114
- setDefault('DRAWIO_LIGHTBOX_URL', baseUrl.replace(/\/$/, ''));
115
- setDefault('DRAWIO_SERVER_URL', baseUrl);
116
- setDefault('DRAWIO_VIEWER_URL', `${baseUrl}viewer-static.min.js`);
117
- setDefault('DRAWIO_LOG_URL', '');
118
- setDefault('EXPORT_URL', `${baseUrl}export`);
119
- setDefault('PLANT_URL', `${baseUrl}plant`);
120
- setDefault('VSS_CONVERT_URL', `${baseUrl}VsdConverter/api/converter`);
121
- setDefault('DRAWIO_GITLAB_URL', baseUrl);
122
- setDefault('DRAWIO_GITHUB_URL', baseUrl);
123
- setDefault('DRAWIO_GITHUB_API_URL', baseUrl);
124
- setDefault('RT_WEBSOCKET_URL', `${baseUrl}rt`);
125
- setDefault('NOTIFICATIONS_URL', `${baseUrl}notifications`);
126
- };
127
- const getDiagramsViewerPromiseMap = (documentRef) => {
128
- let promiseMap = diagramsViewerPromises.get(documentRef);
129
- if (!promiseMap) {
130
- promiseMap = new Map();
131
- diagramsViewerPromises.set(documentRef, promiseMap);
132
- }
133
- return promiseMap;
134
- };
135
- const deleteDiagramsViewerPromise = (documentRef, scriptUrl) => {
136
- const promiseMap = diagramsViewerPromises.get(documentRef);
137
- promiseMap?.delete(scriptUrl);
138
- if (promiseMap && promiseMap.size === 0) {
139
- diagramsViewerPromises.delete(documentRef);
140
- }
141
- };
142
- const loadDiagramsViewer = (documentRef, scriptUrl, t) => {
143
- const ownerWindow = documentRef.defaultView || (typeof window !== 'undefined' ? window : undefined);
144
- if (ownerWindow?.GraphViewer) {
145
- return Promise.resolve();
146
- }
147
- configureOfflineDiagramsViewerAssets(documentRef, scriptUrl);
148
- const promiseMap = getDiagramsViewerPromiseMap(documentRef);
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 normalizeDrawioText = (documentRef, value) => {
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, '&lt;'), '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
- const helper = documentRef.createElement('textarea');
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
- await loadDiagramsViewer(documentRef, scriptUrl, t);
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
- ownerWindow.GraphViewer.createViewerForElement(host);
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
- if (options?.preferOfficial === false) {
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 runWithTimeout(renderOfficialDrawio(documentRef, text, target, scriptUrl, t), DRAWIO_OFFICIAL_TIMEOUT, t('drawing.error.drawioTimeout'));
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,3 @@
1
+ export declare const enableFileViewerDrawioOfficialViewer: () => void;
2
+ export declare const disableFileViewerDrawioOfficialViewer: () => void;
3
+ export declare const isFileViewerDrawioOfficialViewerEnabled: () => boolean;
@@ -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;
@@ -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": "2.4.0",
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": "2.4.0",
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"