UNPKG

tldraw

Version:

A tiny little drawing editor.

600 lines (599 loc) 13.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var sanitizeSvg_exports = {}; __export(sanitizeSvg_exports, { sanitizeSvg: () => sanitizeSvg }); module.exports = __toCommonJS(sanitizeSvg_exports); /*! * SVG/attribute allowlists and URI sanitization approach derived from DOMPurify. * DOMPurify — MIT License, Copyright (c) 2015 Mario Heiderich * https://github.com/cure53/DOMPurify/blob/main/LICENSE */ const ALLOWED_SVG_TAGS = /* @__PURE__ */ new Set([ "svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animate", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "fedropshadow", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "feimage", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "foreignobject", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "set", "stop", "style", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern" ]); const ALLOWED_HTML_TAGS = /* @__PURE__ */ new Set([ "a", "b", "blockquote", "body", "br", "code", "del", "div", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "li", "mark", "ol", "p", "pre", "span", "strong", "s", "sub", "sup", "table", "tbody", "td", "th", "thead", "tr", "u", "ul" ]); const BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([ "script", "iframe", "object", "embed", "form", "input", "textarea", "select", "button", "link", "meta", "base", "img", // onerror vector "video", "audio", "source", "picture", "svg" // no nested SVG inside foreignObject ]); const ALLOWED_SVG_ATTRS = /* @__PURE__ */ new Set([ "accent-height", "accumulate", "additive", "alignment-baseline", "amplitude", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clip-path", "clip-rule", "clippathunits", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "diffuseconstant", "direction", "display", "divisor", "dominant-baseline", "dur", "dx", "dy", "edgemode", "elevation", "end", "exponent", "fill", "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "from", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradienttransform", "gradientunits", "height", "href", "id", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kerning", "kernelmatrix", "kernelunitlength", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "mask", "mask-type", "maskcontentunits", "maskunits", "max", "media", "method", "min", "mode", "name", "numoctaves", "offset", "opacity", "operator", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "pointer-events", "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "radius", "refx", "refy", "repeatcount", "repeatdur", "requiredfeatures", "restart", "result", "role", "rotate", "rx", "ry", "scale", "seed", "shape-rendering", "slope", "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "surfacescale", "systemlanguage", "tabindex", "tablevalues", "targetx", "targety", "text-anchor", "text-decoration", "text-rendering", "textlength", "to", "transform", "transform-origin", "type", "u1", "u2", "unicode", "values", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "viewbox", "visibility", "width", "word-spacing", "wrap", "writing-mode", "x", "x1", "x2", "xchannelselector", "xlink:href", "xml:id", "xml:space", "xlink:title", "xmlns", "xmlns:xlink", "y", "y1", "y2", "z", "zoomandpan" ]); const ALLOWED_HTML_ATTRS = /* @__PURE__ */ new Set([ "class", "dir", "href", // only on <a> "id", "lang", "role", "style", "tabindex", "title" ]); const DATA_ATTR_PATTERN = /^data-/; const ARIA_ATTR_PATTERN = /^aria-/; const INVISIBLE_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; const SAFE_LINK_PROTOCOLS = /^(?:https?:|mailto:)/i; const DATA_URI = /^data:/i; const RASTER_DATA_URI = /^data:image\/(?:png|jpeg|jpg|gif|webp|avif|bmp|tiff|x-icon|vnd\.microsoft\.icon)[;,]/i; const SVG_DATA_URI = /^data:image\/svg\+xml[;,]/i; const FRAGMENT_REF = /^#/; function decodeCssEscapes(css) { return css.replace(/\\([0-9a-fA-F]{1,6})\s?|\\([^\n])/g, (_, hex, literal) => { if (hex) { const codePoint = parseInt(hex, 16); if (codePoint > 1114111 || codePoint === 0) return "\uFFFD"; return String.fromCodePoint(codePoint); } return literal; }); } const SAFE_CSS_DATA_MIME = /^data:(?:image\/(?:png|jpeg|jpg|gif|webp|avif)|font\/(?:woff2?|opentype|truetype|sfnt)|application\/(?:x-font-woff|font-woff2?|x-font-ttf|x-font-opentype|font-sfnt))[;,]/i; function sanitizeCssValue(css) { let decoded = decodeCssEscapes(css); decoded = decoded.replace( /@import\s+(?:url\s*\([^)]*\)|"[^"]*"|'[^']*')[^;]*;?|@import\b[^;]*;?/gi, "" ); decoded = decoded.replace(/expression\s*\([^)]*\)/gi, ""); decoded = decoded.replace(/-moz-binding\s*:[^;]*/gi, ""); decoded = decoded.replace(/behavior\s*:[^;]*/gi, ""); decoded = decoded.replace(/url\s*\(\s*(['"]?)(.*?)\1\s*\)/gis, (match, _quote, uri) => { const stripped = uri.replace(INVISIBLE_WHITESPACE, ""); if (SAFE_CSS_DATA_MIME.test(stripped) || FRAGMENT_REF.test(stripped)) { return match; } return ""; }); return decoded; } function sanitizeStyleElement(textContent) { return sanitizeCssValue(textContent); } const ANIMATION_TAGS = /* @__PURE__ */ new Set(["animate", "set", "animatecolor", "animatetransform"]); const DANGEROUS_ANIMATION_TARGETS = /^(?:href|xlink:href|on)/i; function isAnimationDangerous(el) { const attrName = el.getAttribute("attributeName"); if (!attrName) return false; return DANGEROUS_ANIMATION_TARGETS.test(attrName.replace(INVISIBLE_WHITESPACE, "")); } const EVENT_HANDLER_PATTERN = /^on/i; const URL_BEARING_SVG_ATTRS = /* @__PURE__ */ new Set([ "clip-path", "cursor", "fill", "filter", "marker-end", "marker-mid", "marker-start", "mask", "stroke" ]); const MAX_EMBED_DEPTH = 10; function decodeDataUri(value) { const base64Idx = value.search(/;base64,/i); if (base64Idx >= 0) { const base64 = value.slice(base64Idx + 8); const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); return new TextDecoder().decode(bytes); } const commaIdx = value.indexOf(","); if (commaIdx < 0) return null; return decodeURIComponent(value.slice(commaIdx + 1)); } function encodeAsSvgDataUri(svgText) { const bytes = new TextEncoder().encode(svgText); const binaryStr = Array.from(bytes, (b) => String.fromCharCode(b)).join(""); return "data:image/svg+xml;base64," + btoa(binaryStr); } function sanitizeEmbeddedSvgDataUri(value, depth) { if (depth >= MAX_EMBED_DEPTH) { console.warn(`Embedded SVG data URI recursion depth limit (${MAX_EMBED_DEPTH}) reached`); return null; } let svgText; try { const decoded = decodeDataUri(value); if (decoded === null) return null; svgText = decoded; } catch { return null; } const sanitized = sanitizeSvgInner(svgText, depth + 1); if (!sanitized) return null; return encodeAsSvgDataUri(sanitized); } function sanitizeUri(el, attrName, value, depth) { const stripped = value.replace(INVISIBLE_WHITESPACE, ""); const tagName = el.tagName.toLowerCase(); if (tagName === "image" || tagName === "feimage") { if (RASTER_DATA_URI.test(stripped)) return value; if (SVG_DATA_URI.test(stripped)) return sanitizeEmbeddedSvgDataUri(stripped, depth); return null; } if (tagName === "use") { if (FRAGMENT_REF.test(stripped)) return value; return null; } if (tagName === "a") { if (SAFE_LINK_PROTOCOLS.test(stripped)) return value; return null; } if (DATA_URI.test(stripped) || FRAGMENT_REF.test(stripped)) return value; return null; } function sanitizeSvgAttributes(el, depth) { for (let i = el.attributes.length - 1; i >= 0; i--) { const attr = el.attributes[i]; const name = attr.name.toLowerCase(); const normalized = name.replace(INVISIBLE_WHITESPACE, ""); if (EVENT_HANDLER_PATTERN.test(normalized)) { el.removeAttribute(attr.name); continue; } if (DATA_ATTR_PATTERN.test(name) || ARIA_ATTR_PATTERN.test(name)) { continue; } if (!ALLOWED_SVG_ATTRS.has(name)) { el.removeAttribute(attr.name); continue; } if (name === "href" || name === "xlink:href") { const sanitized = sanitizeUri(el, name, attr.value, depth); if (sanitized === null) { el.removeAttribute(attr.name); } else if (sanitized !== attr.value) { attr.value = sanitized; } continue; } if (name === "style") { attr.value = sanitizeCssValue(attr.value); continue; } if (URL_BEARING_SVG_ATTRS.has(name) && /url\s*\(/i.test(attr.value)) { attr.value = sanitizeCssValue(attr.value); } } } function sanitizeHtmlAttributes(el) { const tagName = el.tagName.toLowerCase(); for (let i = el.attributes.length - 1; i >= 0; i--) { const attr = el.attributes[i]; const name = attr.name.toLowerCase(); const normalized = name.replace(INVISIBLE_WHITESPACE, ""); if (EVENT_HANDLER_PATTERN.test(normalized)) { el.removeAttribute(attr.name); continue; } if (DATA_ATTR_PATTERN.test(name) || ARIA_ATTR_PATTERN.test(name)) { continue; } if (!ALLOWED_HTML_ATTRS.has(name)) { el.removeAttribute(attr.name); continue; } if (name === "href") { if (tagName !== "a") { el.removeAttribute(attr.name); continue; } const stripped = attr.value.replace(INVISIBLE_WHITESPACE, ""); if (!SAFE_LINK_PROTOCOLS.test(stripped)) { el.removeAttribute(attr.name); } continue; } if (name === "style") { attr.value = sanitizeCssValue(attr.value); } } } function sanitizeNode(node, mode, depth) { for (let i = node.children.length - 1; i >= 0; i--) { const child = node.children[i]; const tag = child.tagName.toLowerCase(); if (mode === "svg") { if (tag === "foreignobject") { sanitizeSvgAttributes(child, depth); sanitizeNode(child, "html", depth); } else if (tag === "style") { sanitizeSvgAttributes(child, depth); if (child.textContent) { child.textContent = sanitizeStyleElement(child.textContent); } } else if (ANIMATION_TAGS.has(tag) && isAnimationDangerous(child)) { child.remove(); } else if (ALLOWED_SVG_TAGS.has(tag)) { sanitizeSvgAttributes(child, depth); sanitizeNode(child, "svg", depth); } else { child.remove(); } } else { if (BLOCKED_HTML_TAGS.has(tag)) { child.remove(); } else if (ALLOWED_HTML_TAGS.has(tag)) { sanitizeHtmlAttributes(child); sanitizeNode(child, "html", depth); } else { child.remove(); } } } } function sanitizeSvgInner(svgText, depth) { const doc = new DOMParser().parseFromString(svgText, "image/svg+xml"); const parseError = doc.querySelector("parsererror"); if (parseError) return ""; const svg = doc.documentElement; if (svg.tagName.toLowerCase() !== "svg") return ""; sanitizeSvgAttributes(svg, depth); sanitizeNode(svg, "svg", depth); if (svg.children.length === 0) return ""; return new XMLSerializer().serializeToString(svg); } function sanitizeSvg(svgText) { return sanitizeSvgInner(svgText, 0); } //# sourceMappingURL=sanitizeSvg.js.map