@file-viewer/pptx
Advanced tools
| export declare const sanitizePptxCss: (documentRef: Document, cssText: string) => string; | ||
| export declare const sanitizePptxMarkup: (documentRef: Document, html: string) => DocumentFragment; |
+167
| import createDOMPurify from 'dompurify'; | ||
| const PPTX_CONTENT_STYLE_SCOPE = '.flyfish-pptx-content'; | ||
| const GENERATED_STYLE_RULE = /\s*(\._(?:css|svg_css|tbl_cell_css)_[A-Za-z0-9_-]+)\s*\{([^{}]*)\}/y; | ||
| const purifierByDocument = new WeakMap(); | ||
| const createSafeFallbackFragment = (documentRef) => { | ||
| const fragment = documentRef.createDocumentFragment(); | ||
| const fallback = documentRef.createElement('section'); | ||
| fallback.className = 'slide flyfish-pptx-slide-error'; | ||
| fallback.textContent = 'This slide could not be displayed safely.'; | ||
| fragment.append(fallback); | ||
| return fragment; | ||
| }; | ||
| const getPurifier = (documentRef) => { | ||
| const cached = purifierByDocument.get(documentRef); | ||
| if (cached) { | ||
| return cached; | ||
| } | ||
| const windowRef = documentRef.defaultView; | ||
| if (!windowRef) { | ||
| return null; | ||
| } | ||
| const purifier = createDOMPurify(windowRef); | ||
| if (!purifier.isSupported) { | ||
| return null; | ||
| } | ||
| purifierByDocument.set(documentRef, purifier); | ||
| return purifier; | ||
| }; | ||
| const isAllowedCssUrl = (value) => { | ||
| const target = value | ||
| .trim() | ||
| .replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2') | ||
| .trim(); | ||
| if (/^#[A-Za-z0-9_.:-]+$/.test(target)) { | ||
| return true; | ||
| } | ||
| return /^data:image\/[A-Za-z0-9.+-]+;base64,[A-Za-z0-9+/=\s]+$/i.test(target); | ||
| }; | ||
| const isSafeEmbeddedResourceUrl = (value, allowFragment) => { | ||
| const normalized = value.trim(); | ||
| if (allowFragment && /^#[A-Za-z0-9_.:-]+$/.test(normalized)) { | ||
| return true; | ||
| } | ||
| if (/^blob:/i.test(normalized)) { | ||
| return true; | ||
| } | ||
| return /^data:image\/[A-Za-z0-9.+-]+;base64,[A-Za-z0-9+/=\s]+$/i.test(normalized); | ||
| }; | ||
| const isSafeCssValue = (value) => { | ||
| const normalized = value.replace(/\/\*[\s\S]*?\*\//g, '').trim(); | ||
| if (/(?:expression|image(?:-set)?|paint|var)\s*\(/i.test(normalized) || | ||
| /(?:javascript|vbscript)\s*:/i.test(normalized) || | ||
| /(?:^|[^-])behavior\s*:/i.test(normalized) || | ||
| /-moz-binding/i.test(normalized) || | ||
| normalized.includes('\\')) { | ||
| return false; | ||
| } | ||
| let unsafeUrl = false; | ||
| const withoutUrls = normalized.replace(/url\(\s*([^)]*?)\s*\)/gi, (_match, target) => { | ||
| if (!isAllowedCssUrl(target)) { | ||
| unsafeUrl = true; | ||
| } | ||
| return ''; | ||
| }); | ||
| return !unsafeUrl && !/url\s*\(/i.test(withoutUrls); | ||
| }; | ||
| const sanitizeStyleDeclaration = (documentRef, cssText) => { | ||
| const probe = documentRef.createElement('span'); | ||
| probe.setAttribute('style', cssText); | ||
| const style = probe.style; | ||
| const propertyNames = Array.from({ length: style.length }, (_, index) => style.item(index)); | ||
| for (const propertyName of propertyNames) { | ||
| const value = style.getPropertyValue(propertyName); | ||
| if (!propertyName || | ||
| propertyName.startsWith('--') || | ||
| propertyName === 'behavior' || | ||
| propertyName === '-moz-binding' || | ||
| (propertyName === 'position' && /^(?:fixed|sticky)$/i.test(value.trim())) || | ||
| !isSafeCssValue(value)) { | ||
| style.removeProperty(propertyName); | ||
| } | ||
| } | ||
| return style.cssText; | ||
| }; | ||
| const sanitizeStyleAttributes = (documentRef, root) => { | ||
| root.querySelectorAll('[style]').forEach((element) => { | ||
| const sanitized = sanitizeStyleDeclaration(documentRef, element.getAttribute('style') || ''); | ||
| if (sanitized) { | ||
| element.setAttribute('style', sanitized); | ||
| } | ||
| else { | ||
| element.removeAttribute('style'); | ||
| } | ||
| }); | ||
| }; | ||
| const sanitizeEmbeddedResourceUrls = (root) => { | ||
| root | ||
| .querySelectorAll('[src],[srcset],[poster],[href],[xlink\\:href]') | ||
| .forEach((element) => { | ||
| for (const attributeName of ['src', 'srcset', 'poster', 'href', 'xlink:href']) { | ||
| if (!element.hasAttribute(attributeName)) { | ||
| continue; | ||
| } | ||
| const isHtmlAnchor = element.namespaceURI === 'http://www.w3.org/1999/xhtml' && element.localName === 'a'; | ||
| if (isHtmlAnchor && attributeName === 'href') { | ||
| continue; | ||
| } | ||
| const allowFragment = element.namespaceURI === 'http://www.w3.org/2000/svg' && | ||
| (attributeName === 'href' || attributeName === 'xlink:href'); | ||
| if (!isSafeEmbeddedResourceUrl(element.getAttribute(attributeName) || '', allowFragment)) { | ||
| element.removeAttribute(attributeName); | ||
| } | ||
| } | ||
| }); | ||
| }; | ||
| const sanitizeSvgUrlAttributes = (root) => { | ||
| root.querySelectorAll('svg, svg *').forEach((element) => { | ||
| for (const attribute of Array.from(element.attributes)) { | ||
| const value = attribute.value; | ||
| if (/url\s*\(/i.test(value) && !isSafeCssValue(value)) { | ||
| element.removeAttribute(attribute.name); | ||
| } | ||
| } | ||
| }); | ||
| }; | ||
| export const sanitizePptxCss = (documentRef, cssText) => { | ||
| const rules = []; | ||
| let offset = 0; | ||
| while (offset < cssText.length) { | ||
| if (!cssText.slice(offset).trim()) { | ||
| break; | ||
| } | ||
| GENERATED_STYLE_RULE.lastIndex = offset; | ||
| const match = GENERATED_STYLE_RULE.exec(cssText); | ||
| if (!match) { | ||
| return ''; | ||
| } | ||
| const declarations = sanitizeStyleDeclaration(documentRef, match[2]); | ||
| if (declarations) { | ||
| rules.push(`${PPTX_CONTENT_STYLE_SCOPE} ${match[1]}{${declarations}}`); | ||
| } | ||
| offset = GENERATED_STYLE_RULE.lastIndex; | ||
| } | ||
| return rules.join('\n'); | ||
| }; | ||
| export const sanitizePptxMarkup = (documentRef, html) => { | ||
| const purifier = getPurifier(documentRef); | ||
| if (!purifier) { | ||
| return createSafeFallbackFragment(documentRef); | ||
| } | ||
| const fragment = purifier.sanitize(html, { | ||
| RETURN_DOM_FRAGMENT: true, | ||
| USE_PROFILES: { html: true, svg: true, svgFilters: true }, | ||
| ADD_ATTR: ['target'], | ||
| FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'], | ||
| FORBID_ATTR: ['srcdoc'] | ||
| }); | ||
| sanitizeStyleAttributes(documentRef, fragment); | ||
| sanitizeEmbeddedResourceUrls(fragment); | ||
| sanitizeSvgUrlAttributes(fragment); | ||
| fragment.querySelectorAll('a[target]').forEach((anchor) => { | ||
| if ((anchor.getAttribute('target') || '').trim().toLowerCase() === '_blank') { | ||
| anchor.rel = 'noopener noreferrer'; | ||
| } | ||
| }); | ||
| return fragment; | ||
| }; |
+8
-7
| import { renderPptxPostProcessing } from './chart.js'; | ||
| import { resolvePptxEngineOptions, RECOMMENDED_ZIP_LIMITS } from './options.js'; | ||
| import { ensurePptxViewerStyles, scopePptxContentStyleText } from './styles.js'; | ||
| import { sanitizePptxCss, sanitizePptxMarkup } from './sanitize.js'; | ||
| import { ensurePptxViewerStyles } from './styles.js'; | ||
| import { createPptxWorker } from './worker.js'; | ||
@@ -48,6 +49,5 @@ const clamp = (value, min, max) => { | ||
| const appendHtml = (container, html) => { | ||
| const template = container.ownerDocument.createElement('template'); | ||
| template.innerHTML = html; | ||
| const nodes = Array.from(template.content.children); | ||
| container.append(template.content); | ||
| const fragment = sanitizePptxMarkup(container.ownerDocument, html); | ||
| const nodes = Array.from(fragment.children); | ||
| container.append(fragment); | ||
| return nodes[0] || null; | ||
@@ -261,7 +261,8 @@ }; | ||
| appendGlobalCss(css) { | ||
| if (!css) { | ||
| const sanitized = sanitizePptxCss(this.target.ownerDocument, css); | ||
| if (!sanitized) { | ||
| return; | ||
| } | ||
| const style = this.target.ownerDocument.createElement('style'); | ||
| style.textContent = scopePptxContentStyleText(css); | ||
| style.textContent = sanitized; | ||
| this.content.append(style); | ||
@@ -268,0 +269,0 @@ } |
+2
-1
| { | ||
| "name": "@file-viewer/pptx", | ||
| "version": "2.2.8", | ||
| "version": "2.2.9", | ||
| "private": false, | ||
@@ -66,2 +66,3 @@ "type": "module", | ||
| "dingbat-to-unicode": "^1.0.1", | ||
| "dompurify": "^3.4.13", | ||
| "jszip": "^3.10.1", | ||
@@ -68,0 +69,0 @@ "tinycolor2": "^1.6.0", |
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
661691
1.1%22
10%4013
4.48%7
16.67%4
33.33%+ Added
+ Added
+ Added