1import { x as splitProps, i as getNextElement, t as template, j as spread, k as mergeProps, v as createRenderEffect, r as runHydrationEvents, z as setAttribute, e as createSignal } from './web-CqJQ5ziD.js'; 2import { e as env } from './env-CTimTkDP.js'; 3import { l as logger } from './logger-BHOJRl6t.js'; 4 5var _tmpl$ = /* @__PURE__ */ template(`<svg width=24 height=24 viewBox="0 0 24 24"fill=none xmlns=http://www.w3.org/2000/svg><path stroke=currentColor stroke-width=1.5 stroke-linecap=round stroke-linejoin=round>`); 6const PATHS = { 7 left: "M6.75 15.75L3 12M3 12L6.75 8.25M3 12H21", 8 right: "M17.25 8.25L21 12M21 12L17.25 15.75M21 12H3" 9}; 10function ArrowIcon(props) { 11 const [local, others] = splitProps(props, ["class", "classList", "direction"]); 12 const direction = () => local.direction ?? "right"; 13 const path = () => PATHS[direction()]; 14 return (() => { 15 var _el$ = getNextElement(_tmpl$), _el$2 = _el$.firstChild; 16 spread(_el$, mergeProps({ 17 get ["class"]() { 18 return local.class; 19 }, 20 get classList() { 21 return local.classList; 22 } 23 }, others), true, true); 24 createRenderEffect(() => setAttribute(_el$2, "d", path())); 25 runHydrationEvents(); 26 return _el$; 27 })(); 28} 29 30function hasFiniteDuration(duration) { 31 return duration !== void 0 && isFinite(duration) && duration > 0; 32} 33function formatTime(seconds) { 34 if (!isFinite(seconds)) return "0:00"; 35 const mins = Math.floor(seconds / 60); 36 const secs = Math.floor(seconds % 60); 37 const paddedSecs = secs < 10 ? "0" + secs : secs; 38 return `${mins}:${paddedSecs}`; 39} 40function resolveMediaUrl(media) { 41 if (!media) return null;
42 if (typeof media === "number") return null; 43 if (typeof media === "string") { 44 if (media.startsWith("http")) return media; 45 return `${env.cmsApiUrl}${media}`; 46 } 47 const url = media.url; 48 if (!url) return null; 49 if (url.startsWith("http")) return url; 50 return `${env.cmsApiUrl}${url}`; 51} 52function resolveOgImage(media) { 53 if (!media || typeof media === "number" || typeof media === "string") { 54 return resolveMedia(media); 55 } 56 const og = "sizes" in media ? media.sizes?.og : void 0; 57 if (og?.url) { 58 return { 59 url: og.url.startsWith("http") ? og.url : `${env.cmsApiUrl}${og.url}`, 60 width: og.width ?? void 0, 61 height: og.height ?? void 0, 62 alt: media.alt || void 0 63 }; 64 } 65 return resolveMedia(media); 66} 67function resolveMedia(media) { 68 if (!media) return null; 69 if (typeof media === "number") return null; 70 let url = null; 71 let width; 72 let height; 73 let alt; 74 if (typeof media === "string") { 75 url = media; 76 } else { 77 url = media.url; 78 width = media.width; 79 height = media.height; 80 alt = media.alt || void 0; 81 } 82 if (!url) return null; 83 if (!url.startsWith("http")) { 84 url = `${env.cmsApiUrl}${url}`; 85 } 86 return { 87 url, 88 width, 89 height, 90 alt 91 }; 92} 93function getTcfConsentString() { 94 if (typeof window === "undefined") return "usercentrics_not_implemented"; 95 try { 96 const tcString = window.__ucCmp?.cmpController?.consent?.tcString; 97 return typeof tcString === "string" && tcString ? tcString : "null"; 98 } catch { 99 return "usercentrics_not_implemented"; 100 } 101} 102const appendParamsToMediaUrl = (url) => { 103 if (!url) return url; 104 const parsed = new URL(url); 105 parsed.searchParams.set("t", String(Date.now())); 106 const defaults = { 107 aggregator: "website", 108 companionAds: "false", 109 "aw_0_1st.playerId": "website-player", 110 "aw_0_req.userConsentV2": getTcfConsentString() 111 }; 112 for (const [key, value] of Object.entries(defaults)) { 113 if (!parsed.searchParams.has(key)) { 114 parsed.searchParams.set(key, value); 115 } 116 } 117 const finalUrl = parsed.toString(); 118 return finalUrl; 119}; 120 121/*! @license DOMPurify 3.4.15 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.15
vendor: 9,549 bytes, lines 121-415
121/LICENSE */ 122 123function _arrayLikeToArray(r, a) { 124 (null == a || a > r.length) && (a = r.length); 125 for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; 126 return n; 127} 128function _arrayWithHoles(r) { 129 if (Array.isArray(r)) return r; 130} 131function _iterableToArrayLimit(r, l) { 132 var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; 133 if (null != t) { 134 var e, 135 n, 136 i, 137 u, 138 a = [], 139 f = true, 140 o = false; 141 try { 142 if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); 143 } catch (r) { 144 o = true, n = r; 145 } finally { 146 try { 147 if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; 148 } finally { 149 if (o) throw n; 150 } 151 } 152 return a; 153 } 154} 155function _nonIterableRest() { 156 throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 157} 158function _slicedToArray(r, e) { 159 return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); 160} 161function _unsupportedIterableToArray(r, a) { 162 if (r) { 163 if ("string" == typeof r) return _arrayLikeToArray(r, a); 164 var t = {}.toString.call(r).slice(8, -1); 165 return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; 166 } 167} 168 169const entries = Object.entries, 170 setPrototypeOf = Object.setPrototypeOf, 171 isFrozen = Object.isFrozen, 172 getPrototypeOf = Object.getPrototypeOf, 173 getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; 174let freeze = Object.freeze, 175 seal = Object.seal, 176 create = Object.create; // eslint-disable-line import/no-mutable-exports 177let _ref = typeof Reflect !== 'undefined' && Reflect, 178 apply = _ref.apply, 179 construct = _ref.construct; 180if (!freeze) { 181 freeze = function freeze(x) { 182 return x; 183 }; 184} 185if (!seal) { 186 seal = function seal(x) { 187 return x; 188 }; 189} 190if (!apply) { 191 apply = function apply(func, thisArg) { 192 for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { 193 args[_key - 2] = arguments[_key]; 194 } 195 return func.apply(thisArg, args); 196 }; 197} 198if (!construct) { 199 construct = function construct(Func) { 200 for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { 201 args[_key2 - 1] = arguments[_key2]; 202 } 203 return new Func(...args); 204 }; 205} 206const arrayForEach = unapply(Array.prototype.forEach); 207const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf); 208const arrayPop = unapply(Array.prototype.pop); 209const arrayPush = unapply(Array.prototype.push); 210const arraySplice = unapply(Array.prototype.splice); 211const arrayIsArray = Array.isArray; 212const stringToLowerCase = unapply(String.prototype.toLowerCase); 213const stringToString = unapply(String.prototype.toString); 214const stringMatch = unapply(String.prototype.match); 215const stringReplace = unapply(String.prototype.replace); 216const stringIndexOf = unapply(String.prototype.indexOf); 217const stringTrim = unapply(String.prototype.trim); 218const numberToString = unapply(Number.prototype.toString); 219const booleanToString = unapply(Boolean.prototype.toString); 220const bigintToString = typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString); 221const symbolToString = typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString); 222const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty); 223const objectToString = unapply(Object.prototype.toString); 224const regExpTest = unapply(RegExp.prototype.test); 225const typeErrorCreate = unconstruct(TypeError); 226/** 227 * Creates a new function that calls the given function with a specified thisArg and arguments. 228 * 229 * @param func - The function to be wrapped and called. 230 * @returns A new function that calls the given function with a specified thisArg and arguments. 231 */ 232function unapply(func) { 233 return function (thisArg) { 234 if (thisArg instanceof RegExp) { 235 thisArg.lastIndex = 0; 236 } 237 for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { 238 args[_key3 - 1] = arguments[_key3]; 239 } 240 return apply(func, thisArg, args); 241 }; 242} 243/** 244 * Creates a new function that constructs an instance of the given constructor function with the provided arguments. 245 * 246 * @param func - The constructor function to be wrapped and called. 247 * @returns A new function that constructs an instance of the given constructor function with the provided arguments. 248 */ 249function unconstruct(Func) { 250 return function () { 251 for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 252 args[_key4] = arguments[_key4]; 253 } 254 return construct(Func, args); 255 }; 256} 257/** 258 * Add properties to a lookup table 259 * 260 * @param set - The set to which elements will be added. 261 * @param array - The array containing elements to be added to the set. 262 * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set. 263 * @returns The modified set with added elements. 264 */ 265function addToSet(set, array) { 266 let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase; 267 if (setPrototypeOf) { 268 // Make 'in' and truthy checks like Boolean(set.constructor) 269 // independent of any properties defined on Object.prototype. 270 // Prevent prototype setters from intercepting set as a this value. 271 setPrototypeOf(set, null); 272 } 273 if (!arrayIsArray(array)) { 274 return set; 275 } 276 let l = array.length; 277 while (l--) { 278 let element = array[l]; 279 if (typeof element === 'string') { 280 const lcElement = transformCaseFunc(element); 281 if (lcElement !== element) { 282 // Config presets (e.g. tags.js, attrs.js) are immutable. 283 if (!isFrozen(array)) { 284 array[l] = lcElement; 285 } 286 element = lcElement; 287 } 288 } 289 set[element] = true; 290 } 291 return set; 292} 293/** 294 * Clean up an array to harden against CSPP 295 * 296 * @param array - The array to be cleaned. 297 * @returns The cleaned version of the array 298 */ 299function cleanArray(array) { 300 for (let index = 0; index < array.length; index++) { 301 const isPropertyExist = objectHasOwnProperty(array, index); 302 if (!isPropertyExist) { 303 array[index] = null; 304 } 305 } 306 return array; 307} 308/** 309 * Shallow clone an object 310 * 311 * @param object - The object to be cloned. 312 * @returns A new object that copies the original. 313 */ 314function clone(object) { 315 const newObject = create(null); 316 for (const _ref2 of entries(object)) { 317 var _ref3 = _slicedToArray(_ref2, 2); 318 const property = _ref3[0]; 319 const value = _ref3[1]; 320 const isPropertyExist = objectHasOwnProperty(object, property); 321 if (isPropertyExist) { 322 if (arrayIsArray(value)) { 323 newObject[property] = cleanArray(value); 324 } else if (value && typeof value === 'object' && value.constructor === Object) { 325 newObject[property] = clone(value); 326 } else { 327 newObject[property] = value; 328 } 329 } 330 } 331 return newObject; 332} 333/** 334 * Convert non-node values into strings without depending on direct property access. 335 * 336 * @param value - The value to stringify. 337 * @returns A string representation of the provided value. 338 */ 339function stringifyValue(value) { 340 switch (typeof value) { 341 case 'string': 342 { 343 return value; 344 } 345 case 'number': 346 { 347 return numberToString(value); 348 } 349 case 'boolean': 350 { 351 return booleanToString(value); 352 } 353 case 'bigint': 354 { 355 return bigintToString ? bigintToString(value) : '0'; 356 } 357 case 'symbol': 358 { 359 return symbolToString ? symbolToString(value) : 'Symbol()'; 360 } 361 case 'undefined': 362 { 363 return objectToString(value); 364 } 365 case 'function': 366 case 'object': 367 { 368 if (value === null) { 369 return objectToString(value); 370 } 371 const valueAsRecord = value; 372 const valueToString = lookupGetter(valueAsRecord, 'toString'); 373 if (typeof valueToString === 'function') { 374 const stringified = valueToString(valueAsRecord); 375 return typeof stringified === 'string' ? stringified : objectToString(stringified); 376 } 377 return objectToString(value); 378 } 379 default: 380 { 381 return objectToString(value); 382 } 383 } 384} 385/** 386 * This method automatically checks if the prop is function or getter and behaves accordingly. 387 * 388 * @param object - The object to look up the getter function in its prototype chain. 389 * @param prop - The property name for which to find the getter function. 390 * @returns The getter function found in the prototype chain or a fallback function. 391 */ 392function lookupGetter(object, prop) { 393 while (object !== null) { 394 const desc = getOwnPropertyDescriptor(object, prop); 395 if (desc) { 396 if (desc.get) { 397 return unapply(desc.get); 398 } 399 if (typeof desc.value === 'function') { 400 return unapply(desc.value); 401 } 402 } 403 object = getPrototypeOf(object); 404 } 405 function fallbackValue() { 406 return null; 407 } 408 return fallbackValue; 409} 410function isRegex(value) { 411 try { 412 regExpTest(value, ''); 413 return true; 414 } catch (_unused) { 415 return false;
vendor: 9,004 bytes, lines 416-461
416 } 417} 418 419const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']); 420const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']); 421const svgFilters = freeze(['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']); 422// List of SVG elements that are disallowed by default. 423// We still need to know them so that we can do namespace 424// checks properly in case one wants to add them to 425// allow-list. 426const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']); 427const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']); 428// Similarly to SVG, we want to know all MathML elements, 429// even those that we disallow by default. 430const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']); 431const text = freeze(['#text']); 432 433const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']); 434const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', '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', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'pointer-events', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'vector-effect', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']); 435const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); 436const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); 437 438const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g); 439const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g); 440const TMPLIT_EXPR = seal(/\${[\w\W]*/g); 441const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape 442const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape 443const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape 444); 445const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); 446const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex 447); 448const DOCTYPE_NAME = seal(/^html$/i); 449const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i); 450// Markup-significant character probes used by _sanitizeElements. 451// Shared module-level instances are safe despite the sticky /g flags: 452// unapply() resets lastIndex for RegExp receivers before every call. 453const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g); 454const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g); 455const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i); 456const SELF_CLOSING_TAG = seal(/\/>/i); 457 458// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType 459const NODE_TYPE = { 460 element: 1, 461
vendor: 8,671 bytes, lines 461-640
461attribute: 2, 462 text: 3, 463 cdataSection: 4, 464 entityReference: 5, 465 // Deprecated 466 entityNode: 6, 467 // Deprecated 468 processingInstruction: 7, 469 comment: 8, 470 document: 9, 471 documentType: 10, 472 documentFragment: 11, 473 notation: 12 // Deprecated 474}; 475/* HTML-namespace elements whose child text nodes are serialized *literally* 476 (unescaped) by the HTML fragment-serialization algorithm. Two reparse-mXSS 477 shapes ride on that literal serialization: 478 (a) an element child - a tree the HTML parser can never build, but the DOM 479 API and an XML/XHTML parse can - after which a `</tag>`-bearing text 480 sibling breaks the element open on reparse; and 481 (b) text-only content that already carries the element's OWN end tag, e.g. 482 `<style>...</style><img onerror=x>` built as a node, which the literal 483 serializer emits verbatim for the HTML parser to re-open. 484 Shape (a) is handled by the firstElementChild branch in _isUnsafeNode; shape 485 (b) by the LITERAL_TEXT_CLOSE probe. Both read textContent (the raw-serialized 486 form for these elements) rather than innerHTML, because an XML/XHTML working 487 document serializes innerHTML with `<` escaped, which silently blinds the 488 innerHTML-based probes (rule 1's second probe and FALLBACK_TAG_CLOSE) there. 489 `script` is never allow-listed, but is kept here so the guard matches the 490 serializer's own literal-text list exactly. */ 491const LITERAL_TEXT_ELEMENT_NAMES = ['style', 'script', 'xmp', 'iframe', 'noembed', 'noframes', 'plaintext', 'noscript']; 492const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES)); 493/* Per-element end-tag matcher. On an HTML reparse the ONLY token that 494 terminates a literal-text element's raw content is its own end tag; a foreign 495 literal-text close (e.g. `</xmp>` sitting inside `<style>`) does not break 496 out, so matching is per-element, not a shared alternation. The lookahead 497 requires an HTML tag-name terminator (whitespace, `/` or `>`) so a longer 498 name such as `</styles` is not mistaken for `</style`. */ 499const LITERAL_TEXT_CLOSE = function () { 500 const map = {}; 501 arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, name => { 502 map[name] = seal(new RegExp('</' + name + '(?=[\\t\\n\\f\\r />])', 'i')); 503 }); 504 return freeze(map); 505}(); 506const getGlobal = function getGlobal() { 507 return typeof window === 'undefined' ? null : window; 508}; 509/** 510 * Creates a no-op policy for internal use only. 511 * Don't export this function outside this module! 512 * @param trustedTypes The policy factory. 513 * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix). 514 * @return The policy created (or null, if Trusted Types 515 * are not supported or creating the policy failed). 516 */ 517const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) { 518 if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') { 519 return null; 520 } 521 // Allow the callers to control the unique policy name 522 // by adding a data-tt-policy-suffix to the script element with the DOMPurify. 523 // Policy creation with duplicate names throws in Trusted Types. 524 let suffix = null; 525 const ATTR_NAME = 'data-tt-policy-suffix'; 526 if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { 527 suffix = purifyHostElement.getAttribute(ATTR_NAME); 528 } 529 const policyName = 'dompurify' + (suffix ? '#' + suffix : ''); 530 try { 531 return trustedTypes.createPolicy(policyName, { 532 createHTML(html) { 533 return html; 534 }, 535 createScriptURL(scriptUrl) { 536 return scriptUrl; 537 } 538 }); 539 } catch (_) { 540 // Policy creation failed (most likely another DOMPurify script has 541 // already run). Skip creating the policy, as this will only cause errors 542 // if TT are enforced. 543 console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); 544 return null; 545 } 546}; 547const _createHooksMap = function _createHooksMap() { 548 return { 549 afterSanitizeAttributes: [], 550 afterSanitizeElements: [], 551 afterSanitizeShadowDOM: [], 552 beforeSanitizeAttributes: [], 553 beforeSanitizeElements: [], 554 beforeSanitizeShadowDOM: [], 555 uponSanitizeAttribute: [], 556 uponSanitizeElement: [], 557 uponSanitizeShadowNode: [] 558 }; 559}; 560/** 561 * Resolve a set-valued configuration option: a fresh set built from 562 * cfg[key] when it is an own array property (seeded with a clone of 563 * options.base when given, case-normalized via options.transform), 564 * the fallback set otherwise. 565 * 566 * @param cfg the cloned, prototype-free configuration object 567 * @param key the configuration property to read 568 * @param fallback the set to use when the option is absent or not an array 569 * @param options transform and optional base set to merge into 570 * @returns the resolved set 571 */ 572const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) { 573 return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback; 574}; 575/** 576 * Resolve an object-valued configuration option: a prototype-free clone 577 * of cfg[key] when it is an own, truthy object property, else a fresh 578 * fallback built by makeFallback (fresh on every parse, so a previous 579 * parse can never leak state into the next one). 580 * 581 * @param cfg the cloned, prototype-free configuration object 582 * @param key the configuration property to read 583 * @param makeFallback builds the fallback value when the option is absent 584 * @returns the resolved object 585 */ 586const _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallback) { 587 const value = objectHasOwnProperty(cfg, key) ? cfg[key] : undefined; 588 return value && typeof value === 'object' ? clone(value) : makeFallback(); 589}; 590function createDOMPurify() { 591 let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); 592 const DOMPurify = root => createDOMPurify(root); 593 DOMPurify.version = '3.4.15'; 594 DOMPurify.removed = []; 595 if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) { 596 // Not running in a browser, provide a factory function 597 // so that you can pass your own Window 598 DOMPurify.isSupported = false; 599 return DOMPurify; 600 } 601 let document = window.document; 602 const originalDocument = document; 603 const currentScript = originalDocument.currentScript; 604 window.DocumentFragment; 605 const HTMLTemplateElement = window.HTMLTemplateElement, 606 Node = window.Node, 607 Element = window.Element, 608 NodeFilter = window.NodeFilter, 609 _window$NamedNodeMap = window.NamedNodeMap; 610 _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap; 611 window.HTMLFormElement; 612 const DOMParser = window.DOMParser, 613 trustedTypes = window.trustedTypes; 614 const ElementPrototype = Element.prototype; 615 const cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); 616 const remove = lookupGetter(ElementPrototype, 'remove'); 617 // Clobber-safe Attr-node removal. On an HTMLFormElement a descendant named 618 // "removeAttributeNode" shadows the prototype method via 619 // [LegacyOverrideBuiltIns], so element.removeAttributeNode(attr) throws. 620 // Calling the cached Element.prototype method with the element as the 621 // receiver removes the exact live Attr node regardless of the shadowing. 622 const removeAttributeNode = lookupGetter(ElementPrototype, 'removeAttributeNode'); 623 const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); 624 const getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); 625 const getParentNode = lookupGetter(ElementPrototype, 'parentNode'); 626 const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot'); 627 const getAttributes = lookupGetter(ElementPrototype, 'attributes'); 628 const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null; 629 const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null; 630 const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null; 631 /* Clobber-safe nodeType / nodeName reads through the cached Node.prototype 632 getters, with a direct-property fallback for environments that lack 633 Node.prototype. Sites that need a different fallback (e.g. _isClobbered 634 returns early on a null name) intentionally keep their own reads. */ 635 const _readNodeType = function _readNodeType(node) { 636 return getNodeType ? getNodeType(node) : node.nodeType; 637 }; 638 const _readNodeName = function _readNodeName(node) { 639 return getNodeName ? getNodeName(node) : node.nodeName; 640 }
vendor: 13,881 bytes, lines 640-924
640; 641 // As per issue #47, the web-components registry is inherited by a 642 // new document created via createHTMLDocument. As per the spec 643 // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) 644 // a new empty registry is used when creating a template contents owner 645 // document, so we use that as our parent document to ensure nothing 646 // is inherited. 647 if (typeof HTMLTemplateElement === 'function') { 648 const template = document.createElement('template'); 649 if (template.content && template.content.ownerDocument) { 650 document = template.content.ownerDocument; 651 } 652 } 653 let trustedTypesPolicy; 654 let emptyHTML = ''; 655 // The instance's own internal Trusted Types policy. Unlike a caller-supplied 656 // `TRUSTED_TYPES_POLICY`, this is created at most once â Trusted Types throws 657 // on duplicate policy names â and is the only policy allowed to persist 658 // across configurations and survive `clearConfig()`. 659 let defaultTrustedTypesPolicy; 660 let defaultTrustedTypesPolicyResolved = false; 661 // Tracks whether we are already inside a call to the configured Trusted Types 662 // policy (`createHTML` or `createScriptURL`). If a supplied policy callback 663 // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would 664 // re-enter the policy and recurse until the stack overflows. We detect that 665 // re-entry and throw a clear, actionable error instead. The guard is shared 666 // across both callbacks, because either one re-entering `sanitize` triggers 667 // the same unbounded recursion. 668 let IN_TRUSTED_TYPES_POLICY = 0; 669 const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() { 670 if (IN_TRUSTED_TYPES_POLICY > 0) { 671 throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or ' + 'createScriptURL) must not call DOMPurify.sanitize, as that causes ' + 'infinite recursion. Do not pass a policy whose callbacks wrap ' + 'DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted ' + 'Types" section of the README.'); 672 } 673 }; 674 const _createTrustedHTML = function _createTrustedHTML(html) { 675 _assertNotInTrustedTypesPolicy(); 676 IN_TRUSTED_TYPES_POLICY++; 677 try { 678 return trustedTypesPolicy.createHTML(html); 679 } finally { 680 IN_TRUSTED_TYPES_POLICY--; 681 } 682 }; 683 const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) { 684 _assertNotInTrustedTypesPolicy(); 685 IN_TRUSTED_TYPES_POLICY++; 686 try { 687 return trustedTypesPolicy.createScriptURL(scriptUrl); 688 } finally { 689 IN_TRUSTED_TYPES_POLICY--; 690 } 691 }; 692 // Lazily resolve (and cache) the instance's internal default policy. 693 // Resolution is attempted at most once: a successful `createPolicy` cannot be 694 // repeated (Trusted Types throws on duplicate names), and a failed or 695 // unsupported attempt must not be retried on every parse. 696 const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() { 697 if (!defaultTrustedTypesPolicyResolved) { 698 defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); 699 defaultTrustedTypesPolicyResolved = true; 700 } 701 return defaultTrustedTypesPolicy; 702 }; 703 const _document = document, 704 implementation = _document.implementation, 705 createNodeIterator = _document.createNodeIterator, 706 createDocumentFragment = _document.createDocumentFragment, 707 getElementsByTagName = _document.getElementsByTagName; 708 const importNode = originalDocument.importNode; 709 let hooks = _createHooksMap(); 710 /** 711 * Expose whether this browser supports running the full DOMPurify. 712 */ 713 DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined; 714 const MUSTACHE_EXPR$1 = MUSTACHE_EXPR, 715 ERB_EXPR$1 = ERB_EXPR, 716 TMPLIT_EXPR$1 = TMPLIT_EXPR, 717 DATA_ATTR$1 = DATA_ATTR, 718 ARIA_ATTR$1 = ARIA_ATTR, 719 IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA, 720 ATTR_WHITESPACE$1 = ATTR_WHITESPACE, 721 CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT; 722 let IS_ALLOWED_URI$1 = IS_ALLOWED_URI; 723 /** 724 * We consider the elements and attributes below to be safe. Ideally 725 * don't add any new ones but feel free to remove unwanted ones. 726 */ 727 /* allowed element names */ 728 let ALLOWED_TAGS = null; 729 const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]); 730 /* Allowed attribute names */ 731 let ALLOWED_ATTR = null; 732 const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); 733 /* 734 * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements. 735 * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) 736 * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) 737 * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. 738 */ 739 let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, { 740 tagNameCheck: { 741 writable: true, 742 configurable: false, 743 enumerable: true, 744 value: null 745 }, 746 attributeNameCheck: { 747 writable: true, 748 configurable: false, 749 enumerable: true, 750 value: null 751 }, 752 allowCustomizedBuiltInElements: { 753 writable: true, 754 configurable: false, 755 enumerable: true, 756 value: false 757 } 758 })); 759 /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ 760 let FORBID_TAGS = null; 761 /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ 762 let FORBID_ATTR = null; 763 /* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */ 764 const EXTRA_ELEMENT_HANDLING = Object.seal(create(null, { 765 tagCheck: { 766 writable: true, 767 configurable: false, 768 enumerable: true, 769 value: null 770 }, 771 attributeCheck: { 772 writable: true, 773 configurable: false, 774 enumerable: true, 775 value: null 776 } 777 })); 778 /* Decide if ARIA attributes are okay */ 779 let ALLOW_ARIA_ATTR = true; 780 /* Decide if custom data attributes are okay */ 781 let ALLOW_DATA_ATTR = true; 782 /* Decide if unknown protocols are okay */ 783 let ALLOW_UNKNOWN_PROTOCOLS = false; 784 /* Decide if self-closing tags in attributes are allowed. 785 * Usually removed due to a mXSS issue in jQuery 3.0 */ 786 let ALLOW_SELF_CLOSE_IN_ATTR = true; 787 /* Output should be safe for common template engines. 788 * This means, DOMPurify removes data attributes, mustaches and ERB 789 */ 790 let SAFE_FOR_TEMPLATES = false; 791 /* Output should be safe even for XML used within HTML and alike. 792 * This means, DOMPurify removes comments when containing risky content. 793 */ 794 let SAFE_FOR_XML = true; 795 /* Decide if document with <html>... should be returned */ 796 let WHOLE_DOCUMENT = false; 797 /* Track whether config is already set on this instance of DOMPurify. */ 798 let SET_CONFIG = false; 799 /* Pristine allowlist bindings captured at setConfig() time. On the 800 * persistent-config path sanitize() restores the sets from these before 801 * the per-walk hook clone-guard, so a hook's in-call widening cannot 802 * carry across calls. Null until setConfig() is called; reset by 803 * clearConfig(). */ 804 let SET_CONFIG_ALLOWED_TAGS = null; 805 let SET_CONFIG_ALLOWED_ATTR = null; 806 /* Decide if all elements (e.g. style, script) must be children of 807 * document.body. By default, browsers might move them to document.head */ 808 let FORCE_BODY = false; 809 /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html 810 * string (or a TrustedHTML object if Trusted Types are supported). 811 * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead 812 */ 813 let RETURN_DOM = false; 814 /* Decide if a DOM `DocumentFragment` should be returned, instead of a html 815 * string (or a TrustedHTML object if Trusted Types are supported) */ 816 let RETURN_DOM_FRAGMENT = false; 817 /* Try to return a Trusted Type object instead of a string, return a string in 818 * case Trusted Types are not supported */ 819 let RETURN_TRUSTED_TYPE = false; 820 /* Output should be free from DOM clobbering attacks? 821 * This sanitizes markups named with colliding, clobberable built-in DOM APIs. 822 */ 823 let SANITIZE_DOM = true; 824 /* Achieve full DOM Clobbering protection by isolating the namespace of named 825 * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. 826 * 827 * HTML/DOM spec rules that enable DOM Clobbering: 828 * - Named Access on Window (§7.3.3) 829 * - DOM Tree Accessors (§3.1.5) 830 * - Form Element Parent-Child Relations (§4.10.3) 831 * - Iframe srcdoc / Nested WindowProxies (§4.8.5) 832 * - HTMLCollection (§4.2.10.2) 833 * 834 * Namespace isolation is implemented by prefixing `id` and `name` attributes 835 * with a constant string, i.e., `user-content-` 836 */ 837 let SANITIZE_NAMED_PROPS = false; 838 const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; 839 /* Keep element content when removing element? */ 840 let KEEP_CONTENT = true; 841 /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead 842 * of importing it into a new Document and returning a sanitized copy */ 843 let IN_PLACE = false; 844 /* Allow usage of profiles like html, svg and mathMl */ 845 let USE_PROFILES = {}; 846 /* Tags to ignore content of when KEEP_CONTENT is true */ 847 let FORBID_CONTENTS = null; 848 const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 849 // <selectedcontent> mirrors the selected <option>'s subtree, cloned by 850 // the UA (customizable <select>) â including any on* handlers â and the 851 // engine re-mirrors synchronously whenever a removal changes which 852 // option/selectedcontent is current, even inside DOMPurify's inert 853 // DOMParser document. Hoisting its children on removal re-inserts a fresh 854 // mirror target ahead of the walk, which the engine refills, looping 855 // forever (DoS) and amplifying output. Dropping its content on removal 856 // (rather than hoisting) breaks that cascade; the content is a duplicate 857 // of the option, which is sanitized on its own. See campaign-3 F1/F6. 858 'selectedcontent', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); 859 /* Tags that are safe for data: URIs */ 860 let DATA_URI_TAGS = null; 861 const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); 862 /* Attributes safe for values like "javascript:" */ 863 let URI_SAFE_ATTRIBUTES = null; 864 const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); 865 const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; 866 const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; 867 const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; 868 /* Document namespace */ 869 let NAMESPACE = HTML_NAMESPACE; 870 let IS_EMPTY_INPUT = false; 871 /* Allowed XHTML+XML namespaces */ 872 let ALLOWED_NAMESPACES = null; 873 const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); 874 const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']); 875 let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); 876 const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']); 877 let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); 878 // Certain elements are allowed in both SVG and HTML 879 // namespace. We need to specify them explicitly 880 // so that they don't get erroneously deleted from 881 // HTML namespace. 882 const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); 883 /* Parsing of strict XHTML documents */ 884 let PARSER_MEDIA_TYPE = null; 885 const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; 886 const DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; 887 let transformCaseFunc = null; 888 /* Keep a reference to config to pass to hooks */ 889 let CONFIG = null; 890 /* Ideally, do not touch anything below this line */ 891 /* ______________________________________________ */ 892 const formElement = document.createElement('form'); 893 const isRegexOrFunction = function isRegexOrFunction(testValue) { 894 return testValue instanceof RegExp || testValue instanceof Function; 895 }; 896 /** 897 * _parseConfig 898 * 899 * @param cfg optional config literal 900 */ 901 // eslint-disable-next-line complexity 902 const _parseConfig = function _parseConfig() { 903 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 904 if (CONFIG && CONFIG === cfg) { 905 return; 906 } 907 /* Shield configuration object from tampering */ 908 if (!cfg || typeof cfg !== 'object') { 909 cfg = {}; 910 } 911 /* Shield configuration object from prototype pollution */ 912 cfg = clone(cfg); 913 PARSER_MEDIA_TYPE = 914 // eslint-disable-next-line unicorn/prefer-includes 915 SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE; 916 // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. 917 transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; 918 /* Set configuration parameters */ 919 ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, { 920 transform: transformCaseFunc 921 }); 922 ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, { 923 transform: transformCaseFunc 924 });
vendor: 12,903 bytes, lines 925-1182
925 ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, { 926 transform: stringToString 927 }); 928 URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, { 929 transform: transformCaseFunc, 930 base: DEFAULT_URI_SAFE_ATTRIBUTES 931 }); 932 DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, { 933 transform: transformCaseFunc, 934 base: DEFAULT_DATA_URI_TAGS 935 }); 936 FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, { 937 transform: transformCaseFunc 938 }); 939 FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), { 940 transform: transformCaseFunc 941 }); 942 FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), { 943 transform: transformCaseFunc 944 }); 945 USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false; 946 ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true 947 ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true 948 ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false 949 ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true 950 SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false 951 SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true 952 WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false 953 RETURN_DOM = cfg.RETURN_DOM || false; // Default false 954 RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false 955 RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false 956 FORCE_BODY = cfg.FORCE_BODY || false; // Default false 957 SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true 958 SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false 959 KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true 960 IN_PLACE = cfg.IN_PLACE || false; // Default false 961 IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp 962 NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace 963 MATHML_TEXT_INTEGRATION_POINTS = _resolveObjectOption(cfg, 'MATHML_TEXT_INTEGRATION_POINTS', () => addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS) // Default built-in map 964 ); 965 HTML_INTEGRATION_POINTS = _resolveObjectOption(cfg, 'HTML_INTEGRATION_POINTS', () => addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS) // Default built-in map 966 ); 967 const customElementHandling = _resolveObjectOption(cfg, 'CUSTOM_ELEMENT_HANDLING', () => create(null)); 968 CUSTOM_ELEMENT_HANDLING = create(null); 969 if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) { 970 CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined 971 } 972 if (objectHasOwnProperty(customElementHandling, 'attributeNameCheck') && isRegexOrFunction(customElementHandling.attributeNameCheck)) { 973 CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; // Default undefined 974 } 975 if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') { 976 CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined 977 } 978 seal(CUSTOM_ELEMENT_HANDLING); 979 if (SAFE_FOR_TEMPLATES) { 980 ALLOW_DATA_ATTR = false; 981 } 982 if (RETURN_DOM_FRAGMENT) { 983 RETURN_DOM = true; 984 } 985 /* Parse profile info */ 986 if (USE_PROFILES) { 987 ALLOWED_TAGS = addToSet({}, text); 988 ALLOWED_ATTR = create(null); 989 if (USE_PROFILES.html === true) { 990 addToSet(ALLOWED_TAGS, html$1); 991 addToSet(ALLOWED_ATTR, html); 992 } 993 if (USE_PROFILES.svg === true) { 994 addToSet(ALLOWED_TAGS, svg$1); 995 addToSet(ALLOWED_ATTR, svg); 996 addToSet(ALLOWED_ATTR, xml); 997 } 998 if (USE_PROFILES.svgFilters === true) { 999 addToSet(ALLOWED_TAGS, svgFilters); 1000 addToSet(ALLOWED_ATTR, svg); 1001 addToSet(ALLOWED_ATTR, xml); 1002 } 1003 if (USE_PROFILES.mathMl === true) { 1004 addToSet(ALLOWED_TAGS, mathMl$1); 1005 addToSet(ALLOWED_ATTR, mathMl); 1006 addToSet(ALLOWED_ATTR, xml); 1007 } 1008 } 1009 /* Always reset function-based ADD_TAGS / ADD_ATTR checks to prevent 1010 * leaking across calls when switching from function to array config */ 1011 EXTRA_ELEMENT_HANDLING.tagCheck = null; 1012 EXTRA_ELEMENT_HANDLING.attributeCheck = null; 1013 /* Merge configuration parameters */ 1014 if (objectHasOwnProperty(cfg, 'ADD_TAGS')) { 1015 if (typeof cfg.ADD_TAGS === 'function') { 1016 EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS; 1017 } else if (arrayIsArray(cfg.ADD_TAGS)) { 1018 if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { 1019 ALLOWED_TAGS = clone(ALLOWED_TAGS); 1020 } 1021 addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); 1022 } 1023 } 1024 if (objectHasOwnProperty(cfg, 'ADD_ATTR')) { 1025 if (typeof cfg.ADD_ATTR === 'function') { 1026 EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR; 1027 } else if (arrayIsArray(cfg.ADD_ATTR)) { 1028 if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { 1029 ALLOWED_ATTR = clone(ALLOWED_ATTR); 1030 } 1031 addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); 1032 } 1033 } 1034 if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) { 1035 if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { 1036 FORBID_CONTENTS = clone(FORBID_CONTENTS); 1037 } 1038 addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc); 1039 } 1040 /* Add #text in case KEEP_CONTENT is set to true */ 1041 if (KEEP_CONTENT) { 1042 ALLOWED_TAGS['#text'] = true; 1043 } 1044 /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ 1045 if (WHOLE_DOCUMENT) { 1046 addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); 1047 } 1048 /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ 1049 if (ALLOWED_TAGS.table) { 1050 addToSet(ALLOWED_TAGS, ['tbody']); 1051 delete FORBID_TAGS.tbody; 1052 } 1053 // Re-derive the active Trusted Types policy from this configuration on 1054 // every parse. The active policy must never be sticky closure state that 1055 // outlives the config that set it: a caller-supplied policy left in place 1056 // after `clearConfig()` â or after a later call that supplied none, or 1057 // `TRUSTED_TYPES_POLICY: null` â could sign a subsequent "default" 1058 // `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy. 1059 // See GHSA-vxr8-fq34-vvx9. 1060 if (cfg.TRUSTED_TYPES_POLICY) { 1061 if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') { 1062 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); 1063 } 1064 if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') { 1065 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); 1066 } 1067 // A caller-supplied policy applies to this configuration only. 1068 const previousTrustedTypesPolicy = trustedTypesPolicy; 1069 trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY; 1070 // Sign local variables required by `sanitize`. If the supplied policy's 1071 // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this 1072 // throws via the re-entrancy guard. Restore the previous policy first so 1073 // the instance is not left in a poisoned state. See #1422. 1074 try { 1075 emptyHTML = _createTrustedHTML(''); 1076 } catch (error) { 1077 trustedTypesPolicy = previousTrustedTypesPolicy; 1078 throw error; 1079 } 1080 } else if (cfg.TRUSTED_TYPES_POLICY === null) { 1081 // Explicit opt-out for this call: perform no Trusted Types signing and 1082 // create nothing (so a strict `trusted-types` CSP that disallows a 1083 // `dompurify` policy can still call `sanitize` from inside its own 1084 // policy â see #1422). Resetting to `undefined` rather than a sticky 1085 // `null` also drops any previously retained caller policy, so it cannot 1086 // resurface on a later call, while still allowing the next config-less 1087 // call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9. 1088 trustedTypesPolicy = undefined; 1089 emptyHTML = ''; 1090 } else { 1091 // No policy supplied: keep the currently active policy if one is set â a 1092 // previously supplied policy is intentionally sticky across config-less 1093 // calls â otherwise fall back to the instance's own internal policy, 1094 // created at most once. (A policy supplied for a *single* call still 1095 // lingers by design; what must not linger is a policy whose configuration 1096 // has been torn down via `clearConfig()`, which restores the default.) 1097 if (trustedTypesPolicy === undefined) { 1098 trustedTypesPolicy = _getDefaultTrustedTypesPolicy(); 1099 } 1100 // Sign internal variables only when a policy is active. A falsy policy 1101 // (Trusted Types unsupported, creation failed, or an explicit opt-out) 1102 // leaves `emptyHTML` as a plain string, so we never call `.createHTML` on 1103 // a non-policy and throw. See #1422. 1104 if (trustedTypesPolicy && typeof emptyHTML === 'string') { 1105 emptyHTML = _createTrustedHTML(''); 1106 } 1107 } 1108 // Prevent further manipulation of configuration. 1109 // Not available in IE8, Safari 5, etc. 1110 if (freeze) { 1111 freeze(cfg); 1112 } 1113 CONFIG = cfg; 1114 }; 1115 /* Keep track of all possible SVG and MathML tags 1116 * so that we can perform the namespace checks 1117 * correctly. */ 1118 const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]); 1119 const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]); 1120 /** 1121 * Namespace rules for an element in the SVG namespace. 1122 * 1123 * @param tagName the element's lowercase tag name 1124 * @param parent the (possibly simulated) parent node 1125 * @param parentTagName the parent's lowercase tag name 1126 * @returns true if a spec-compliant parser could produce this element 1127 */ 1128 const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) { 1129 // The only way to switch from HTML namespace to SVG 1130 // is via <svg>. If it happens via any other tag, then 1131 // it should be killed. 1132 if (parent.namespaceURI === HTML_NAMESPACE) { 1133 return tagName === 'svg'; 1134 } 1135 // The only way to switch from MathML to SVG is via <svg> 1136 // if the parent is either <annotation-xml> or a MathML 1137 // text integration point. 1138 if (parent.namespaceURI === MATHML_NAMESPACE) { 1139 return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); 1140 } 1141 // We only allow elements that are defined in SVG 1142 // spec. All others are disallowed in SVG namespace. 1143 return Boolean(ALL_SVG_TAGS[tagName]); 1144 }; 1145 /** 1146 * Namespace rules for an element in the MathML namespace. 1147 * 1148 * @param tagName the element's lowercase tag name 1149 * @param parent the (possibly simulated) parent node 1150 * @param parentTagName the parent's lowercase tag name 1151 * @returns true if a spec-compliant parser could produce this element 1152 */ 1153 const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) { 1154 // The only way to switch from HTML namespace to MathML 1155 // is via <math>. If it happens via any other tag, then 1156 // it should be killed. 1157 if (parent.namespaceURI === HTML_NAMESPACE) { 1158 return tagName === 'math'; 1159 } 1160 // The only way to switch from SVG to MathML is via 1161 // <math> and HTML integration points 1162 if (parent.namespaceURI === SVG_NAMESPACE) { 1163 return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; 1164 } 1165 // We only allow elements that are defined in MathML 1166 // spec. All others are disallowed in MathML namespace. 1167 return Boolean(ALL_MATHML_TAGS[tagName]); 1168 }; 1169 /** 1170 * Namespace rules for an element in the HTML namespace. 1171 * 1172 * @param tagName the element's lowercase tag name 1173 * @param parent the (possibly simulated) parent node 1174 * @param parentTagName the parent's lowercase tag name 1175 * @returns true if a spec-compliant parser could produce this element 1176 */ 1177 const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) { 1178 // The only way to switch from SVG to HTML is via 1179 // HTML integration points, and from MathML to HTML 1180 // is via MathML text integration points 1181 if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { 1182 return false;
vendor: 3,863 bytes, lines 1183-1269
1183 } 1184 if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { 1185 return false; 1186 } 1187 // We disallow tags that are specific for MathML 1188 // or SVG and should never appear in HTML namespace 1189 return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); 1190 }; 1191 /** 1192 * @param element a DOM element whose namespace is being checked 1193 * @returns Return false if the element has a 1194 * namespace that a spec-compliant parser would never 1195 * return. Return true otherwise. 1196 */ 1197 const _checkValidNamespace = function _checkValidNamespace(element) { 1198 let parent = getParentNode(element); 1199 // In JSDOM, if we're inside shadow DOM, then parentNode 1200 // can be null. We just simulate parent in this case. 1201 if (!parent || !parent.tagName) { 1202 parent = { 1203 namespaceURI: NAMESPACE, 1204 tagName: 'template' 1205 }; 1206 } 1207 const tagName = stringToLowerCase(element.tagName); 1208 const parentTagName = stringToLowerCase(parent.tagName); 1209 if (!ALLOWED_NAMESPACES[element.namespaceURI]) { 1210 return false; 1211 } 1212 if (element.namespaceURI === SVG_NAMESPACE) { 1213 return _checkSvgNamespace(tagName, parent, parentTagName); 1214 } 1215 if (element.namespaceURI === MATHML_NAMESPACE) { 1216 return _checkMathMlNamespace(tagName, parent, parentTagName); 1217 } 1218 if (element.namespaceURI === HTML_NAMESPACE) { 1219 return _checkHtmlNamespace(tagName, parent, parentTagName); 1220 } 1221 // For XHTML and XML documents that support custom namespaces 1222 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { 1223 return true; 1224 } 1225 // The code should never reach this place (this means 1226 // that the element somehow got namespace that is not 1227 // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). 1228 // Return false just in case. 1229 return false; 1230 }; 1231 /** 1232 * _forceRemove 1233 * 1234 * @param node a DOM node 1235 */ 1236 const _forceRemove = function _forceRemove(node) { 1237 arrayPush(DOMPurify.removed, { 1238 element: node 1239 }); 1240 try { 1241 // eslint-disable-next-line unicorn/prefer-dom-node-remove 1242 getParentNode(node).removeChild(node); 1243 } catch (_) { 1244 /* The normal detach failed â this is reached for a parentless node 1245 (getParentNode() is null, so .removeChild throws). Element.prototype 1246 .remove() is itself a spec no-op on a parentless node, so a recorded 1247 "removal" would otherwise hand the caller back an intact, 1248 payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or 1249 the style-with-element-child rule decided to kill). Fail closed by 1250 throwing â exactly as a clobbered root does at the IN_PLACE entry â 1251 rather than trying to "neutralize" the node via its own methods. 1252 Neutralizing would mean calling getAttributeNames()/removeAttribute() 1253 on the node, both of which a <form> root can clobber via a named child 1254 (and _isClobbered does not even probe getAttributeNames), so the 1255 neutralize step could itself be silently defeated, leaving the payload 1256 intact. A throw touches only the cached, clobber-safe remove() and 1257 getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root) 1258 to every root-kill reason. REPORT-3. 1259 This lives inside the catch, so it never fires for a normally-removed 1260 in-tree node: those have a parent, removeChild() succeeds, and the 1261 catch is not entered. Only a kept (parentless) root reaches here. */ 1262 remove(node); 1263 if (!getParentNode(node)) { 1264 throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place'); 1265 } 1266 } 1267 }; 1268 /** 1269 * _
1269stripAttributeNode 1270 * 1271 * Remove a single Attr node case/namespace-exactly on an attribute-teardown 1272 * path. Name-based removeAttribute() ASCII-lowercases its lookup key for an 1273 * HTML element in an HTML document and so silently misses a case-preserved 1274 * handler (e.g. `ONERROR` off an XML/XHTML import) - the same defect 1275 * _removeAttribute() was fixed for, which a name-based call would reintroduce 1276 * on these IN_PLACE teardown paths. Unlike _removeAttribute this does not 1277 * record into DOMPurify.removed: the neutralize passes intentionally do not 1278 * book-keep. A clobbered/detached node falls back to best-effort name-based 1279 * removal. 1280 * 1281 * @param element the element to strip the attribute from 1282 * @param attribute the Attr node to remove 1283 * @param name the attribute's name, for the fallback path 1284 */ 1285 const _stripAttributeNode = function _stripAttributeNode(element, attribute, name) { 1286 try { 1287 removeAttributeNode(element, attribute); 1288 } catch (_) { 1289 try { 1290 element.removeAttribute(name); 1291 } catch (_) {} 1292 } 1293 }; 1294 /** 1295 * _neutralizeRoot 1296 * 1297 * Fail-closed teardown of an in-place root after the sanitize walk aborts 1298 * (campaign-3 F2). An internal throw mid-walk â e.g. a page-registered 1299 * custom element's reaction detaches a node so `_forceRemove`'s deliberate 1300 * parentless guard throws, or any other re-entrant engine mutation â would 1301 * otherwise leave the caller's *live* tree half-sanitized, with everything 1302 * after the abort point still carrying its handlers. There is no safe way 1303 * to resume the walk (the tree mutated under us), so we strip the root bare: 1304 * remove every child and every attribute, then let the caller's catch see 1305 * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes` 1306 * getters; the root was already clobber-pre-flighted at the IN_PLACE entry). 1307 * 1308 * @param root the in-place root to empty 1309 */ 1310 const _neutralizeRoot = function _neutralizeRoot(root) { 1311 /* Strip every disallowed attribute (on* handlers included) off the whole 1312 subtree BEFORE detaching anything. Detaching first would hand back 1313 handler-bearing originals (e.g. an already-loading `<img onerror>`) 1314 whose queued resource event still fires in page scope after we throw. 1315 Clobber-safe reads; a doomed clobbered node's own attributes are 1316 irrelevant while its non-clobbered descendants are reached and scrubbed. */ 1317 _neutralizeSubtree(root); 1318 const childNodes = getChildNodes(root); 1319 if (childNodes) { 1320 const snapshot = []; 1321 arrayForEach(childNodes, child => { 1322 arrayPush(snapshot, child); 1323 }); 1324 arrayForEach(snapshot, child => { 1325 try { 1326 remove(child); 1327 } catch (_) { 1328 /* Best-effort teardown; a still-attached child is handled below */ 1329 } 1330 }); 1331 } 1332 const attributes = getAttributes(root); 1333 if (attributes) { 1334 for (let i = attributes.length - 1; i >= 0; --i) { 1335 const attribute = attributes[i]; 1336 const name = attribute && attribute.name; 1337 if (typeof name === 'string') { 1338 _stripAttributeNode(root, attribute, name); 1339 } 1340 } 1341 } 1342 }; 1343 /** 1344 * _removeAttribute 1345 * 1346 * Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their 1347 * lookup key for HTML elements in an HTML document, so they silently miss an 1348 * attribute whose stored qualified name still contains uppercase ASCII 1349 * letters. That happens when the node came from a case-preserving source 1350 * (an XML/XHTML document imported via importNode(), or createAttributeNS()), 1351 * where e.g. `ONERROR` survives the walk: the policy check lowercases to 1352 * `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up 1353 * `onerror` and finds nothing. Remove the exact Attr node instead, which is 1354 * case- and namespace-exact, and fall back to name-based removal only when 1355 * the caller could not supply the node. 1356 * 1357 * @param name an Attribute name 1358 * @param element a DOM node 1359 * @param attr the exact Attr node to remove, when the caller has it 1360 */ 1361 const _removeAttribute = function _removeAttribute(name, element, attr) { 1362 if (!attr) {
1363 try { 1364 attr = element.getAttributeNode(name); 1365 } catch (_) { 1366 attr = null; 1367 } 1368 } 1369 arrayPush(DOMPurify.removed, { 1370 attribute: attr || null, 1371 from: element 1372 }); 1373 try { 1374 if (attr) { 1375 removeAttributeNode(element, attr); 1376 } else { 1377 element.removeAttribute(name); 1378 } 1379 } catch (_) { 1380 /* Clobbered or already-detached node - best-effort fall back to a 1381 name-based removal so the "is" handling below still runs. Note this 1382 fallback ASCII-lowercases its lookup key in an HTML document and so 1383 cannot reach a case-preserved attribute name; the cached 1384 removeAttributeNode() above is what removes those, and a clobbered 1385 form is already removed wholesale by _isClobbered() before reaching 1386 here
vendor: 15,411 bytes, lines 1386-1722
1386. */ 1387 try { 1388 element.removeAttribute(name); 1389 } catch (_) {} 1390 } 1391 // We void attribute values for unremovable "is" attributes 1392 if (name === 'is') { 1393 if (RETURN_DOM || RETURN_DOM_FRAGMENT) { 1394 try { 1395 _forceRemove(element); 1396 } catch (_) {} 1397 } else { 1398 try { 1399 element.setAttribute(name, ''); 1400 } catch (_) {} 1401 } 1402 } 1403 }; 1404 /** 1405 * _stripDisallowedAttributes 1406 * 1407 * Removes every attribute the active configuration does not allow from a 1408 * single element, using the same allowlist as the main attribute pass (so 1409 * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to 1410 * neutralise nodes that are being discarded from an in-place tree. 1411 * 1412 * @param element the element to strip 1413 */ 1414 const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) { 1415 const attributes = getAttributes(element); 1416 if (!attributes) { 1417 return; 1418 } 1419 for (let i = attributes.length - 1; i >= 0; --i) { 1420 const attribute = attributes[i]; 1421 const name = attribute && attribute.name; 1422 if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) { 1423 continue; 1424 } 1425 _stripAttributeNode(element, attribute, name); 1426 } 1427 }; 1428 /** 1429 * _neutralizeSubtree 1430 * 1431 * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT 1432 * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary, 1433 * namespace, comment, processing-instruction and KEEP_CONTENT:false removals 1434 * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path 1435 * those dropped nodes are detached from the caller's LIVE tree but a 1436 * handler-bearing original among them (an `<img onerror>`/`<video>` that was 1437 * loading) keeps its queued resource event, which fires in page scope after 1438 * sanitize returns. This walks a removed subtree and strips every attribute 1439 * the active configuration does not allow â so `on*` handlers are cancelled 1440 * through the SAME allowlist that governs kept nodes, not a separate `/^on/` 1441 * blocklist. Run synchronously before sanitize returns, i.e. before any 1442 * queued event can fire. Hook-free by design: these nodes leave the output, 1443 * so firing attribute hooks for them would be surprising. Clobber-safe reads; 1444 * a doomed clobbered node may shadow `removeAttribute` (its own attributes are 1445 * irrelevant â it is discarded â while its non-clobbered descendants, e.g. 1446 * the `<img>`, are reached and scrubbed). 1447 * 1448 * @param root the root of a removed subtree to neutralise 1449 */ 1450 const _neutralizeSubtree = function _neutralizeSubtree(root) { 1451 const stack = [root]; 1452 while (stack.length > 0) { 1453 const node = stack.pop(); 1454 const nodeType = _readNodeType(node); 1455 if (nodeType === NODE_TYPE.element) { 1456 _stripDisallowedAttributes(node); 1457 } 1458 const childNodes = getChildNodes(node); 1459 if (childNodes) { 1460 for (let i = childNodes.length - 1; i >= 0; --i) { 1461 stack.push(childNodes[i]); 1462 } 1463 } 1464 } 1465 }; 1466 /** 1467 * _neutralizePatchLinkage 1468 * 1469 * IN_PLACE entry pre-pass (declarative-partial-updates / streaming 1470 * hardening, https://github.com/WICG/declarative-partial-updates). 1471 * 1472 * The main walk strips patch linkage (`for`/`patchsrc`) and removes range 1473 * markers (PIs / markup comments) node-by-node, in document order, AS it 1474 * reaches each node. On a live in-place root that leaves a window: from the 1475 * moment the root is connected until the walk arrives at a given node, that 1476 * node's linkage is live. A patch applied on connection/stream can fire as 1477 * a microtask during the walk and inject or teleport an unsanitized DOM 1478 * range into a region the iterator has already passed and will not revisit, 1479 * so the post-return "tree is sanitized" contract is violated. Sweep the 1480 * whole tree once up front and sever every linkage before the walk begins, 1481 * closing that window. 1482 * 1483 * This CANNOT undo a patch that already fired before sanitize ran â that is 1484 * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat â 1485 * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML 1486 * to group with the rest of the declarative-partial-updates handling and 1487 * stay overridable, consistent with the codebase. 1488 * 1489 * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a 1490 * clobbered root cannot defeat the sweep of its non-clobbered descendants. 1491 * 1492 * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe 1493 * .html Q1): this mirrors the existing policy of keeping `for` on 1494 * <label>/<output>. If the shipping feature can drive a patch through a 1495 * surviving `for`-on-label/output + `id` pair, this pre-pass and the 1496 * attribute check at _isBasicCustomElement's caller must additionally drop 1497 * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified. 1498 * 1499 * @param root the in-place root to sweep 1500 */ 1501 /** 1502 * Central policy for declarative-partial-updates patch-linkage attributes, 1503 * shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so 1504 * the two sites cannot drift: `patchsrc` always links, `for` links 1505 * everywhere except on <label>/<output>, and the whole policy is gated on 1506 * SAFE_FOR_XML (see the rationale block in _isValidAttribute). 1507 * 1508 * @param lcName the transformCaseFunc'd attribute name 1509 * @param lcTag the transformCaseFunc'd tag name of the carrying element 1510 * @return true if the attribute is patch linkage and must be dropped 1511 */ 1512 const _isPatchLinkageAttribute = function _isPatchLinkageAttribute(lcName, lcTag) { 1513 if (!SAFE_FOR_XML) { 1514 return false; 1515 } 1516 if (lcName === 'patchsrc') { 1517 return true; 1518 } 1519 return lcName === 'for' && lcTag !== 'label' && lcTag !== 'output'; 1520 }; 1521 const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) { 1522 if (!SAFE_FOR_XML) { 1523 return; 1524 } 1525 const stack = [root]; 1526 while (stack.length > 0) { 1527 const node = stack.pop(); 1528 const nodeType = _readNodeType(node); 1529 /* Remove range markers (the target side of a patch linkage): every 1530 processing instruction, and any markup-bearing comment. */ 1531 if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) { 1532 try { 1533 remove(node); 1534 } catch (_) { 1535 /* Best-effort */ 1536 } 1537 continue; 1538 } 1539 /* Strip patch-source attributes (the source side) off elements. */ 1540 if (nodeType === NODE_TYPE.element) { 1541 const element = node; 1542 const lcTag = transformCaseFunc(_readNodeName(node)); 1543 try { 1544 if (element.hasAttribute && element.hasAttribute('patchsrc')) { 1545 element.removeAttribute('patchsrc'); 1546 } 1547 if (element.hasAttribute && element.hasAttribute('for') && _isPatchLinkageAttribute('for', lcTag)) { 1548 element.removeAttribute('for'); 1549 } 1550 } catch (_) { 1551 /* Clobbered removeAttribute/hasAttribute on a doomed node â ignore */ 1552 } 1553 } 1554 const childNodes = getChildNodes(node); 1555 if (childNodes) { 1556 for (let i = childNodes.length - 1; i >= 0; --i) { 1557 stack.push(childNodes[i]); 1558 } 1559 } 1560 } 1561 }; 1562 /** 1563 * _initDocument 1564 * 1565 * @param dirty - a string of dirty markup 1566 * @return a DOM, filled with the dirty markup 1567 */ 1568 const _initDocument = function _initDocument(dirty) { 1569 /* Create a HTML document */ 1570 let doc = null; 1571 let leadingWhitespace = null; 1572 if (FORCE_BODY) { 1573 dirty = '<remove></remove>' + dirty; 1574 } else { 1575 /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ 1576 const matches = stringMatch(dirty, /^[\r\n\t ]+/); 1577 leadingWhitespace = matches && matches[0]; 1578 } 1579 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { 1580 // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) 1581 dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>'; 1582 } 1583 const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty; 1584 /* 1585 * Use the DOMParser API by default, fallback later if needs be 1586 * DOMParser not work for svg when has multiple root element. 1587 */ 1588 if (NAMESPACE === HTML_NAMESPACE) { 1589 try { 1590 doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); 1591 } catch (_) {} 1592 } 1593 /* Use createHTMLDocument in case DOMParser is not available */ 1594 if (!doc || !doc.documentElement) { 1595 doc = implementation.createDocument(NAMESPACE, 'template', null); 1596 try { 1597 doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; 1598 } catch (_) { 1599 // Syntax error if dirtyPayload is invalid xml 1600 } 1601 } 1602 const body = doc.body || doc.documentElement; 1603 if (dirty && leadingWhitespace) { 1604 body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); 1605 } 1606 /* Work on whole document or just its body */ 1607 if (NAMESPACE === HTML_NAMESPACE) { 1608 return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; 1609 } 1610 return WHOLE_DOCUMENT ? doc.documentElement : body; 1611 }; 1612 /** 1613 * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document. 1614 * 1615 * @param root The root element or node to start traversing on. 1616 * @return The created NodeIterator 1617 */ 1618 const _createNodeIterator = function _createNodeIterator(root) { 1619 /* Read ownerDocument through the cached Node.prototype getter, never the 1620 direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a 1621 clobbering child (<input name="ownerDocument"> or a form-associated 1622 external input) shadows the prototype getter and makes a direct read 1623 return that <input>. createNodeIterator.call(<input>, ...) then throws 1624 "Illegal invocation", and on the IN_PLACE path that throw lands before 1625 the walk's fail-closed barrier - leaving the caller's live tree, with 1626 any already-armed handler in it, un-neutralized. The cached getter 1627 returns the real Document regardless of the clobber. */ 1628 const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument; 1629 return createNodeIterator.call(doc || root, root, 1630 // eslint-disable-next-line no-bitwise 1631 NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null); 1632 }; 1633 /** 1634 * Replace template expression syntax (mustache, ERB, template 1635 * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub 1636 * sites. Order matters: mustache, then ERB, then template literal. 1637 * 1638 * @param value the string to scrub 1639 * @returns the scrubbed string 1640 */ 1641 const _stripTemplateExpressions = function _stripTemplateExpressions(value) { 1642 value = stringReplace(value, MUSTACHE_EXPR$1, ' '); 1643 value = stringReplace(value, ERB_EXPR$1, ' '); 1644 value = stringReplace(value, TMPLIT_EXPR$1, ' '); 1645 return value; 1646 }; 1647 /** 1648 * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the 1649 * character data of an element subtree. Used as the final safety net for 1650 * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions 1651 * which only form after text-node normalization (e.g. fragments split across 1652 * stripped elements) cannot survive into a template-evaluating framework. 1653 * 1654 * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data` 1655 * in place rather than round-tripping through innerHTML. This preserves 1656 * descendant node references (important for IN_PLACE callers), avoids a 1657 * serialize/reparse cycle, and reads literal character data â which means 1658 * `<%...%>` in text content matches the ERB regex against its real bytes 1659 * instead of the HTML-entity-escaped form innerHTML would produce. 1660 * 1661 * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for 1662 * attributes is performed during the per-node `_sanitizeAttributes` pass. 1663 * 1664 * @param node The root element whose character data should be scrubbed. 1665 */ 1666 const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) { 1667 var _node$querySelectorAl; 1668 node.normalize(); 1669 /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator: 1670 under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may 1671 carry a form-named-getter override of ownerDocument. */ 1672 const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument; 1673 const walker = createNodeIterator.call(doc || node, node, 1674 // eslint-disable-next-line no-bitwise 1675 NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null); 1676 let currentNode = walker.nextNode(); 1677 while (currentNode) { 1678 currentNode.data = _stripTemplateExpressions(currentNode.data); 1679 currentNode = walker.nextNode(); 1680 } 1681 // NodeIterator does not descend into <template>.content per the DOM spec, 1682 // so we must explicitly recurse into each template's content fragment, 1683 // mirroring the approach used by _sanitizeShadowDOM. 1684 const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template'); 1685 if (templates) { 1686 arrayForEach(templates, tmpl => { 1687 if (_isDocumentFragment(tmpl.content)) { 1688 _scrubTemplateExpressions2(tmpl.content); 1689 } 1690 }); 1691 } 1692 }; 1693 /** 1694 * _isClobbered 1695 * 1696 * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML 1697 * interface with [LegacyOverrideBuiltIns]; a descendant element with a 1698 * `name` attribute matching a prototype property shadows that property 1699 * on direct reads. We use this check at the IN_PLACE entry-point and 1700 * during attribute sanitization to refuse clobbered forms. 1701 * 1702 * @param element element to check for clobbering attacks 1703 * @return true if clobbered, false if safe 1704 */ 1705 const _isClobbered = function _isClobbered(element) { 1706 // Realm-independent tag-name probe. If we can't determine the tag 1707 // name at all, we can't reason about clobbering â return false 1708 // (the caller's other defences still apply). 1709 const realTagName = getNodeName ? getNodeName(element) : null; 1710 if (typeof realTagName !== 'string') { 1711 return false; 1712 } 1713 if (transformCaseFunc(realTagName) !== 'form') { 1714 return false; 1715 } 1716 return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || 1717 // Realm-safe NamedNodeMap detection: equality against the cached 1718 // prototype getter. Clobbered .attributes (e.g. <input name="attributes">) 1719 // makes the direct read diverge from the cached read; a clean form 1720 // (same-realm OR foreign-realm) has both reads pointing at the same 1721 // canonical NamedNodeMap. 1722 element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' ||
1722 1723 // A form descendant named "removeAttributeNode" or "getAttributeNode" 1724 // shadows these Attr-node methods via [LegacyOverrideBuiltIns]. 1725 // _removeAttribute() / _stripAttributeNode() reach for 1726 // element.removeAttributeNode(attr) first; when it is shadowed the call 1727 // throws and the name-based fallback element.removeAttribute(name) 1728 // ASCII-lowercases its lookup key in an HTML document, silently missing 1729 // a case-preserved event-handler attribute (e.g. an ONANIMATIONSTART 1730 // that reached the sanitizer through an XML/XHTML parse). Flag the form 1731 // so it is removed wholesale, exactly as for the other shadowed methods. 1732 typeof element.removeAttributeNode !== 'function' || typeof element.getAttributeNod
vendor: 25,440 bytes, lines 1732-2223
1732e !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' || 1733 // NodeType clobbering probe. Cached Node.prototype.nodeType getter 1734 // returns the integer 1 for any Element regardless of realm; direct 1735 // read on a clobbered form (e.g. <input name="nodeType">) returns 1736 // the named child element. Cheap addition â nodeType is read from 1737 // an internal slot, no serialization cost â and removes a residual 1738 // clobbering surface used by several mXSS / PI / comment branches 1739 // in _sanitizeElements that compare currentNode.nodeType directly. 1740 element.nodeType !== getNodeType(element) || 1741 // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named 1742 // "childNodes" shadows the prototype getter. Direct reads of 1743 // form.childNodes from a clobbered form return the named child 1744 // instead of the real NodeList, so any walk that reads it directly 1745 // skips the form's real children. Compare the direct read to the 1746 // cached Node.prototype getter â when the form's named-property 1747 // getter intercepts the read, the two values differ and we flag 1748 // the form. This catches every clobbering child type (input, 1749 // select, etc.) regardless of whether the named child happens to 1750 // carry a numeric .length, which a typeof-based probe would miss 1751 // (e.g. HTMLSelectElement.length is a defined unsigned-long). 1752 element.childNodes !== getChildNodes(element); 1753 }; 1754 /** 1755 * Checks whether the given value is a DocumentFragment from any realm. 1756 * 1757 * The realm-independent replacement reads `nodeType` through the cached 1758 * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE 1759 * constant (11). nodeType is a numeric value resolved from the node's 1760 * internal slot, identical across realms for the same kind of node. 1761 * 1762 * @param value object to check 1763 * @return true if value is a DocumentFragment-shaped node from any realm 1764 */ 1765 const _isDocumentFragment = function _isDocumentFragment(value) { 1766 if (!getNodeType || typeof value !== 'object' || value === null) { 1767 return false; 1768 } 1769 try { 1770 return getNodeType(value) === NODE_TYPE.documentFragment; 1771 } catch (_) { 1772 return false; 1773 } 1774 }; 1775 /** 1776 * Checks whether the given object is a DOM node, including nodes that 1777 * originate from a different window/realm (e.g. an iframe's 1778 * contentDocument). The previous `value instanceof Node` check was 1779 * realm-bound: nodes from a different window failed it, causing 1780 * sanitize() to silently stringify them and reset IN_PLACE to false, 1781 * returning the original node unsanitized. See GHSA-4w3q-35jp-p934. 1782 * 1783 * @param value object to check whether it's a DOM node 1784 * @return true if value is a DOM node from any realm 1785 */ 1786 const _isNode = function _isNode(value) { 1787 if (!getNodeType || typeof value !== 'object' || value === null) { 1788 return false; 1789 } 1790 try { 1791 return typeof getNodeType(value) === 'number'; 1792 } catch (_) { 1793 return false; 1794 } 1795 }; 1796 function _executeHooks(hooks, currentNode, data) { 1797 if (hooks.length === 0) { 1798 return; 1799 } 1800 arrayForEach(hooks, hook => { 1801 hook.call(DOMPurify, currentNode, data, CONFIG); 1802 }); 1803 } 1804 /** 1805 * Structural-threat checks that condemn a node regardless of the 1806 * allowlists: mXSS via namespace confusion, risky CSS construction, 1807 * processing instructions, markup-bearing comments. Pure predicate; 1808 * the caller removes. Check order is load-bearing. 1809 * 1810 * @param currentNode the node to inspect 1811 * @param tagName the node's transformCaseFunc'd tag name 1812 * @return true if the node must be removed 1813 */ 1814 const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) { 1815 /* Detect mXSS attempts abusing namespace confusion */ 1816 if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) { 1817 return true; 1818 } 1819 /* Remove rawtext/literal-text elements whose literal serialization 1820 re-opens markup on an HTML reparse - shapes (a) and (b) documented at 1821 LITERAL_TEXT_ELEMENTS. Both are invisible to rule 1 above (which 1822 self-disables once there is an element child, and whose second probe 1823 reads the innerHTML an XML/XHTML document serializes escaped), which 1824 is why both probes here read textContent instead. Previously only 1825 `style`-with-element-child was covered; every element in 1826 LITERAL_TEXT_ELEMENTS shares this literal serialization and is 1827 equally affected. */ 1828 if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && LITERAL_TEXT_ELEMENTS[tagName] && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === 'string' && regExpTest(LITERAL_TEXT_CLOSE[tagName], currentNode.textContent))) { 1829 return true; 1830 } 1831 /* Remove any occurrence of processing instructions */ 1832 if (currentNode.nodeType === NODE_TYPE.processingInstruction) { 1833 return true; 1834 } 1835 /* Remove any kind of possibly harmful comments */ 1836 if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) { 1837 return true; 1838 } 1839 return false; 1840 }; 1841 /** 1842 * Evaluate a CUSTOM_ELEMENT_HANDLING check (a RegExp or a predicate 1843 * function, per the validation in _parseConfig) against a name. 1844 * Additional arguments are forwarded to predicate functions - the 1845 * attributeNameCheck predicate receives the tag name as its second 1846 * argument. A null/absent check never matches. 1847 * 1848 * @param check the configured tagNameCheck / attributeNameCheck value 1849 * @param name the name to test 1850 * @param args extra arguments forwarded to a predicate function 1851 * @return true if the check matches the name 1852 */ 1853 const _matchesNameCheck = function _matchesNameCheck(check, name) { 1854 if (check instanceof RegExp) { 1855 return regExpTest(check, name); 1856 } 1857 if (check instanceof Function) { 1858 for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { 1859 args[_key - 2] = arguments[_key]; 1860 } 1861 return Boolean(check(name, ...args)); 1862 } 1863 return false; 1864 }; 1865 /** 1866 * Handle a node whose tag is forbidden or not allowlisted: keep 1867 * allowed custom elements (false return exits _sanitizeElements 1868 * early - the namespace and fallback-tag removal checks are 1869 * intentionally skipped for kept custom elements), else hoist 1870 * content per KEEP_CONTENT and remove. 1871 * 1872 * A kept custom element is the ONLY case in which this function 1873 * returns false, so the caller uses that return value to run the 1874 * afterSanitizeElements hook on the kept element and keep the 1875 * element-hook lifecycle consistent with normal allowlisted 1876 * elements (GHSA-c2j3-45gr-mqc4). 1877 * 1878 * @param currentNode the disallowed node 1879 * @param tagName the node's transformCaseFunc'd tag name 1880 * @return true if the node was removed, false if kept 1881 */ 1882 const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) { 1883 /* Check if we have a custom element to handle */ 1884 if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { 1885 return false; 1886 } 1887 /* Keep content except for bad-listed elements. 1888 Use the cached prototype getters exclusively â the previous code 1889 had `|| currentNode.parentNode` / `|| currentNode.childNodes` 1890 fallbacks, but the cached getters always return the canonical 1891 value (or null for a real parent-less node), so the fallback 1892 path was dead in safe cases and a clobbering surface in unsafe 1893 ones. Falsy cached results stay falsy; the `if (childNodes && 1894 parentNode)` check already gates correctly. */ 1895 if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { 1896 const parentNode = getParentNode(currentNode); 1897 const childNodes = getChildNodes(currentNode); 1898 if (childNodes && parentNode) { 1899 const childCount = childNodes.length; 1900 /* Hoist by moving each child up one level rather than deep-cloning 1901 it. Moving transfers every descendant exactly once, so a chain of 1902 nested disallowed elements costs O(n) instead of the O(n^2) that 1903 re-cloning the shrinking subtree at each level produced; it also 1904 empties the removed original, so `DOMPurify.removed` no longer 1905 pins whole subtrees. Moving preserves the in-place guarantee too: 1906 an original carrying already-queued resource events (`<img 1907 onerror>`, `<video>`/`<audio>` error, lazy/`onload`, â¦) is 1908 relocated and sanitised rather than left detached but still armed. 1909 The sole case that must clone is removing the walk root itself. 1910 The result is serialised from the root's subtree, so a restrictive 1911 ALLOWED_TAGS that strips the root (`body` on the string path) must 1912 leave the content inside it, which only cloning does. In IN_PLACE 1913 the root is pre-validated as an allowed tag and so is never removed 1914 here, so that path always takes the move branch. 1915 `childNodes` is live; a tail-to-head walk keeps `childNodes[i]` 1916 valid whether we move (drops the trailing entry) or clone (leaves 1917 the list intact). */ 1918 for (let i = childCount - 1; i >= 0; --i) { 1919 const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i]; 1920 parentNode.insertBefore(hoisted, getNextSibling(currentNode)); 1921 } 1922 } 1923 } 1924 _forceRemove(currentNode); 1925 return true; 1926 }; 1927 /** 1928 * Fork a hook-mutable allowlist off its shared binding the first time a 1929 * (possibly lazily-installed) uponSanitize* hook is about to see it, so the 1930 * hook cannot widen the per-instance default or the setConfig binding by 1931 * reference and leak past the call. Returns the set unchanged once it is 1932 * already call-local, so repeated calls across elements are idempotent. 1933 * 1934 * @param hookList the uponSanitize* hook array for this event 1935 * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding 1936 * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant 1937 * @param setConfigSet the captured setConfig() binding, or null 1938 * @return a call-local clone if a hook is present and set is still shared, 1939 * else set unchanged 1940 */ 1941 const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) { 1942 if (hookList.length === 0) { 1943 return set; 1944 } 1945 return set === defaultSet || set === setConfigSet ? clone(set) : set; 1946 }; 1947 /** 1948 * Shared guard for a node that a hook has detached from the walk tree, 1949 * used after each element-hook site in _sanitizeElements. Detaching is a 1950 * long-standing user pattern (issue #469; draw.io-style foreignObject 1951 * filtering). Per the cached, unclobberable parentNode getter the node is 1952 * genuinely out of the tree, so it can reach neither the serialized 1953 * output nor an IN_PLACE live tree; treat it as removed and stop 1954 * processing it. Without this guard, the unsafe-node / namespace checks 1955 * would call _forceRemove on a parentless node and hit the REPORT-3 1956 * fail-closed throw â which exists for nodes DOMPurify wants gone but 1957 * *cannot* detach (clobbered / parentless roots), the opposite of a node 1958 * that is already safely gone. The walk root is exempt: a detached 1959 * IN_PLACE root is legitimate input and must still be fully sanitized, 1960 * and a kill-decision on it must keep hitting the REPORT-3 throw. 1961 * 1962 * Nodes detached by hooks stay the hook's responsibility for placement: 1963 * they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE 1964 * pass (which iterates DOMPurify.removed) does not reach them. But a 1965 * hook-detached subtree can still hold a queued resource-event handler - 1966 * e.g. an <img onload> that began loading when the caller built the live 1967 * tree - which fires in page scope after sanitize returns even though the 1968 * handler never reached the returned tree. That is the audit-5 F1 hazard, 1969 * and the documented node.remove() hook pattern walks straight into it. 1970 * So on the IN_PLACE path we neutralize the detached subtree inline, 1971 * stripping its non-allow-listed attributes before returning, exactly as 1972 * the post-walk pass does for _forceRemove'd subtrees. 1973 * 1974 * @param currentNode the node a hook may have detached 1975 * @param root the current walk root 1976 * @return true if the node is detached and now handled, false otherwise 1977 */ 1978 const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) { 1979 if (currentNode === root || getParentNode(currentNode) !== null) { 1980 return false; 1981 } 1982 if (IN_PLACE) { 1983 _neutralizeSubtree(currentNode); 1984 } 1985 return true; 1986 }; 1987 /** 1988 * _sanitizeElements 1989 * 1990 * @protect nodeName 1991 * @protect textContent 1992 * @protect removeChild 1993 * @param currentNode to check for permission to exist 1994 * @return true if node was killed, false if left alive 1995 */ 1996 const _sanitizeElements = function _sanitizeElements(currentNode, root) { 1997 /* Execute a hook if present */ 1998 _executeHooks(hooks.beforeSanitizeElements, currentNode, null); 1999 /* A hook may have detached the node - treat it as removed (see 2000 _handleHookDetachedNode for the full rationale). */ 2001 if (_handleHookDetachedNode(currentNode, root)) { 2002 return true; 2003 } 2004 /* Check if element is clobbered or can clobber */ 2005 if (_isClobbered(currentNode)) { 2006 _forceRemove(currentNode); 2007 return true; 2008 } 2009 /* Now let's check the element's type and name */ 2010 const tagName = transformCaseFunc(_readNodeName(currentNode)); 2011 /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement 2012 hook may have been installed after that guard sampled the hook arrays 2013 (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still 2014 aliasing a shared binding that a widening hook would mutate by 2015 reference. Fork it before exposing it to the hook. */ 2016 ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS); 2017 /* Execute a hook if present */ 2018 _executeHooks(hooks.uponSanitizeElement, currentNode, { 2019 tagName, 2020 allowedTags: ALLOWED_TAGS 2021 }); 2022 /* The uponSanitizeElement hook may have detached the node, exactly as 2023 above (see _handleHookDetachedNode for the full rationale). */ 2024 if (_handleHookDetachedNode(currentNode, root)) { 2025 return true; 2026 } 2027 /* Remove mXSS vectors, processing instructions and risky comments */ 2028 if (_isUnsafeNode(currentNode, tagName)) { 2029 _forceRemove(currentNode); 2030 return true; 2031 } 2032 /* Remove element if anything forbids its presence */ 2033 if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) { 2034 const removed = _sanitizeDisallowedNode(currentNode, tagName, root); 2035 /* A false return means the node is a custom element kept via 2036 CUSTOM_ELEMENT_HANDLING - the only keep path through 2037 _sanitizeDisallowedNode. Run afterSanitizeElements on it so the 2038 element-hook lifecycle matches normal allowlisted elements: a 2039 security policy applied in this hook (e.g. stripping an attribute 2040 from every surviving element) must not silently skip kept custom 2041 elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element 2042 tail below - the hook runs, then the walker's subsequent 2043 _sanitizeAttributes pass sanitizes the element's attributes. The 2044 deliberately skipped namespace and fallback-tag removal checks stay 2045 skipped; they are removal decisions, not the hook contract. */ 2046 if (removed === false) { 2047 _executeHooks(hooks.afterSanitizeElements, currentNode, null); 2048 } 2049 return removed; 2050 } 2051 /* Check whether element has a valid namespace. 2052 Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype 2053 nodeType getter rather than `instanceof Element`, which is realm- 2054 bound and short-circuits to false for any node minted in a different 2055 realm â letting a foreign-realm element with a forbidden namespace 2056 slip past the namespace check entirely. */ 2057 const nt = _readNodeType(currentNode); 2058 if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) { 2059 _forceRemove(currentNode); 2060 return true; 2061 } 2062 /* Make sure that older browsers don't get fallback-tag mXSS */ 2063 if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) { 2064 _forceRemove(currentNode); 2065 return true; 2066 } 2067 /* Sanitize element content to be template-safe */ 2068 if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) { 2069 /* Get the element's text content */ 2070 const content = _stripTemplateExpressions(currentNode.textContent); 2071 if (currentNode.textContent !== content) { 2072 arrayPush(DOMPurify.removed, { 2073 element: currentNode.cloneNode() 2074 }); 2075 currentNode.textContent = content; 2076 } 2077 } 2078 /* Execute a hook if present */ 2079 _executeHooks(hooks.afterSanitizeElements, currentNode, null); 2080 return false; 2081 }; 2082 /** 2083 * _isValidAttribute 2084 * 2085 * @param lcTag Lowercase tag name of containing element. 2086 * @param lcName Lowercase attribute name. 2087 * @param value Attribute value. 2088 * @return Returns true if `value` is valid, otherwise false. 2089 */ 2090 // eslint-disable-next-line complexity 2091 const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) { 2092 /* FORBID_ATTR must always win, even if ADD_ATTR predicate would allow it */ 2093 if (FORBID_ATTR[lcName]) { 2094 return false; 2095 } 2096 /* Reject declarative-partial-updates patch-linkage attributes 2097 (https://github.com/WICG/declarative-partial-updates). 2098 Empirical note (Chrome 150, verified â see 2099 test/declarative-patch-probe-v3.html): expansion is NOT applied after 2100 sanitization. For the string path it fires during sanitize()'s own 2101 parse, so the walk sees and sanitizes the fully materialized expanded 2102 tree â teleports into MathML/SVG integration points included; a 2103 weaponized `<template for>`->`<img onerror>` comes back with the handler 2104 stripped. For the IN_PLACE path it fires on connection, before the walk. 2105 Either way DOMPurify is NOT blind to the patch. 2106 This removal is therefore defense-in-depth rather than the sole barrier: 2107 it prevents live linkage from surviving into the OUTPUT and re-expanding 2108 in the caller's context, and keeps behaviour deterministic if a future 2109 engine defers expansion. `for` is legitimate only on <label>/<output>; 2110 anywhere else (notably <template for>) it links the element to a patch 2111 target and teleports or removes an arbitrary DOM range by id/marker name. 2112 `patchsrc` fetches remote markup and is treated as a script-loading 2113 mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the 2114 other structural-threat checks and stays overridable, consistent with 2115 the rest of the codebase. PI range markers are already removed by 2116 _isUnsafeNode. */ 2117 if (_isPatchLinkageAttribute(lcName, lcTag)) { 2118 return false; 2119 } 2120 /* Make sure attribute cannot clobber */ 2121 if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { 2122 return false; 2123 } 2124 const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag); 2125 /* Allow valid data-* attributes: At least one character after "-" 2126 (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) 2127 XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) 2128 We don't need to check the value; it's always URI safe. */ 2129 if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) { 2130 return true; 2131 } 2132 /* Allow valid aria-* attributes, the value is always URI safe */ 2133 if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) { 2134 return true; 2135 } 2136 /* A name outside the allowlist is acceptable on custom-element terms 2137 only. The value checks below are intentionally skipped in that case: 2138 if the user supplied a tagNameCheck we also allow derived custom 2139 elements using the same test, and attributes passing the configured 2140 attributeNameCheck are allowed as custom elements define these at 2141 their own discretion. */ 2142 if (!nameIsPermitted) { 2143 return ( 2144 // Condition a) covers a basically valid custom element tag name whose 2145 // tag passes the configured tagNameCheck and whose attribute name 2146 // passes the configured attributeNameCheck ... 2147 _isBasicCustomElement(lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName, lcTag) || 2148 // Condition b) covers an `is` attribute whose value passes the 2149 // configured tagNameCheck while customized built-in elements are 2150 // allowed. 2151 lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) 2152 ); 2153 } 2154 /* Check value is safe. First, is attr inert? If so, is safe */ 2155 if (URI_SAFE_ATTRIBUTES[lcName]) { 2156 return true; 2157 } 2158 /* Check no script, data or unknown possibly unsafe URI 2159 unless we know URI values are safe for that attribute */ 2160 if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) { 2161 return true; 2162 } 2163 /* Keep image data URIs alive if src/xlink:href is allowed */ 2164 /* Further prevent gadget XSS for dynamically built script tags */ 2165 if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) { 2166 return true; 2167 } 2168 /* Allow unknown protocols: This provides support for links that 2169 are handled by protocol handlers which may be unknown ahead of 2170 time, e.g. fb:, spotify: */ 2171 if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) { 2172 return true; 2173 } 2174 /* Only an empty (binary) value remains safe at this point; 2175 anything else is presumed unsafe, do not add it back */ 2176 return !value; 2177 }; 2178 /* Names the HTML spec reserves from valid-custom-element-name; these must 2179 * never be treated as basic custom elements even when a permissive 2180 * CUSTOM_ELEMENT_HANDLING.tagNameCheck is configured. */ 2181 const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, ['annotation-xml', 'color-profile', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'missing-glyph']); 2182 /** 2183 * _isBasicCustomElement 2184 * checks if at least one dash is included in tagName, and it's not the first char 2185 * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name 2186 * 2187 * @param tagName name of the tag of the node to sanitize 2188 * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false. 2189 */ 2190 const _isBasicCustomElement = function _isBasicCustomElement(tagName) { 2191 return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName); 2192 }; 2193 /** 2194 * Wrap an attribute value in the matching Trusted Types object when 2195 * the active policy requires it. Namespaced attributes pass through 2196 * unchanged (no TT support yet, see 2197 * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293). 2198 * 2199 * @param lcTag lowercase tag name of the containing element 2200 * @param lcName lowercase attribute name 2201 * @param namespaceURI the attribute's namespace, if any 2202 * @param value the attribute value to wrap 2203 * @return the value, wrapped when Trusted Types demand it 2204 */ 2205 const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) { 2206 if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) { 2207 switch (trustedTypes.getAttributeType(lcTag, lcName)) { 2208 case 'TrustedHTML': 2209 { 2210 return _createTrustedHTML(value); 2211 } 2212 case 'TrustedScriptURL': 2213 { 2214 return _createTrustedScriptURL(value); 2215 } 2216 } 2217 } 2218 return value; 2219 }; 2220 /** 2221 * Write a modified attribute value back onto the element. On 2222 * success, re-probe for clobbering introduced by the new value and 2223 * remove the element when found; otherwise
2223, when this writeback is the 2224 * recreate half of the SANITIZE_NAMED_PROPS remove-and-recreate, pop the 2225 * removal entry that path recorded so it does not show as removed. On 2226 * failure, remove the attribute instead. 2227 * 2228 * Returns true only on a clean write (the value was set and the new value 2229 * introduced no clobbering). The caller uses that, together with its own 2230 * knowledge of whether this attribute pushed a DOMPurify.removed record, to 2231 * decide whether to pop that record. The pop must happen ONLY for the 2232 * named-prop remove-and-recreate; popping on any other value change (trim, 2233 * template scrubbing, Trusted Types) would consume an unrelated _forceRemove 2234 * subtree-cleanup record and let that detached subtree keep a live event 2235 * handler through the IN_PLACE neutralization pass (SO-001). 2236 * 2237 * @param currentNode the element carrying the attribute 2238 * @param name the attribute name as present on the element 2239 * @param namespaceURI the attribute's namespace, if any 2240 * @param value the new attribute value 2241 * @return true if the value was written without introducing clobbering 2242 */ 2243 const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) { 2244 try { 2245 if (namespaceURI) { 2246 currentNode.setAttributeNS(namespaceURI, name, value); 2247 } else { 2248 /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ 2249 currentNode.setAttribute(name, value); 2250 } 2251 if (_isClobbered(currentNode)) { 2252 _forceRemove(currentNode); 2253 return false; 2254 } 2255 return true; 2256 } catch (_) { 2257 _removeAttribute(name, currentNode); 2258 return false;
vendor: 4,874 bytes, lines 2259-2366
2259 } 2260 }; 2261 /** 2262 * _sanitizeAttributes 2263 * 2264 * @protect attributes 2265 * @protect nodeName 2266 * @protect removeAttribute 2267 * @protect setAttribute 2268 * 2269 * @param currentNode to sanitize 2270 */ 2271 // eslint-disable-next-line complexity 2272 const _sanitizeAttributes = function _sanitizeAttributes(currentNode) { 2273 /* Execute a hook if present */ 2274 _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null); 2275 const attributes = currentNode.attributes; 2276 /* Check if we have attributes; if not we might have a text node */ 2277 if (!attributes || _isClobbered(currentNode)) { 2278 return; 2279 } 2280 /* Same lazy-install guard as uponSanitizeElement (see there): fork the 2281 attribute allowlist off its shared binding before a hook can see it. */ 2282 ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR); 2283 const hookEvent = { 2284 attrName: '', 2285 attrValue: '', 2286 keepAttr: true, 2287 allowedAttributes: ALLOWED_ATTR, 2288 forceKeepAttr: undefined 2289 }; 2290 let l = attributes.length; 2291 const lcTag = transformCaseFunc(currentNode.nodeName); 2292 /* Go backwards over all attributes; safely remove bad ones */ 2293 while (l--) { 2294 const attr = attributes[l]; 2295 const name = attr.name, 2296 namespaceURI = attr.namespaceURI, 2297 attrValue = attr.value; 2298 const lcName = transformCaseFunc(name); 2299 const initValue = attrValue; 2300 let value = name === 'value' ? initValue : stringTrim(initValue); 2301 /* Tracks whether this specific attribute pushed a DOMPurify.removed 2302 record that a later _setAttributeValue() writeback is expected to pop 2303 back off (the SANITIZE_NAMED_PROPS remove-and-recreate below is the 2304 only path that does). A value that merely changed via trim, template 2305 scrubbing, or Trusted Types pushes no such record, so its writeback 2306 must NOT pop â otherwise it consumes an unrelated _forceRemove subtree 2307 record and that detached subtree escapes the IN_PLACE handler- 2308 neutralization pass (SO-001). */ 2309 let recreatedNamedProp = false; 2310 /* Execute a hook if present */ 2311 hookEvent.attrName = lcName; 2312 hookEvent.attrValue = value; 2313 hookEvent.keepAttr = true; 2314 hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set 2315 _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent); 2316 value = hookEvent.attrValue; 2317 /* Full DOM Clobbering protection via namespace isolation, 2318 * Prefix id and name attributes with `user-content-` 2319 */ 2320 if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) { 2321 // Remove the attribute with this value 2322 _removeAttribute(name, currentNode, attr); 2323 // Prefix the value and later re-create the attribute with the sanitized value 2324 value = SANITIZE_NAMED_PROPS_PREFIX + value; 2325 // This attribute owns the record just pushed; its recreate writeback 2326 // is the one allowed to pop it back off. 2327 recreatedNamedProp = true; 2328 } 2329 // Else: already prefixed, leave the attribute alone â the prefix is 2330 // itself the clobbering protection, and re-applying it is incorrect. 2331 /* Work around a security issue with comments inside attributes */ 2332 if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) { 2333 _removeAttribute(name, currentNode, attr); 2334 continue; 2335 } 2336 /* Make sure we cannot easily use animated hrefs, even if animations are allowed */ 2337 if (lcName === 'attributename' && stringMatch(value, 'href')) { 2338 _removeAttribute(name, currentNode, attr); 2339 continue; 2340 } 2341 /* Did the hooks force-keep the attribute? */ 2342 if (hookEvent.forceKeepAttr) { 2343 continue; 2344 } 2345 /* Did the hooks approve of the attribute? */ 2346 if (!hookEvent.keepAttr) { 2347 _removeAttribute(name, currentNode, attr); 2348 continue; 2349 } 2350 /* Work around a security issue in jQuery 3.0 */ 2351 if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) { 2352 _removeAttribute(name, currentNode, attr); 2353 continue; 2354 } 2355 /* Sanitize attribute content to be template-safe */ 2356 if (SAFE_FOR_TEMPLATES) { 2357 value = _stripTemplateExpressions(value); 2358 } 2359 /* Is `value` valid for this attribute? */ 2360 if (!_isValidAttribute(lcTag, lcName, value)) { 2361 _removeAttribute(name, currentNode, attr); 2362 continue; 2363 } 2364 /* Handle attributes that require Trusted Types */ 2365 value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value); 2366 /* Handle invalid data-* attribute set by try-catching it */
2367 if (value !== initValue) { 2368 const cleanWrite = _setAttributeValue(currentNode, name, namespaceURI, value); 2369 /* Only the named-prop remove-and-recreate owns a DOMPurify.removed 2370 record to pop; a plain trim/template/Trusted-Types writeback owns 2371 none, so popping here would drop an unrelated subtree's cleanup 2372 record (SO-001). */ 2373 if (cleanWrite && recreatedNamedProp) { 2374 arrayPop(DOMPurify.removed); 2375 }
vendor: 13,096 bytes, lines 2375-2651
2375 2376 } 2377 } 2378 /* Execute a hook if present */ 2379 _executeHooks(hooks.afterSanitizeAttributes, currentNode, null); 2380 }; 2381 /** 2382 * _sanitizeShadowDOM 2383 * 2384 * @param fragment to iterate over recursively 2385 */ 2386 const _sanitizeShadowDOM2 = function _sanitizeShadowDOM(fragment) { 2387 let shadowNode = null; 2388 const shadowIterator = _createNodeIterator(fragment); 2389 /* Execute a hook if present */ 2390 _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null); 2391 while (shadowNode = shadowIterator.nextNode()) { 2392 /* Execute a hook if present */ 2393 _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null); 2394 /* Sanitize tags and elements */ 2395 _sanitizeElements(shadowNode, fragment); 2396 /* Check attributes next */ 2397 _sanitizeAttributes(shadowNode); 2398 /* Deep shadow DOM detected. 2399 Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the 2400 DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we 2401 recurse into <template>.content from foreign realms too. */ 2402 if (_isDocumentFragment(shadowNode.content)) { 2403 _sanitizeShadowDOM2(shadowNode.content); 2404 } 2405 /* An element iterated here may itself host an attached 2406 shadow root. The default NodeIterator does not enter shadow 2407 trees, so a shadow root nested inside template.content was 2408 previously reached by no walk at all (the pre-pass at 2409 _sanitizeAttachedShadowRoots descends via childNodes, which 2410 doesn't enter template.content; the template-content recursion 2411 above iterates the content but never inspected shadowRoot). 2412 Walk it explicitly. The nodeType guard avoids reading 2413 shadowRoot off text / comment / CDATA / PI nodes that the 2414 iterator also surfaces. */ 2415 if (_readNodeType(shadowNode) === NODE_TYPE.element) { 2416 const innerSr = getShadowRoot(shadowNode); 2417 if (_isDocumentFragment(innerSr)) { 2418 _sanitizeAttachedShadowRoots(innerSr); 2419 _sanitizeShadowDOM2(innerSr); 2420 } 2421 } 2422 } 2423 /* Execute a hook if present */ 2424 _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null); 2425 }; 2426 /** 2427 * _sanitizeAttachedShadowRoots 2428 * 2429 * Walks `root` and feeds every attached shadow root we encounter into 2430 * the existing _sanitizeShadowDOM pipeline. The default node iterator 2431 * does not descend into shadow trees, so nodes inside an attached 2432 * shadow root would otherwise be skipped entirely. 2433 * 2434 * Two real input paths put attached shadow roots in front of us: 2435 * 1. IN_PLACE on a DOM node that already has shadow roots attached. 2436 * 2. DOM-node input where importNode(dirty, true) deep-clones the 2437 * shadow root because it was created with `clonable: true`. 2438 * 2439 * This pass runs once, up front, so the main iteration loop (and the 2440 * existing _sanitizeShadowDOM template-content recursion) stay 2441 * untouched â string-input paths are not affected. 2442 * 2443 * @param root the subtree root to walk for attached shadow roots 2444 */ 2445 const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) { 2446 /* Iterative (explicit stack) rather than per-child recursion. DOM APIs 2447 impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data 2448 built straight into the DOM â the IN_PLACE surface) deeper than the JS 2449 call-stack budget would otherwise overflow native recursion here and 2450 throw at the IN_PLACE entry pre-pass, before a single node is 2451 sanitized, leaving the caller's live tree untouched (fail-open). See 2452 campaign-3 F4. A heap stack keeps depth off the call stack. 2453 Each work item is either a node to descend into, or a deferred 2454 `_sanitizeShadowDOM` for an already-walked shadow root. The deferred 2455 form preserves the original post-order discipline: a shadow root's 2456 nested shadow roots are discovered before the outer shadow is 2457 sanitized (which may remove hosts). Pushes are in reverse of the 2458 desired processing order (LIFO): template content, then children, then 2459 the shadow-sanitize, then the shadow walk â so the order matches the 2460 previous recursion exactly. */ 2461 const stack = [{ 2462 node: root, 2463 shadow: null 2464 }]; 2465 while (stack.length > 0) { 2466 const item = stack.pop(); 2467 /* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */ 2468 if (item.shadow) { 2469 _sanitizeShadowDOM2(item.shadow); 2470 continue; 2471 } 2472 const node = item.node; 2473 const nodeType = _readNodeType(node); 2474 const isElement = nodeType === NODE_TYPE.element; 2475 /* (pushed last â processed first) Children, snapshotted in reverse so 2476 the first child is processed first. Snapshotting matters because a 2477 hook may detach siblings mid-walk. */ 2478 const childNodes = getChildNodes(node); 2479 if (childNodes) { 2480 for (let i = childNodes.length - 1; i >= 0; --i) { 2481 stack.push({ 2482 node: childNodes[i], 2483 shadow: null 2484 }); 2485 } 2486 } 2487 /* (pushed before children â processed after them, matching the old 2488 "template content last" order) When the node is a <template>, 2489 descend into its content. */ 2490 if (isElement) { 2491 const rootName = getNodeName ? getNodeName(node) : null; 2492 if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') { 2493 const content = node.content; 2494 if (_isDocumentFragment(content)) { 2495 stack.push({ 2496 node: content, 2497 shadow: null 2498 }); 2499 } 2500 } 2501 } 2502 /* Shadow root (processed first): walk its subtree, then sanitise it. 2503 Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection 2504 rather than `instanceof DocumentFragment`, which is realm-bound and 2505 silently skipped foreign-realm shadow roots (e.g. 2506 iframe.contentDocument attachShadow). */ 2507 if (isElement) { 2508 const sr = getShadowRoot(node); 2509 if (_isDocumentFragment(sr)) { 2510 /* Push the deferred sanitise first so it pops after the shadow 2511 walk we push next, i.e. nested shadow roots are discovered 2512 before this one is sanitised. */ 2513 stack.push({ 2514 node: null, 2515 shadow: sr 2516 }, { 2517 node: sr, 2518 shadow: null 2519 }); 2520 } 2521 } 2522 } 2523 }; 2524 // eslint-disable-next-line complexity 2525 DOMPurify.sanitize = function (dirty) { 2526 let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 2527 let body = null; 2528 let importedNode = null; 2529 let currentNode = null; 2530 let returnNode = null; 2531 /* Make sure we have a string to sanitize. 2532 DO NOT return early, as this will return the wrong type if 2533 the user has requested a DOM object rather than a string */ 2534 IS_EMPTY_INPUT = !dirty; 2535 if (IS_EMPTY_INPUT) { 2536 dirty = '<!-->'; 2537 } 2538 /* Stringify, in case dirty is an object */ 2539 if (typeof dirty !== 'string' && !_isNode(dirty)) { 2540 dirty = stringifyValue(dirty); 2541 if (typeof dirty !== 'string') { 2542 throw typeErrorCreate('dirty is not a string, aborting'); 2543 } 2544 } 2545 /* Return dirty HTML if DOMPurify cannot run */ 2546 if (!DOMPurify.isSupported) { 2547 return dirty; 2548 } 2549 /* Assign config vars */ 2550 if (SET_CONFIG) { 2551 /* Persistent setConfig() path: _parseConfig is skipped, so the sets are 2552 * not re-derived per call. Restore them from the pristine bindings 2553 * captured at setConfig() time so a previous call's hook clone (mutated 2554 * below) does not carry over. */ 2555 ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS; 2556 ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR; 2557 } else { 2558 _parseConfig(cfg); 2559 } 2560 /* Clone the hook-mutable allowlists before the walk whenever an 2561 * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS 2562 * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so 2563 * a hook that widens them would otherwise mutate the shared set 2564 * permanently: across later calls and across every element. Cloning per 2565 * walk keeps documented in-call widening working while scoping it to the 2566 * call. A single guard for both config paths - the per-call path rebinds 2567 * the sets in _parseConfig each call, the persistent path restores them 2568 * from the captured bindings just above - so the two cannot diverge. */ 2569 if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) { 2570 ALLOWED_TAGS = clone(ALLOWED_TAGS); 2571 } 2572 if (hooks.uponSanitizeAttribute.length > 0) { 2573 ALLOWED_ATTR = clone(ALLOWED_ATTR); 2574 } 2575 /* Clean up removed elements */ 2576 DOMPurify.removed = []; 2577 /* Resolve IN_PLACE for this call without mutating persistent config. 2578 Writing the IN_PLACE closure variable here leaks under setConfig(), 2579 where _parseConfig is skipped on later calls: a single string call would 2580 disable in-place mode for every subsequent node call, returning a 2581 sanitized copy while leaving the caller's node â which in-place callers 2582 keep using and whose return value they ignore â unsanitized. REPORT-2. */ 2583 const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty); 2584 if (inPlace) { 2585 /* Declarative-partial-updates / streaming pre-pass: sever every patch 2586 linkage across the live tree BEFORE the walk, so no patch can fire 2587 mid-walk and inject into an already-processed region. Runs first, so 2588 it also covers the forbidden/clobbered roots that throw below. */ 2589 _neutralizePatchLinkage(dirty); 2590 /* Do some early pre-sanitization to avoid unsafe root nodes. 2591 Read nodeName through the cached prototype getter â a clobbering 2592 child named "nodeName" on the form root would otherwise shadow 2593 the property and let this check skip the root-allowlist 2594 validation entirely. */ 2595 const nn = _readNodeName(dirty); 2596 if (typeof nn === 'string') { 2597 const tagName = transformCaseFunc(nn); 2598 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { 2599 /* Fail closed on a live root: neutralize handlers/children before 2600 throwing, exactly as the mid-walk abort path does. */ 2601 _neutralizeRoot(dirty); 2602 throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place'); 2603 } 2604 } 2605 /* Pre-flight the root through _isClobbered. The iterator-driven 2606 removal path can not detach a parent-less root: _forceRemove 2607 falls through to Element.prototype.remove(), which per spec 2608 is a no-op on a node with no parent. A clobbered root would 2609 then survive the main loop with its attributes uninspected, 2610 because _sanitizeAttributes early-returns on _isClobbered. The 2611 result would be an attacker-controlled form, complete with any 2612 event-handler attributes the caller passed in, handed back to 2613 the application unsanitized. Refuse to sanitize such a root 2614 the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */ 2615 if (_isClobbered(dirty)) { 2616 /* Fail closed on a live clobbered root before throwing. 2617 _neutralizeRoot's reads are clobber-safe (cached getters); the 2618 form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */ 2619 _neutralizeRoot(dirty); 2620 throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place'); 2621 } 2622 /* Sanitize attached shadow roots before the main iterator runs. 2623 The iterator does not descend into shadow trees. Same fail-closed 2624 barrier as the main walk (campaign-3 F2): a custom-element reaction 2625 inside a shadow root could abort this pre-pass before the walk runs, 2626 which would otherwise leave the entire live tree unsanitized. */ 2627 try { 2628 _sanitizeAttachedShadowRoots(dirty); 2629 } catch (error) { 2630 _neutralizeRoot(dirty); 2631 throw error; 2632 } 2633 } else if (_isNode(dirty)) { 2634 /* If dirty is a DOM element, append to an empty document to avoid 2635 elements being stripped by the parser */ 2636 body = _initDocument('<!---->'); 2637 importedNode = body.ownerDocument.importNode(dirty, true); 2638 if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') { 2639 /* Node is already a body, use as is */ 2640 body = importedNode; 2641 } else if (importedNode.nodeName === 'HTML') { 2642 body = importedNode; 2643 } else { 2644 // eslint-disable-next-line unicorn/prefer-dom-node-append 2645 body.appendChild(importedNode); 2646 } 2647 /* Clonable shadow roots are deep-cloned by importNode(); sanitize 2648 them before the main iterator runs, since the iterator does not 2649 descend into shadow trees. The walk routes every read through a 2650 cached prototype getter so clobbering descendants on a form root 2651 cannot hide a shadow host from this pass.
2651 2652 Traverse from `body`, not `importedNode`: when importedNode is a 2653 DocumentFragment, the appendChild() above moves its children into 2654 `body` and leaves the fragment empty, so walking importedNode would 2655 scan nothing and miss every attached shadow host now under `body` 2656 (SO-003). In the BODY/HTML branches `body === importedNode`, so this 2657 is equivalent there; in the element branch `body` contains the 2658 appended element and its descendants. `body` covers all three. */ 2659 _sanitizeAttachedShadowRoots(body); 2660 } else { 2661 /* Exit directly if we have nothing to do */ 2662 if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && 2663 // eslint-disable-next-line unicorn/prefer-includes 2664 dirty.indexOf('<') === -1) { 2665 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty; 2666 } 2667 /* Initialize the document to work on */ 2668 body = _initDocument(dirty); 2669 /* Check we have a DOM node from the data */ 2670 if (!body) { 2671 return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ''; 2672 } 2673 } 2674 /* Remove first element node (ours) if FORCE_BODY is set */ 2675 if (body && FORCE_BODY) { 2676 _forceRemove(body.firstChild); 2677 } 2678 /* Get node iterator */ 2679 const walkRoot = inPlace ? dirty : body; 2680 /* Now start iterating over the created document. 2681 The walk runs inside an exception barrier (campaign-3 F2): a re-entrant 2682 engine/custom-element mutation can detach a node mid-walk so 2683 `_forceRemove`'s parentless guard throws, aborting the loop. Without the 2684 barrier the caller's in-place tree would be left half-sanitized with the 2685 unvisited tail still armed. _createNodeIterator itself is inside the 2686 barrier too: constructing the iterator dereferences the root's document, 2687 and any failure there (e.g. an exotic/clobbered root) must still fail 2688 closed rather than skip the neutralize. On any throw we fail closed - 2689 strip the in-place root bare - then rethrow so the existing throw 2690 contract is preserved. (String/DOM-copy paths never return the partial 2691 body, so the propagating throw is already fail-closed there.) */ 2692 try { 2693 const nodeIterator = _createNodeIterator(walkRoot);
vendor: 5,911 bytes, lines 2693-2840
2693 2694 while (currentNode = nodeIterator.nextNode()) { 2695 /* Sanitize tags and elements */ 2696 _sanitizeElements(currentNode, walkRoot); 2697 /* Check attributes next */ 2698 _sanitizeAttributes(currentNode); 2699 /* Shadow DOM detected, sanitize it. 2700 Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection 2701 instead of instanceof, so foreign-realm <template>.content is 2702 walked correctly. */ 2703 if (_isDocumentFragment(currentNode.content)) { 2704 _sanitizeShadowDOM2(currentNode.content); 2705 } 2706 } 2707 } catch (error) { 2708 if (inPlace) { 2709 _neutralizeRoot(dirty); 2710 /* Nodes _forceRemove'd earlier in the aborted walk are already 2711 detached from the root, so _neutralizeRoot's subtree pass does not 2712 reach them. Defuse them too, mirroring the success-path loop below. */ 2713 arrayForEach(DOMPurify.removed, entry => { 2714 if (entry.element) { 2715 _neutralizeSubtree(entry.element); 2716 } 2717 }); 2718 } 2719 throw error; 2720 } 2721 /* If we sanitized `dirty` in-place, return it. */ 2722 if (inPlace) { 2723 /* Fail-closed completion of the audit-5 F1 fix: every node removed from 2724 the caller's live tree is detached but may still hold a queued 2725 resource-event handler that fires in page scope after we return. The 2726 move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the 2727 non-allow-listed attributes off every other removed subtree (clobber, 2728 mXSS, namespace, comments, KEEP_CONTENT:false, â¦) so those handlers are 2729 cancelled before any event can fire. Runs synchronously, pre-return. */ 2730 arrayForEach(DOMPurify.removed, entry => { 2731 if (entry.element) { 2732 _neutralizeSubtree(entry.element); 2733 } 2734 }); 2735 if (SAFE_FOR_TEMPLATES) { 2736 _scrubTemplateExpressions2(dirty); 2737 } 2738 return dirty; 2739 } 2740 /* Return sanitized string or DOM */ 2741 if (RETURN_DOM) { 2742 if (SAFE_FOR_TEMPLATES) { 2743 _scrubTemplateExpressions2(body); 2744 } 2745 if (RETURN_DOM_FRAGMENT) { 2746 returnNode = createDocumentFragment.call(body.ownerDocument); 2747 while (body.firstChild) { 2748 // eslint-disable-next-line unicorn/prefer-dom-node-append 2749 returnNode.appendChild(body.firstChild); 2750 } 2751 } else { 2752 returnNode = body; 2753 } 2754 if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { 2755 /* 2756 AdoptNode() is not used because internal state is not reset 2757 (e.g. the past names map of a HTMLFormElement), this is safe 2758 in theory but we would rather not risk another attack vector. 2759 The state that is cloned by importNode() is explicitly defined 2760 by the specs. 2761 */ 2762 returnNode = importNode.call(originalDocument, returnNode, true); 2763 } 2764 return returnNode; 2765 } 2766 let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; 2767 /* Serialize doctype if allowed */ 2768 if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { 2769 serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\n' + serializedHTML; 2770 } 2771 /* Sanitize final string template-safe */ 2772 if (SAFE_FOR_TEMPLATES) { 2773 serializedHTML = _stripTemplateExpressions(serializedHTML); 2774 } 2775 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML; 2776 }; 2777 DOMPurify.setConfig = function () { 2778 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 2779 _parseConfig(cfg); 2780 SET_CONFIG = true; 2781 SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS; 2782 SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR; 2783 }; 2784 DOMPurify.clearConfig = function () { 2785 CONFIG = null; 2786 SET_CONFIG = false; 2787 SET_CONFIG_ALLOWED_TAGS = null; 2788 SET_CONFIG_ALLOWED_ATTR = null; 2789 // Drop any caller-supplied Trusted Types policy so it cannot poison later 2790 // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and 2791 // never recreated â Trusted Types throws on duplicate names) is restored by 2792 // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9. 2793 trustedTypesPolicy = defaultTrustedTypesPolicy; 2794 emptyHTML = ''; 2795 }; 2796 DOMPurify.isValidAttribute = function (tag, attr, value) { 2797 /* Initialize shared config vars if necessary. */ 2798 if (!CONFIG) { 2799 _parseConfig({}); 2800 } 2801 const lcTag = transformCaseFunc(tag); 2802 const lcName = transformCaseFunc(attr); 2803 return _isValidAttribute(lcTag, lcName, value); 2804 }; 2805 DOMPurify.addHook = function (entryPoint, hookFunction) { 2806 if (typeof hookFunction !== 'function') { 2807 return; 2808 } 2809 /* Reject unknown entry points. Without this, a non-hook key (e.g. 2810 * '__proto__') indexes off the prototype chain rather than a real 2811 * hook array, and arrayPush then writes to Object.prototype. Guard 2812 * with an own-property check against the known hook names. */ 2813 if (!objectHasOwnProperty(hooks, entryPoint)) { 2814 return; 2815 } 2816 arrayPush(hooks[entryPoint], hookFunction); 2817 }; 2818 DOMPurify.removeHook = function (entryPoint, hookFunction) { 2819 if (!objectHasOwnProperty(hooks, entryPoint)) { 2820 return undefined; 2821 } 2822 if (hookFunction !== undefined) { 2823 const index = arrayLastIndexOf(hooks[entryPoint], hookFunction); 2824 return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0]; 2825 } 2826 return arrayPop(hooks[entryPoint]); 2827 }; 2828 DOMPurify.removeHooks = function (entryPoint) { 2829 if (!objectHasOwnProperty(hooks, entryPoint)) { 2830 return; 2831 } 2832 hooks[entryPoint] = []; 2833 }; 2834 DOMPurify.removeAllHooks = function () { 2835 hooks = _createHooksMap(); 2836 }; 2837 return DOMPurify; 2838} 2839var purify = createDOMPurify(); 2840
2841// src/browser.ts 2842var browser_default = purify; 2843purify.sanitize.bind(purify); 2844purify.isSupported; 2845purify.addHook.bind(purify); 2846purify.removeHook.bind(purify); 2847purify.removeHooks.bind(purify); 2848purify.removeAllHooks.bind(purify); 2849purify.setConfig.bind(purify); 2850purify.clearConfig.bind(purify); 2851purify.isValidAttribute.bind(purify); 2852purify.version; 2853purify.removed; 2854 2855const UC_INIT_TIMEOUT_MS = 1e4; 2856const [isInitialized, setIsInitialized] = createSignal(false); 2857const [consentDetails, setConsentDetails] = createSignal(); 2858const [needsRefresh, setNeedsRefresh] = createSignal(false); 2859const consentStore = { 2860 consentDetails, 2861 needsRefresh 2862}; 2863const changeHandlers = []; 2864function onConsentChange(handler) { 2865 changeHandlers.push(handler); 2866 return () => { 2867 const index = changeHandlers.indexOf(handler); 2868 if (index >= 0) changeHandlers.splice(index, 1); 2869 }; 2870} 2871function notifySubscribers(details) { 2872 for (const handler of changeHandlers) { 2873 try { 2874 handler(details); 2875 } catch (error) { 2876 logger.error("[Consent] Subscriber threw:", error); 2877 } 2878 } 2879} 2880let listenersBound = false; 2881const pendingReadyCallbacks = []; 2882const activeTimeouts = /* @__PURE__ */ new Set(); 2883const flushReadyCallbacks = () => { 2884 if (!isInitialized()) return; 2885 const callbacks = pendingReadyCallbacks.splice(0, pendingReadyCallbacks.length); 2886 for (const callback of callbacks) { 2887 try { 2888 callback(); 2889 } catch (error) { 2890 logger.error("[Consent] Ready callback threw:", error); 2891 } 2892 } 2893}; 2894const handleInitialized = () => { 2895 logger.consent("Usercentrics initialized"); 2896 setIsInitialized(true); 2897 flushReadyCallbacks(); 2898}; 2899const handleConsentChange = (event) => { 2900 const detail = event.detail; 2901 logger.consent("Consent changed", { 2902 status: detail.consent.status, 2903 updatedBy: detail.consent.updatedBy 2904 }); 2905 setConsentDetails(detail); 2906 notifySubscribers(detail); 2907}; 2908const loadInitialConsentDetails = async () => { 2909 if (!window.__ucCmp) return; 2910 try { 2911 const initialized = await window.__ucCmp.isInitialized(); 2912 if (!initialized) return; 2913 logger.consent("Usercentrics already initialized"); 2914 setIsInitialized(true); 2915 flushReadyCallbacks(); 2916 const details = await window.__ucCmp.getConsentDetails(); 2917 logger.consent("Initial consent details loaded", { 2918 status: details.consent.status 2919 }); 2920 setConsentDetails(details); 2921 notifySubscribers(details); 2922 } catch (error) { 2923 logger.error("Failed to load initial consent details", error); 2924 } 2925};
2926const initConsentStore = () => { 2927 if (listenersBound) return; 2928 listenersBound = true; 2929 window.addEventListener("UC_UI_INITIALIZED", handleInitialized); 2930 window.addEventListener("UC_CONSENT", handleConsentChange); 2931 loadInitialConsentDetails(); 2932}; 2933const disposeConsentStore = () => { 2934 if (!listenersBound) return; 2935 activeTimeouts.forEach((t) => clearTimeout(t)); 2936 activeTimeouts.clear(); 2937 window.removeEventListener("UC_UI_INITIALIZED", handleInitialized); 2938 window.removeEventListener("UC_CONSENT", handleConsentChange); 2939 listenersBound = false; 2940 pendingReadyCallbacks.splice(0, pendingReadyCallbacks.length); 2941 changeHandlers.splice(0, changeHandlers.length); 2942 setIsInitialized(false); 2943}; 2944const whenReady = async (callback) => { 2945 initConsentStore(); 2946 if (isInitialized()) { 2947 return callback(); 2948 } 2949 return new Promise((resolve) => { 2950 const timeout = setTimeout(() => { 2951 activeTimeouts.delete(timeout); 2952 logger.warn("Usercentrics initialization timed out"); 2953 resolve(void 0); 2954 }, UC_INIT_TIMEOUT_MS); 2955 activeTimeouts.add(timeout); 2956 pendingReadyCallbacks.push(() => { 2957 activeTimeouts.delete(timeout); 2958 clearTimeout(timeout); 2959 resolve(callback()); 2960 }); 2961 }); 2962}; 2963const consentActions = { 2964 showConsentBanner: () => { 2965 logger.consent("Opening consent banner"); 2966 return whenReady(() => window.__ucCmp?.showFirstLayer()); 2967 }, 2968 showConsentSettings: () => { 2969 logger.consent("Opening consent settings"); 2970 return whenReady(() => window.__ucCmp?.showSecondLayer()); 2971 }, 2972 acceptAll: () => { 2973 logger.consent("Accepting all consents"); 2974 return whenReady(() => window.__ucCmp?.acceptAllConsents()); 2975 }, 2976 denyAll: () => { 2977 logger.consent("Denying all consents"); 2978 return whenReady(() => window.__ucCmp?.denyAllConsents()); 2979 }, 2980 updateServices: (consents) => { 2981 logger.consent("Updating service consents", { 2982 count: consents.length 2983 }); 2984 return whenReady(() => window.__ucCmp?.updateServicesConsents(consents)); 2985 }, 2986 updateCategories: (consents) => { 2987 logger.consent("Updating category consents", { 2988 count: consents.length 2989 }); 2990 return whenReady(() => window.__ucCmp?.updateCategoriesConsents(consents)); 2991 }, 2992 saveConsents: () => { 2993 logger.consent("Saving consents"); 2994 return whenReady(() => window.__ucCmp?.saveConsents()); 2995 }, 2996 refreshScripts: () => { 2997 logger.consent("Refreshing scripts"); 2998 return whenReady(() => window.__ucCmp?.refreshScripts()); 2999 }, 3000 markNeedsRefresh: () => { 3001 logger.consent("Marking page as needing refresh"); 3002 setNeedsRefresh(true); 3003 }, 3004 resetNeedsRefresh: () => { 3005 logger.consent("Resetting needs refresh flag"); 3006 setNeedsRefresh(false); 3007 }, 3008 refreshPage: () => { 3009 { 3010 logger.consent("Refreshing page"); 3011 window.location.reload(); 3012 } 3013 } 3014}; 3015 3016const UC_SERVICE_TO_EMBED_PLATFORM = { 3017 "BJz7qNsdj-7": "youtube", 3018 // "YouTube Video" 3019 ko1w5PpFl: "facebook" 3020 // "Facebook Pixel" 3021 // NOTE: The following platforms need Usercentrics service IDs before per-platform 3022 // consent can work. Find them in the UC admin panel â Service Settings â Details, 3023 // or via `window.__ucCmp.getConsentDetails()` â services â [id] in the browser console. 3024 // Until mapped, these platforms fall back to the ALL_ACCEPTED check in syncEmbedConsent(), 3025 // meaning embeds only load when the user accepts ALL services via the UC banner. 3026 // Granular per-platform consent (via the embed overlay buttons) will not persist for these. 3027 // 3028 // "SERVICE_ID": "instagram", 3029 // "SERVICE_ID": "linkedin", 3030 // "SERVICE_ID": "x", 3031 // "SERVICE_ID": "tiktok", 3032 // "SERVICE_ID": "spotify", 3033}; 3034const ALL_EMBED_PLATFORMS = ["youtube", "facebook", "instagram", "x", "tiktok", "spotify", "linkedin"]; 3035const EMBED_PLATFORM_TO_UC_SERVICE = Object.fromEntries(Object.entries(UC_SERVICE_TO_EMBED_PLATFORM).map(([ucId, platform]) => [platform, ucId])); 3036function hasEmbedConsent(platform) { 3037 const details = consentStore.consentDetails(); 3038 if (!details) return false; 3039 const allAccepted = details.consent.status === "ALL_ACCEPTED"; 3040 const serviceId = EMBED_PLATFORM_TO_UC_SERVICE[platform]; 3041 const service = serviceId ? details.services[serviceId] : void 0; 3042 if (allAccepted && !service) return true; 3043 return service?.consent?.given ?? false; 3044} 3045async function giveEmbedConsent(platform) { 3046 const serviceId = EMBED_PLATFORM_TO_UC_SERVICE[platform]; 3047 if (!serviceId) { 3048 logger.warn(`[EmbedConsent] No UC service ID mapped for ${platform} â consent will not persist across sessions`); 3049 return; 3050 } 3051 const details = consentStore.consentDetails();
3052 if (!details) { 3053 logger.warn(`[EmbedConsent] UC not initialized yet for ${platform} â consent will not persist`); 3054 return; 3055 } 3056 const service = details.services[serviceId]; 3057 if (!service) { 3058 logger.warn(`[EmbedConsent] UC service ${serviceId} not found for ${platform} â consent will not persist`); 3059 return; 3060 } 3061 logger.consent(`Accepting embed consent for ${platform} (UC service ID: ${serviceId})`); 3062 await consentActions.updateServices([{ 3063 id: serviceId, 3064 consent: true 3065 }]); 3066 await consentActions.saveConsents(); 3067} 3068function initEmbedConsentSync(dom) { 3069 function syncEmbedConsent(details) { 3070 const allAccepted = details.consent.status === "ALL_ACCEPTED"; 3071 for (const platform of ALL_EMBED_PLATFORMS) { 3072 const serviceId = EMBED_PLATFORM_TO_UC_SERVICE[platform]; 3073 const service = serviceId ? details.services[serviceId] : void 0; 3074 if (allAccepted && !service) { 3075 dom.load(platform); 3076 continue; 3077 } 3078 if (!service?.consent) continue; 3079 if (service.consent.given) { 3080 dom.load(platform); 3081 } else { 3082 dom.revoke(platform); 3083 } 3084 } 3085 } 3086 return onConsentChange(syncEmbedConsent); 3087} 3088 3089export { ArrowIcon as A, consentStore as a, initEmbedConsentSync as b, consentActions as c, disposeConsentStore as d, resolveOgImage as e, resolveMedia as f, browser_default as g, giveEmbedConsent as h, initConsentStore as i, hasEmbedConsent as j, appendParamsToMediaUrl as k, formatTime as l, hasFiniteDuration as m, onConsentChange as o, resolveMediaUrl as r }; 3090//# sourceMappingURL=embedConsentSync-D36RIyVL.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.