@file-viewer/pptx 2.2.7 → 2.2.9
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/sanitize.d.ts +2 -0
- package/dist/sanitize.js +167 -0
- package/dist/viewer.js +8 -7
- package/dist/worker/pptx.worker.js +6 -6
- package/package.json +2 -1
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import createDOMPurify from 'dompurify';
|
|
2
|
+
const PPTX_CONTENT_STYLE_SCOPE = '.flyfish-pptx-content';
|
|
3
|
+
const GENERATED_STYLE_RULE = /\s*(\._(?:css|svg_css|tbl_cell_css)_[A-Za-z0-9_-]+)\s*\{([^{}]*)\}/y;
|
|
4
|
+
const purifierByDocument = new WeakMap();
|
|
5
|
+
const createSafeFallbackFragment = (documentRef) => {
|
|
6
|
+
const fragment = documentRef.createDocumentFragment();
|
|
7
|
+
const fallback = documentRef.createElement('section');
|
|
8
|
+
fallback.className = 'slide flyfish-pptx-slide-error';
|
|
9
|
+
fallback.textContent = 'This slide could not be displayed safely.';
|
|
10
|
+
fragment.append(fallback);
|
|
11
|
+
return fragment;
|
|
12
|
+
};
|
|
13
|
+
const getPurifier = (documentRef) => {
|
|
14
|
+
const cached = purifierByDocument.get(documentRef);
|
|
15
|
+
if (cached) {
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
18
|
+
const windowRef = documentRef.defaultView;
|
|
19
|
+
if (!windowRef) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const purifier = createDOMPurify(windowRef);
|
|
23
|
+
if (!purifier.isSupported) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
purifierByDocument.set(documentRef, purifier);
|
|
27
|
+
return purifier;
|
|
28
|
+
};
|
|
29
|
+
const isAllowedCssUrl = (value) => {
|
|
30
|
+
const target = value
|
|
31
|
+
.trim()
|
|
32
|
+
.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2')
|
|
33
|
+
.trim();
|
|
34
|
+
if (/^#[A-Za-z0-9_.:-]+$/.test(target)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return /^data:image\/[A-Za-z0-9.+-]+;base64,[A-Za-z0-9+/=\s]+$/i.test(target);
|
|
38
|
+
};
|
|
39
|
+
const isSafeEmbeddedResourceUrl = (value, allowFragment) => {
|
|
40
|
+
const normalized = value.trim();
|
|
41
|
+
if (allowFragment && /^#[A-Za-z0-9_.:-]+$/.test(normalized)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (/^blob:/i.test(normalized)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return /^data:image\/[A-Za-z0-9.+-]+;base64,[A-Za-z0-9+/=\s]+$/i.test(normalized);
|
|
48
|
+
};
|
|
49
|
+
const isSafeCssValue = (value) => {
|
|
50
|
+
const normalized = value.replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
|
51
|
+
if (/(?:expression|image(?:-set)?|paint|var)\s*\(/i.test(normalized) ||
|
|
52
|
+
/(?:javascript|vbscript)\s*:/i.test(normalized) ||
|
|
53
|
+
/(?:^|[^-])behavior\s*:/i.test(normalized) ||
|
|
54
|
+
/-moz-binding/i.test(normalized) ||
|
|
55
|
+
normalized.includes('\\')) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
let unsafeUrl = false;
|
|
59
|
+
const withoutUrls = normalized.replace(/url\(\s*([^)]*?)\s*\)/gi, (_match, target) => {
|
|
60
|
+
if (!isAllowedCssUrl(target)) {
|
|
61
|
+
unsafeUrl = true;
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
});
|
|
65
|
+
return !unsafeUrl && !/url\s*\(/i.test(withoutUrls);
|
|
66
|
+
};
|
|
67
|
+
const sanitizeStyleDeclaration = (documentRef, cssText) => {
|
|
68
|
+
const probe = documentRef.createElement('span');
|
|
69
|
+
probe.setAttribute('style', cssText);
|
|
70
|
+
const style = probe.style;
|
|
71
|
+
const propertyNames = Array.from({ length: style.length }, (_, index) => style.item(index));
|
|
72
|
+
for (const propertyName of propertyNames) {
|
|
73
|
+
const value = style.getPropertyValue(propertyName);
|
|
74
|
+
if (!propertyName ||
|
|
75
|
+
propertyName.startsWith('--') ||
|
|
76
|
+
propertyName === 'behavior' ||
|
|
77
|
+
propertyName === '-moz-binding' ||
|
|
78
|
+
(propertyName === 'position' && /^(?:fixed|sticky)$/i.test(value.trim())) ||
|
|
79
|
+
!isSafeCssValue(value)) {
|
|
80
|
+
style.removeProperty(propertyName);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return style.cssText;
|
|
84
|
+
};
|
|
85
|
+
const sanitizeStyleAttributes = (documentRef, root) => {
|
|
86
|
+
root.querySelectorAll('[style]').forEach((element) => {
|
|
87
|
+
const sanitized = sanitizeStyleDeclaration(documentRef, element.getAttribute('style') || '');
|
|
88
|
+
if (sanitized) {
|
|
89
|
+
element.setAttribute('style', sanitized);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
element.removeAttribute('style');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const sanitizeEmbeddedResourceUrls = (root) => {
|
|
97
|
+
root
|
|
98
|
+
.querySelectorAll('[src],[srcset],[poster],[href],[xlink\\:href]')
|
|
99
|
+
.forEach((element) => {
|
|
100
|
+
for (const attributeName of ['src', 'srcset', 'poster', 'href', 'xlink:href']) {
|
|
101
|
+
if (!element.hasAttribute(attributeName)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const isHtmlAnchor = element.namespaceURI === 'http://www.w3.org/1999/xhtml' && element.localName === 'a';
|
|
105
|
+
if (isHtmlAnchor && attributeName === 'href') {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const allowFragment = element.namespaceURI === 'http://www.w3.org/2000/svg' &&
|
|
109
|
+
(attributeName === 'href' || attributeName === 'xlink:href');
|
|
110
|
+
if (!isSafeEmbeddedResourceUrl(element.getAttribute(attributeName) || '', allowFragment)) {
|
|
111
|
+
element.removeAttribute(attributeName);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const sanitizeSvgUrlAttributes = (root) => {
|
|
117
|
+
root.querySelectorAll('svg, svg *').forEach((element) => {
|
|
118
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
119
|
+
const value = attribute.value;
|
|
120
|
+
if (/url\s*\(/i.test(value) && !isSafeCssValue(value)) {
|
|
121
|
+
element.removeAttribute(attribute.name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
export const sanitizePptxCss = (documentRef, cssText) => {
|
|
127
|
+
const rules = [];
|
|
128
|
+
let offset = 0;
|
|
129
|
+
while (offset < cssText.length) {
|
|
130
|
+
if (!cssText.slice(offset).trim()) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
GENERATED_STYLE_RULE.lastIndex = offset;
|
|
134
|
+
const match = GENERATED_STYLE_RULE.exec(cssText);
|
|
135
|
+
if (!match) {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
const declarations = sanitizeStyleDeclaration(documentRef, match[2]);
|
|
139
|
+
if (declarations) {
|
|
140
|
+
rules.push(`${PPTX_CONTENT_STYLE_SCOPE} ${match[1]}{${declarations}}`);
|
|
141
|
+
}
|
|
142
|
+
offset = GENERATED_STYLE_RULE.lastIndex;
|
|
143
|
+
}
|
|
144
|
+
return rules.join('\n');
|
|
145
|
+
};
|
|
146
|
+
export const sanitizePptxMarkup = (documentRef, html) => {
|
|
147
|
+
const purifier = getPurifier(documentRef);
|
|
148
|
+
if (!purifier) {
|
|
149
|
+
return createSafeFallbackFragment(documentRef);
|
|
150
|
+
}
|
|
151
|
+
const fragment = purifier.sanitize(html, {
|
|
152
|
+
RETURN_DOM_FRAGMENT: true,
|
|
153
|
+
USE_PROFILES: { html: true, svg: true, svgFilters: true },
|
|
154
|
+
ADD_ATTR: ['target'],
|
|
155
|
+
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'],
|
|
156
|
+
FORBID_ATTR: ['srcdoc']
|
|
157
|
+
});
|
|
158
|
+
sanitizeStyleAttributes(documentRef, fragment);
|
|
159
|
+
sanitizeEmbeddedResourceUrls(fragment);
|
|
160
|
+
sanitizeSvgUrlAttributes(fragment);
|
|
161
|
+
fragment.querySelectorAll('a[target]').forEach((anchor) => {
|
|
162
|
+
if ((anchor.getAttribute('target') || '').trim().toLowerCase() === '_blank') {
|
|
163
|
+
anchor.rel = 'noopener noreferrer';
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return fragment;
|
|
167
|
+
};
|
package/dist/viewer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { renderPptxPostProcessing } from './chart.js';
|
|
2
2
|
import { resolvePptxEngineOptions, RECOMMENDED_ZIP_LIMITS } from './options.js';
|
|
3
|
-
import {
|
|
3
|
+
import { sanitizePptxCss, sanitizePptxMarkup } from './sanitize.js';
|
|
4
|
+
import { ensurePptxViewerStyles } from './styles.js';
|
|
4
5
|
import { createPptxWorker } from './worker.js';
|
|
5
6
|
const clamp = (value, min, max) => {
|
|
6
7
|
return Math.min(max, Math.max(min, value));
|
|
@@ -45,10 +46,9 @@ const ensureZipWithinLimits = (buffer, options) => {
|
|
|
45
46
|
}
|
|
46
47
|
};
|
|
47
48
|
const appendHtml = (container, html) => {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
container.append(template.content);
|
|
49
|
+
const fragment = sanitizePptxMarkup(container.ownerDocument, html);
|
|
50
|
+
const nodes = Array.from(fragment.children);
|
|
51
|
+
container.append(fragment);
|
|
52
52
|
return nodes[0] || null;
|
|
53
53
|
};
|
|
54
54
|
const escapeHtml = (value) => String(value !== null && value !== void 0 ? value : '')
|
|
@@ -257,11 +257,12 @@ export class PptxViewer {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
appendGlobalCss(css) {
|
|
260
|
-
|
|
260
|
+
const sanitized = sanitizePptxCss(this.target.ownerDocument, css);
|
|
261
|
+
if (!sanitized) {
|
|
261
262
|
return;
|
|
262
263
|
}
|
|
263
264
|
const style = this.target.ownerDocument.createElement('style');
|
|
264
|
-
style.textContent =
|
|
265
|
+
style.textContent = sanitized;
|
|
265
266
|
this.content.append(style);
|
|
266
267
|
}
|
|
267
268
|
showThumbnail(base64Jpeg) {
|