@file-viewer/pptx 2.2.8 → 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.
@@ -0,0 +1,2 @@
1
+ export declare const sanitizePptxCss: (documentRef: Document, cssText: string) => string;
2
+ export declare const sanitizePptxMarkup: (documentRef: Document, html: string) => DocumentFragment;
@@ -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 { ensurePptxViewerStyles, scopePptxContentStyleText } from './styles.js';
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 template = container.ownerDocument.createElement('template');
49
- template.innerHTML = html;
50
- const nodes = Array.from(template.content.children);
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
- if (!css) {
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 = scopePptxContentStyleText(css);
265
+ style.textContent = sanitized;
265
266
  this.content.append(style);
266
267
  }
267
268
  showThumbnail(base64Jpeg) {