source: node_modules/dompurify/dist/purify.es.mjs

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 60.5 KB
RevLine 
[d24f17c]1/*! @license DOMPurify 3.0.8 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.8/LICENSE */
2
3const {
4 entries,
5 setPrototypeOf,
6 isFrozen,
7 getPrototypeOf,
8 getOwnPropertyDescriptor
9} = Object;
10let {
11 freeze,
12 seal,
13 create
14} = Object; // eslint-disable-line import/no-mutable-exports
15let {
16 apply,
17 construct
18} = typeof Reflect !== 'undefined' && Reflect;
19if (!freeze) {
20 freeze = function freeze(x) {
21 return x;
22 };
23}
24if (!seal) {
25 seal = function seal(x) {
26 return x;
27 };
28}
29if (!apply) {
30 apply = function apply(fun, thisValue, args) {
31 return fun.apply(thisValue, args);
32 };
33}
34if (!construct) {
35 construct = function construct(Func, args) {
36 return new Func(...args);
37 };
38}
39const arrayForEach = unapply(Array.prototype.forEach);
40const arrayPop = unapply(Array.prototype.pop);
41const arrayPush = unapply(Array.prototype.push);
42const stringToLowerCase = unapply(String.prototype.toLowerCase);
43const stringToString = unapply(String.prototype.toString);
44const stringMatch = unapply(String.prototype.match);
45const stringReplace = unapply(String.prototype.replace);
46const stringIndexOf = unapply(String.prototype.indexOf);
47const stringTrim = unapply(String.prototype.trim);
48const regExpTest = unapply(RegExp.prototype.test);
49const typeErrorCreate = unconstruct(TypeError);
50
51/**
52 * Creates a new function that calls the given function with a specified thisArg and arguments.
53 *
54 * @param {Function} func - The function to be wrapped and called.
55 * @returns {Function} A new function that calls the given function with a specified thisArg and arguments.
56 */
57function unapply(func) {
58 return function (thisArg) {
59 for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
60 args[_key - 1] = arguments[_key];
61 }
62 return apply(func, thisArg, args);
63 };
64}
65
66/**
67 * Creates a new function that constructs an instance of the given constructor function with the provided arguments.
68 *
69 * @param {Function} func - The constructor function to be wrapped and called.
70 * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.
71 */
72function unconstruct(func) {
73 return function () {
74 for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
75 args[_key2] = arguments[_key2];
76 }
77 return construct(func, args);
78 };
79}
80
81/**
82 * Add properties to a lookup table
83 *
84 * @param {Object} set - The set to which elements will be added.
85 * @param {Array} array - The array containing elements to be added to the set.
86 * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.
87 * @returns {Object} The modified set with added elements.
88 */
89function addToSet(set, array) {
90 let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;
91 if (setPrototypeOf) {
92 // Make 'in' and truthy checks like Boolean(set.constructor)
93 // independent of any properties defined on Object.prototype.
94 // Prevent prototype setters from intercepting set as a this value.
95 setPrototypeOf(set, null);
96 }
97 let l = array.length;
98 while (l--) {
99 let element = array[l];
100 if (typeof element === 'string') {
101 const lcElement = transformCaseFunc(element);
102 if (lcElement !== element) {
103 // Config presets (e.g. tags.js, attrs.js) are immutable.
104 if (!isFrozen(array)) {
105 array[l] = lcElement;
106 }
107 element = lcElement;
108 }
109 }
110 set[element] = true;
111 }
112 return set;
113}
114
115/**
116 * Clean up an array to harden against CSPP
117 *
118 * @param {Array} array - The array to be cleaned.
119 * @returns {Array} The cleaned version of the array
120 */
121function cleanArray(array) {
122 for (let index = 0; index < array.length; index++) {
123 if (getOwnPropertyDescriptor(array, index) === undefined) {
124 array[index] = null;
125 }
126 }
127 return array;
128}
129
130/**
131 * Shallow clone an object
132 *
133 * @param {Object} object - The object to be cloned.
134 * @returns {Object} A new object that copies the original.
135 */
136function clone(object) {
137 const newObject = create(null);
138 for (const [property, value] of entries(object)) {
139 if (getOwnPropertyDescriptor(object, property) !== undefined) {
140 if (Array.isArray(value)) {
141 newObject[property] = cleanArray(value);
142 } else if (value && typeof value === 'object' && value.constructor === Object) {
143 newObject[property] = clone(value);
144 } else {
145 newObject[property] = value;
146 }
147 }
148 }
149 return newObject;
150}
151
152/**
153 * This method automatically checks if the prop is function or getter and behaves accordingly.
154 *
155 * @param {Object} object - The object to look up the getter function in its prototype chain.
156 * @param {String} prop - The property name for which to find the getter function.
157 * @returns {Function} The getter function found in the prototype chain or a fallback function.
158 */
159function lookupGetter(object, prop) {
160 while (object !== null) {
161 const desc = getOwnPropertyDescriptor(object, prop);
162 if (desc) {
163 if (desc.get) {
164 return unapply(desc.get);
165 }
166 if (typeof desc.value === 'function') {
167 return unapply(desc.value);
168 }
169 }
170 object = getPrototypeOf(object);
171 }
172 function fallbackValue(element) {
173 console.warn('fallback value for', element);
174 return null;
175 }
176 return fallbackValue;
177}
178
179const 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', 'section', 'select', 'shadow', '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']);
180
181// SVG
182const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
183const 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']);
184
185// List of SVG elements that are disallowed by default.
186// We still need to know them so that we can do namespace
187// checks properly in case one wants to add them to
188// allow-list.
189const 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']);
190const 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']);
191
192// Similarly to SVG, we want to know all MathML elements,
193// even those that we disallow by default.
194const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
195const text = freeze(['#text']);
196
197const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', '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', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'xmlns', 'slot']);
198const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', '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', 'dur', 'edgemode', 'elevation', 'end', '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', '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', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', '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', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', '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']);
199const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', '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']);
200const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
201
202// eslint-disable-next-line unicorn/better-regex
203const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
204const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
205const TMPLIT_EXPR = seal(/\${[\w\W]*}/gm);
206const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape
207const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
208const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
209);
210
211const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
212const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
213);
214
215const DOCTYPE_NAME = seal(/^html$/i);
216
217var EXPRESSIONS = /*#__PURE__*/Object.freeze({
218 __proto__: null,
219 MUSTACHE_EXPR: MUSTACHE_EXPR,
220 ERB_EXPR: ERB_EXPR,
221 TMPLIT_EXPR: TMPLIT_EXPR,
222 DATA_ATTR: DATA_ATTR,
223 ARIA_ATTR: ARIA_ATTR,
224 IS_ALLOWED_URI: IS_ALLOWED_URI,
225 IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
226 ATTR_WHITESPACE: ATTR_WHITESPACE,
227 DOCTYPE_NAME: DOCTYPE_NAME
228});
229
230const getGlobal = function getGlobal() {
231 return typeof window === 'undefined' ? null : window;
232};
233
234/**
235 * Creates a no-op policy for internal use only.
236 * Don't export this function outside this module!
237 * @param {TrustedTypePolicyFactory} trustedTypes The policy factory.
238 * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).
239 * @return {TrustedTypePolicy} The policy created (or null, if Trusted Types
240 * are not supported or creating the policy failed).
241 */
242const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {
243 if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {
244 return null;
245 }
246
247 // Allow the callers to control the unique policy name
248 // by adding a data-tt-policy-suffix to the script element with the DOMPurify.
249 // Policy creation with duplicate names throws in Trusted Types.
250 let suffix = null;
251 const ATTR_NAME = 'data-tt-policy-suffix';
252 if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {
253 suffix = purifyHostElement.getAttribute(ATTR_NAME);
254 }
255 const policyName = 'dompurify' + (suffix ? '#' + suffix : '');
256 try {
257 return trustedTypes.createPolicy(policyName, {
258 createHTML(html) {
259 return html;
260 },
261 createScriptURL(scriptUrl) {
262 return scriptUrl;
263 }
264 });
265 } catch (_) {
266 // Policy creation failed (most likely another DOMPurify script has
267 // already run). Skip creating the policy, as this will only cause errors
268 // if TT are enforced.
269 console.warn('TrustedTypes policy ' + policyName + ' could not be created.');
270 return null;
271 }
272};
273function createDOMPurify() {
274 let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
275 const DOMPurify = root => createDOMPurify(root);
276
277 /**
278 * Version label, exposed for easier checks
279 * if DOMPurify is up to date or not
280 */
281 DOMPurify.version = '3.0.8';
282
283 /**
284 * Array of elements that DOMPurify removed during sanitation.
285 * Empty if nothing was removed.
286 */
287 DOMPurify.removed = [];
288 if (!window || !window.document || window.document.nodeType !== 9) {
289 // Not running in a browser, provide a factory function
290 // so that you can pass your own Window
291 DOMPurify.isSupported = false;
292 return DOMPurify;
293 }
294 let {
295 document
296 } = window;
297 const originalDocument = document;
298 const currentScript = originalDocument.currentScript;
299 const {
300 DocumentFragment,
301 HTMLTemplateElement,
302 Node,
303 Element,
304 NodeFilter,
305 NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
306 HTMLFormElement,
307 DOMParser,
308 trustedTypes
309 } = window;
310 const ElementPrototype = Element.prototype;
311 const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
312 const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
313 const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
314 const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
315
316 // As per issue #47, the web-components registry is inherited by a
317 // new document created via createHTMLDocument. As per the spec
318 // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
319 // a new empty registry is used when creating a template contents owner
320 // document, so we use that as our parent document to ensure nothing
321 // is inherited.
322 if (typeof HTMLTemplateElement === 'function') {
323 const template = document.createElement('template');
324 if (template.content && template.content.ownerDocument) {
325 document = template.content.ownerDocument;
326 }
327 }
328 let trustedTypesPolicy;
329 let emptyHTML = '';
330 const {
331 implementation,
332 createNodeIterator,
333 createDocumentFragment,
334 getElementsByTagName
335 } = document;
336 const {
337 importNode
338 } = originalDocument;
339 let hooks = {};
340
341 /**
342 * Expose whether this browser supports running the full DOMPurify.
343 */
344 DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
345 const {
346 MUSTACHE_EXPR,
347 ERB_EXPR,
348 TMPLIT_EXPR,
349 DATA_ATTR,
350 ARIA_ATTR,
351 IS_SCRIPT_OR_DATA,
352 ATTR_WHITESPACE
353 } = EXPRESSIONS;
354 let {
355 IS_ALLOWED_URI: IS_ALLOWED_URI$1
356 } = EXPRESSIONS;
357
358 /**
359 * We consider the elements and attributes below to be safe. Ideally
360 * don't add any new ones but feel free to remove unwanted ones.
361 */
362
363 /* allowed element names */
364 let ALLOWED_TAGS = null;
365 const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);
366
367 /* Allowed attribute names */
368 let ALLOWED_ATTR = null;
369 const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);
370
371 /*
372 * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements.
373 * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)
374 * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)
375 * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.
376 */
377 let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {
378 tagNameCheck: {
379 writable: true,
380 configurable: false,
381 enumerable: true,
382 value: null
383 },
384 attributeNameCheck: {
385 writable: true,
386 configurable: false,
387 enumerable: true,
388 value: null
389 },
390 allowCustomizedBuiltInElements: {
391 writable: true,
392 configurable: false,
393 enumerable: true,
394 value: false
395 }
396 }));
397
398 /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
399 let FORBID_TAGS = null;
400
401 /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
402 let FORBID_ATTR = null;
403
404 /* Decide if ARIA attributes are okay */
405 let ALLOW_ARIA_ATTR = true;
406
407 /* Decide if custom data attributes are okay */
408 let ALLOW_DATA_ATTR = true;
409
410 /* Decide if unknown protocols are okay */
411 let ALLOW_UNKNOWN_PROTOCOLS = false;
412
413 /* Decide if self-closing tags in attributes are allowed.
414 * Usually removed due to a mXSS issue in jQuery 3.0 */
415 let ALLOW_SELF_CLOSE_IN_ATTR = true;
416
417 /* Output should be safe for common template engines.
418 * This means, DOMPurify removes data attributes, mustaches and ERB
419 */
420 let SAFE_FOR_TEMPLATES = false;
421
422 /* Decide if document with <html>... should be returned */
423 let WHOLE_DOCUMENT = false;
424
425 /* Track whether config is already set on this instance of DOMPurify. */
426 let SET_CONFIG = false;
427
428 /* Decide if all elements (e.g. style, script) must be children of
429 * document.body. By default, browsers might move them to document.head */
430 let FORCE_BODY = false;
431
432 /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
433 * string (or a TrustedHTML object if Trusted Types are supported).
434 * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
435 */
436 let RETURN_DOM = false;
437
438 /* Decide if a DOM `DocumentFragment` should be returned, instead of a html
439 * string (or a TrustedHTML object if Trusted Types are supported) */
440 let RETURN_DOM_FRAGMENT = false;
441
442 /* Try to return a Trusted Type object instead of a string, return a string in
443 * case Trusted Types are not supported */
444 let RETURN_TRUSTED_TYPE = false;
445
446 /* Output should be free from DOM clobbering attacks?
447 * This sanitizes markups named with colliding, clobberable built-in DOM APIs.
448 */
449 let SANITIZE_DOM = true;
450
451 /* Achieve full DOM Clobbering protection by isolating the namespace of named
452 * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.
453 *
454 * HTML/DOM spec rules that enable DOM Clobbering:
455 * - Named Access on Window (§7.3.3)
456 * - DOM Tree Accessors (§3.1.5)
457 * - Form Element Parent-Child Relations (§4.10.3)
458 * - Iframe srcdoc / Nested WindowProxies (§4.8.5)
459 * - HTMLCollection (§4.2.10.2)
460 *
461 * Namespace isolation is implemented by prefixing `id` and `name` attributes
462 * with a constant string, i.e., `user-content-`
463 */
464 let SANITIZE_NAMED_PROPS = false;
465 const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';
466
467 /* Keep element content when removing element? */
468 let KEEP_CONTENT = true;
469
470 /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
471 * of importing it into a new Document and returning a sanitized copy */
472 let IN_PLACE = false;
473
474 /* Allow usage of profiles like html, svg and mathMl */
475 let USE_PROFILES = {};
476
477 /* Tags to ignore content of when KEEP_CONTENT is true */
478 let FORBID_CONTENTS = null;
479 const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
480
481 /* Tags that are safe for data: URIs */
482 let DATA_URI_TAGS = null;
483 const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
484
485 /* Attributes safe for values like "javascript:" */
486 let URI_SAFE_ATTRIBUTES = null;
487 const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);
488 const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
489 const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
490 const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
491 /* Document namespace */
492 let NAMESPACE = HTML_NAMESPACE;
493 let IS_EMPTY_INPUT = false;
494
495 /* Allowed XHTML+XML namespaces */
496 let ALLOWED_NAMESPACES = null;
497 const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
498
499 /* Parsing of strict XHTML documents */
500 let PARSER_MEDIA_TYPE = null;
501 const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];
502 const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';
503 let transformCaseFunc = null;
504
505 /* Keep a reference to config to pass to hooks */
506 let CONFIG = null;
507
508 /* Ideally, do not touch anything below this line */
509 /* ______________________________________________ */
510
511 const formElement = document.createElement('form');
512 const isRegexOrFunction = function isRegexOrFunction(testValue) {
513 return testValue instanceof RegExp || testValue instanceof Function;
514 };
515
516 /**
517 * _parseConfig
518 *
519 * @param {Object} cfg optional config literal
520 */
521 // eslint-disable-next-line complexity
522 const _parseConfig = function _parseConfig() {
523 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
524 if (CONFIG && CONFIG === cfg) {
525 return;
526 }
527
528 /* Shield configuration object from tampering */
529 if (!cfg || typeof cfg !== 'object') {
530 cfg = {};
531 }
532
533 /* Shield configuration object from prototype pollution */
534 cfg = clone(cfg);
535 PARSER_MEDIA_TYPE =
536 // eslint-disable-next-line unicorn/prefer-includes
537 SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;
538
539 // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
540 transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
541
542 /* Set configuration parameters */
543 ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
544 ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
545 ALLOWED_NAMESPACES = 'ALLOWED_NAMESPACES' in cfg ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
546 URI_SAFE_ATTRIBUTES = 'ADD_URI_SAFE_ATTR' in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES),
547 // eslint-disable-line indent
548 cfg.ADD_URI_SAFE_ATTR,
549 // eslint-disable-line indent
550 transformCaseFunc // eslint-disable-line indent
551 ) // eslint-disable-line indent
552 : DEFAULT_URI_SAFE_ATTRIBUTES;
553 DATA_URI_TAGS = 'ADD_DATA_URI_TAGS' in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS),
554 // eslint-disable-line indent
555 cfg.ADD_DATA_URI_TAGS,
556 // eslint-disable-line indent
557 transformCaseFunc // eslint-disable-line indent
558 ) // eslint-disable-line indent
559 : DEFAULT_DATA_URI_TAGS;
560 FORBID_CONTENTS = 'FORBID_CONTENTS' in cfg ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
561 FORBID_TAGS = 'FORBID_TAGS' in cfg ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {};
562 FORBID_ATTR = 'FORBID_ATTR' in cfg ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {};
563 USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false;
564 ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
565 ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
566 ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
567 ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true
568 SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
569 WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
570 RETURN_DOM = cfg.RETURN_DOM || false; // Default false
571 RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
572 RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
573 FORCE_BODY = cfg.FORCE_BODY || false; // Default false
574 SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
575 SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
576 KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
577 IN_PLACE = cfg.IN_PLACE || false; // Default false
578 IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
579 NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
580 CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};
581 if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
582 CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
583 }
584 if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
585 CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
586 }
587 if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
588 CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
589 }
590 if (SAFE_FOR_TEMPLATES) {
591 ALLOW_DATA_ATTR = false;
592 }
593 if (RETURN_DOM_FRAGMENT) {
594 RETURN_DOM = true;
595 }
596
597 /* Parse profile info */
598 if (USE_PROFILES) {
599 ALLOWED_TAGS = addToSet({}, text);
600 ALLOWED_ATTR = [];
601 if (USE_PROFILES.html === true) {
602 addToSet(ALLOWED_TAGS, html$1);
603 addToSet(ALLOWED_ATTR, html);
604 }
605 if (USE_PROFILES.svg === true) {
606 addToSet(ALLOWED_TAGS, svg$1);
607 addToSet(ALLOWED_ATTR, svg);
608 addToSet(ALLOWED_ATTR, xml);
609 }
610 if (USE_PROFILES.svgFilters === true) {
611 addToSet(ALLOWED_TAGS, svgFilters);
612 addToSet(ALLOWED_ATTR, svg);
613 addToSet(ALLOWED_ATTR, xml);
614 }
615 if (USE_PROFILES.mathMl === true) {
616 addToSet(ALLOWED_TAGS, mathMl$1);
617 addToSet(ALLOWED_ATTR, mathMl);
618 addToSet(ALLOWED_ATTR, xml);
619 }
620 }
621
622 /* Merge configuration parameters */
623 if (cfg.ADD_TAGS) {
624 if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
625 ALLOWED_TAGS = clone(ALLOWED_TAGS);
626 }
627 addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
628 }
629 if (cfg.ADD_ATTR) {
630 if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
631 ALLOWED_ATTR = clone(ALLOWED_ATTR);
632 }
633 addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
634 }
635 if (cfg.ADD_URI_SAFE_ATTR) {
636 addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
637 }
638 if (cfg.FORBID_CONTENTS) {
639 if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
640 FORBID_CONTENTS = clone(FORBID_CONTENTS);
641 }
642 addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
643 }
644
645 /* Add #text in case KEEP_CONTENT is set to true */
646 if (KEEP_CONTENT) {
647 ALLOWED_TAGS['#text'] = true;
648 }
649
650 /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */
651 if (WHOLE_DOCUMENT) {
652 addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);
653 }
654
655 /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */
656 if (ALLOWED_TAGS.table) {
657 addToSet(ALLOWED_TAGS, ['tbody']);
658 delete FORBID_TAGS.tbody;
659 }
660 if (cfg.TRUSTED_TYPES_POLICY) {
661 if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
662 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
663 }
664 if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
665 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
666 }
667
668 // Overwrite existing TrustedTypes policy.
669 trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
670
671 // Sign local variables required by `sanitize`.
672 emptyHTML = trustedTypesPolicy.createHTML('');
673 } else {
674 // Uninitialized policy, attempt to initialize the internal dompurify policy.
675 if (trustedTypesPolicy === undefined) {
676 trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
677 }
678
679 // If creating the internal policy succeeded sign internal variables.
680 if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
681 emptyHTML = trustedTypesPolicy.createHTML('');
682 }
683 }
684
685 // Prevent further manipulation of configuration.
686 // Not available in IE8, Safari 5, etc.
687 if (freeze) {
688 freeze(cfg);
689 }
690 CONFIG = cfg;
691 };
692 const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
693 const HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']);
694
695 // Certain elements are allowed in both SVG and HTML
696 // namespace. We need to specify them explicitly
697 // so that they don't get erroneously deleted from
698 // HTML namespace.
699 const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']);
700
701 /* Keep track of all possible SVG and MathML tags
702 * so that we can perform the namespace checks
703 * correctly. */
704 const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
705 const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
706
707 /**
708 * @param {Element} element a DOM element whose namespace is being checked
709 * @returns {boolean} Return false if the element has a
710 * namespace that a spec-compliant parser would never
711 * return. Return true otherwise.
712 */
713 const _checkValidNamespace = function _checkValidNamespace(element) {
714 let parent = getParentNode(element);
715
716 // In JSDOM, if we're inside shadow DOM, then parentNode
717 // can be null. We just simulate parent in this case.
718 if (!parent || !parent.tagName) {
719 parent = {
720 namespaceURI: NAMESPACE,
721 tagName: 'template'
722 };
723 }
724 const tagName = stringToLowerCase(element.tagName);
725 const parentTagName = stringToLowerCase(parent.tagName);
726 if (!ALLOWED_NAMESPACES[element.namespaceURI]) {
727 return false;
728 }
729 if (element.namespaceURI === SVG_NAMESPACE) {
730 // The only way to switch from HTML namespace to SVG
731 // is via <svg>. If it happens via any other tag, then
732 // it should be killed.
733 if (parent.namespaceURI === HTML_NAMESPACE) {
734 return tagName === 'svg';
735 }
736
737 // The only way to switch from MathML to SVG is via`
738 // svg if parent is either <annotation-xml> or MathML
739 // text integration points.
740 if (parent.namespaceURI === MATHML_NAMESPACE) {
741 return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
742 }
743
744 // We only allow elements that are defined in SVG
745 // spec. All others are disallowed in SVG namespace.
746 return Boolean(ALL_SVG_TAGS[tagName]);
747 }
748 if (element.namespaceURI === MATHML_NAMESPACE) {
749 // The only way to switch from HTML namespace to MathML
750 // is via <math>. If it happens via any other tag, then
751 // it should be killed.
752 if (parent.namespaceURI === HTML_NAMESPACE) {
753 return tagName === 'math';
754 }
755
756 // The only way to switch from SVG to MathML is via
757 // <math> and HTML integration points
758 if (parent.namespaceURI === SVG_NAMESPACE) {
759 return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
760 }
761
762 // We only allow elements that are defined in MathML
763 // spec. All others are disallowed in MathML namespace.
764 return Boolean(ALL_MATHML_TAGS[tagName]);
765 }
766 if (element.namespaceURI === HTML_NAMESPACE) {
767 // The only way to switch from SVG to HTML is via
768 // HTML integration points, and from MathML to HTML
769 // is via MathML text integration points
770 if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
771 return false;
772 }
773 if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
774 return false;
775 }
776
777 // We disallow tags that are specific for MathML
778 // or SVG and should never appear in HTML namespace
779 return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
780 }
781
782 // For XHTML and XML documents that support custom namespaces
783 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
784 return true;
785 }
786
787 // The code should never reach this place (this means
788 // that the element somehow got namespace that is not
789 // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).
790 // Return false just in case.
791 return false;
792 };
793
794 /**
795 * _forceRemove
796 *
797 * @param {Node} node a DOM node
798 */
799 const _forceRemove = function _forceRemove(node) {
800 arrayPush(DOMPurify.removed, {
801 element: node
802 });
803 try {
804 // eslint-disable-next-line unicorn/prefer-dom-node-remove
805 node.parentNode.removeChild(node);
806 } catch (_) {
807 node.remove();
808 }
809 };
810
811 /**
812 * _removeAttribute
813 *
814 * @param {String} name an Attribute name
815 * @param {Node} node a DOM node
816 */
817 const _removeAttribute = function _removeAttribute(name, node) {
818 try {
819 arrayPush(DOMPurify.removed, {
820 attribute: node.getAttributeNode(name),
821 from: node
822 });
823 } catch (_) {
824 arrayPush(DOMPurify.removed, {
825 attribute: null,
826 from: node
827 });
828 }
829 node.removeAttribute(name);
830
831 // We void attribute values for unremovable "is"" attributes
832 if (name === 'is' && !ALLOWED_ATTR[name]) {
833 if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
834 try {
835 _forceRemove(node);
836 } catch (_) {}
837 } else {
838 try {
839 node.setAttribute(name, '');
840 } catch (_) {}
841 }
842 }
843 };
844
845 /**
846 * _initDocument
847 *
848 * @param {String} dirty a string of dirty markup
849 * @return {Document} a DOM, filled with the dirty markup
850 */
851 const _initDocument = function _initDocument(dirty) {
852 /* Create a HTML document */
853 let doc = null;
854 let leadingWhitespace = null;
855 if (FORCE_BODY) {
856 dirty = '<remove></remove>' + dirty;
857 } else {
858 /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */
859 const matches = stringMatch(dirty, /^[\r\n\t ]+/);
860 leadingWhitespace = matches && matches[0];
861 }
862 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) {
863 // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
864 dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
865 }
866 const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
867 /*
868 * Use the DOMParser API by default, fallback later if needs be
869 * DOMParser not work for svg when has multiple root element.
870 */
871 if (NAMESPACE === HTML_NAMESPACE) {
872 try {
873 doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
874 } catch (_) {}
875 }
876
877 /* Use createHTMLDocument in case DOMParser is not available */
878 if (!doc || !doc.documentElement) {
879 doc = implementation.createDocument(NAMESPACE, 'template', null);
880 try {
881 doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;
882 } catch (_) {
883 // Syntax error if dirtyPayload is invalid xml
884 }
885 }
886 const body = doc.body || doc.documentElement;
887 if (dirty && leadingWhitespace) {
888 body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);
889 }
890
891 /* Work on whole document or just its body */
892 if (NAMESPACE === HTML_NAMESPACE) {
893 return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];
894 }
895 return WHOLE_DOCUMENT ? doc.documentElement : body;
896 };
897
898 /**
899 * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.
900 *
901 * @param {Node} root The root element or node to start traversing on.
902 * @return {NodeIterator} The created NodeIterator
903 */
904 const _createNodeIterator = function _createNodeIterator(root) {
905 return createNodeIterator.call(root.ownerDocument || root, root,
906 // eslint-disable-next-line no-bitwise
907 NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, null);
908 };
909
910 /**
911 * _isClobbered
912 *
913 * @param {Node} elm element to check for clobbering attacks
914 * @return {Boolean} true if clobbered, false if safe
915 */
916 const _isClobbered = function _isClobbered(elm) {
917 return elm instanceof HTMLFormElement && (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function');
918 };
919
920 /**
921 * Checks whether the given object is a DOM node.
922 *
923 * @param {Node} object object to check whether it's a DOM node
924 * @return {Boolean} true is object is a DOM node
925 */
926 const _isNode = function _isNode(object) {
927 return typeof Node === 'function' && object instanceof Node;
928 };
929
930 /**
931 * _executeHook
932 * Execute user configurable hooks
933 *
934 * @param {String} entryPoint Name of the hook's entry point
935 * @param {Node} currentNode node to work on with the hook
936 * @param {Object} data additional hook parameters
937 */
938 const _executeHook = function _executeHook(entryPoint, currentNode, data) {
939 if (!hooks[entryPoint]) {
940 return;
941 }
942 arrayForEach(hooks[entryPoint], hook => {
943 hook.call(DOMPurify, currentNode, data, CONFIG);
944 });
945 };
946
947 /**
948 * _sanitizeElements
949 *
950 * @protect nodeName
951 * @protect textContent
952 * @protect removeChild
953 *
954 * @param {Node} currentNode to check for permission to exist
955 * @return {Boolean} true if node was killed, false if left alive
956 */
957 const _sanitizeElements = function _sanitizeElements(currentNode) {
958 let content = null;
959
960 /* Execute a hook if present */
961 _executeHook('beforeSanitizeElements', currentNode, null);
962
963 /* Check if element is clobbered or can clobber */
964 if (_isClobbered(currentNode)) {
965 _forceRemove(currentNode);
966 return true;
967 }
968
969 /* Now let's check the element's type and name */
970 const tagName = transformCaseFunc(currentNode.nodeName);
971
972 /* Execute a hook if present */
973 _executeHook('uponSanitizeElement', currentNode, {
974 tagName,
975 allowedTags: ALLOWED_TAGS
976 });
977
978 /* Detect mXSS attempts abusing namespace confusion */
979 if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) {
980 _forceRemove(currentNode);
981 return true;
982 }
983
984 /* Remove element if anything forbids its presence */
985 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
986 /* Check if we have a custom element to handle */
987 if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
988 if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
989 return false;
990 }
991 if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
992 return false;
993 }
994 }
995
996 /* Keep content except for bad-listed elements */
997 if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
998 const parentNode = getParentNode(currentNode) || currentNode.parentNode;
999 const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
1000 if (childNodes && parentNode) {
1001 const childCount = childNodes.length;
1002 for (let i = childCount - 1; i >= 0; --i) {
1003 parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode));
1004 }
1005 }
1006 }
1007 _forceRemove(currentNode);
1008 return true;
1009 }
1010
1011 /* Check whether element has a valid namespace */
1012 if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
1013 _forceRemove(currentNode);
1014 return true;
1015 }
1016
1017 /* Make sure that older browsers don't get fallback-tag mXSS */
1018 if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
1019 _forceRemove(currentNode);
1020 return true;
1021 }
1022
1023 /* Sanitize element content to be template-safe */
1024 if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {
1025 /* Get the element's text content */
1026 content = currentNode.textContent;
1027 arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1028 content = stringReplace(content, expr, ' ');
1029 });
1030 if (currentNode.textContent !== content) {
1031 arrayPush(DOMPurify.removed, {
1032 element: currentNode.cloneNode()
1033 });
1034 currentNode.textContent = content;
1035 }
1036 }
1037
1038 /* Execute a hook if present */
1039 _executeHook('afterSanitizeElements', currentNode, null);
1040 return false;
1041 };
1042
1043 /**
1044 * _isValidAttribute
1045 *
1046 * @param {string} lcTag Lowercase tag name of containing element.
1047 * @param {string} lcName Lowercase attribute name.
1048 * @param {string} value Attribute value.
1049 * @return {Boolean} Returns true if `value` is valid, otherwise false.
1050 */
1051 // eslint-disable-next-line complexity
1052 const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
1053 /* Make sure attribute cannot clobber */
1054 if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
1055 return false;
1056 }
1057
1058 /* Allow valid data-* attributes: At least one character after "-"
1059 (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1060 XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1061 We don't need to check the value; it's always URI safe. */
1062 if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
1063 if (
1064 // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1065 // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
1066 // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck
1067 _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) ||
1068 // Alternative, second condition checks if it's an `is`-attribute, AND
1069 // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
1070 lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {
1071 return false;
1072 }
1073 /* Check value is safe. First, is attr inert? If so, is safe */
1074 } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {
1075 return false;
1076 } else ;
1077 return true;
1078 };
1079
1080 /**
1081 * _isBasicCustomElement
1082 * checks if at least one dash is included in tagName, and it's not the first char
1083 * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name
1084 *
1085 * @param {string} tagName name of the tag of the node to sanitize
1086 * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
1087 */
1088 const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1089 return tagName.indexOf('-') > 0;
1090 };
1091
1092 /**
1093 * _sanitizeAttributes
1094 *
1095 * @protect attributes
1096 * @protect nodeName
1097 * @protect removeAttribute
1098 * @protect setAttribute
1099 *
1100 * @param {Node} currentNode to sanitize
1101 */
1102 const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
1103 /* Execute a hook if present */
1104 _executeHook('beforeSanitizeAttributes', currentNode, null);
1105 const {
1106 attributes
1107 } = currentNode;
1108
1109 /* Check if we have attributes; if not we might have a text node */
1110 if (!attributes) {
1111 return;
1112 }
1113 const hookEvent = {
1114 attrName: '',
1115 attrValue: '',
1116 keepAttr: true,
1117 allowedAttributes: ALLOWED_ATTR
1118 };
1119 let l = attributes.length;
1120
1121 /* Go backwards over all attributes; safely remove bad ones */
1122 while (l--) {
1123 const attr = attributes[l];
1124 const {
1125 name,
1126 namespaceURI,
1127 value: attrValue
1128 } = attr;
1129 const lcName = transformCaseFunc(name);
1130 let value = name === 'value' ? attrValue : stringTrim(attrValue);
1131
1132 /* Execute a hook if present */
1133 hookEvent.attrName = lcName;
1134 hookEvent.attrValue = value;
1135 hookEvent.keepAttr = true;
1136 hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set
1137 _executeHook('uponSanitizeAttribute', currentNode, hookEvent);
1138 value = hookEvent.attrValue;
1139 /* Did the hooks approve of the attribute? */
1140 if (hookEvent.forceKeepAttr) {
1141 continue;
1142 }
1143
1144 /* Remove attribute */
1145 _removeAttribute(name, currentNode);
1146
1147 /* Did the hooks approve of the attribute? */
1148 if (!hookEvent.keepAttr) {
1149 continue;
1150 }
1151
1152 /* Work around a security issue in jQuery 3.0 */
1153 if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
1154 _removeAttribute(name, currentNode);
1155 continue;
1156 }
1157
1158 /* Sanitize attribute content to be template-safe */
1159 if (SAFE_FOR_TEMPLATES) {
1160 arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1161 value = stringReplace(value, expr, ' ');
1162 });
1163 }
1164
1165 /* Is `value` valid for this attribute? */
1166 const lcTag = transformCaseFunc(currentNode.nodeName);
1167 if (!_isValidAttribute(lcTag, lcName, value)) {
1168 continue;
1169 }
1170
1171 /* Full DOM Clobbering protection via namespace isolation,
1172 * Prefix id and name attributes with `user-content-`
1173 */
1174 if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
1175 // Remove the attribute with this value
1176 _removeAttribute(name, currentNode);
1177
1178 // Prefix the value and later re-create the attribute with the sanitized value
1179 value = SANITIZE_NAMED_PROPS_PREFIX + value;
1180 }
1181
1182 /* Handle attributes that require Trusted Types */
1183 if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
1184 if (namespaceURI) ; else {
1185 switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1186 case 'TrustedHTML':
1187 {
1188 value = trustedTypesPolicy.createHTML(value);
1189 break;
1190 }
1191 case 'TrustedScriptURL':
1192 {
1193 value = trustedTypesPolicy.createScriptURL(value);
1194 break;
1195 }
1196 }
1197 }
1198 }
1199
1200 /* Handle invalid data-* attribute set by try-catching it */
1201 try {
1202 if (namespaceURI) {
1203 currentNode.setAttributeNS(namespaceURI, name, value);
1204 } else {
1205 /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
1206 currentNode.setAttribute(name, value);
1207 }
1208 arrayPop(DOMPurify.removed);
1209 } catch (_) {}
1210 }
1211
1212 /* Execute a hook if present */
1213 _executeHook('afterSanitizeAttributes', currentNode, null);
1214 };
1215
1216 /**
1217 * _sanitizeShadowDOM
1218 *
1219 * @param {DocumentFragment} fragment to iterate over recursively
1220 */
1221 const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {
1222 let shadowNode = null;
1223 const shadowIterator = _createNodeIterator(fragment);
1224
1225 /* Execute a hook if present */
1226 _executeHook('beforeSanitizeShadowDOM', fragment, null);
1227 while (shadowNode = shadowIterator.nextNode()) {
1228 /* Execute a hook if present */
1229 _executeHook('uponSanitizeShadowNode', shadowNode, null);
1230
1231 /* Sanitize tags and elements */
1232 if (_sanitizeElements(shadowNode)) {
1233 continue;
1234 }
1235
1236 /* Deep shadow DOM detected */
1237 if (shadowNode.content instanceof DocumentFragment) {
1238 _sanitizeShadowDOM(shadowNode.content);
1239 }
1240
1241 /* Check attributes, sanitize if necessary */
1242 _sanitizeAttributes(shadowNode);
1243 }
1244
1245 /* Execute a hook if present */
1246 _executeHook('afterSanitizeShadowDOM', fragment, null);
1247 };
1248
1249 /**
1250 * Sanitize
1251 * Public method providing core sanitation functionality
1252 *
1253 * @param {String|Node} dirty string or DOM node
1254 * @param {Object} cfg object
1255 */
1256 // eslint-disable-next-line complexity
1257 DOMPurify.sanitize = function (dirty) {
1258 let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1259 let body = null;
1260 let importedNode = null;
1261 let currentNode = null;
1262 let returnNode = null;
1263 /* Make sure we have a string to sanitize.
1264 DO NOT return early, as this will return the wrong type if
1265 the user has requested a DOM object rather than a string */
1266 IS_EMPTY_INPUT = !dirty;
1267 if (IS_EMPTY_INPUT) {
1268 dirty = '<!-->';
1269 }
1270
1271 /* Stringify, in case dirty is an object */
1272 if (typeof dirty !== 'string' && !_isNode(dirty)) {
1273 if (typeof dirty.toString === 'function') {
1274 dirty = dirty.toString();
1275 if (typeof dirty !== 'string') {
1276 throw typeErrorCreate('dirty is not a string, aborting');
1277 }
1278 } else {
1279 throw typeErrorCreate('toString is not a function');
1280 }
1281 }
1282
1283 /* Return dirty HTML if DOMPurify cannot run */
1284 if (!DOMPurify.isSupported) {
1285 return dirty;
1286 }
1287
1288 /* Assign config vars */
1289 if (!SET_CONFIG) {
1290 _parseConfig(cfg);
1291 }
1292
1293 /* Clean up removed elements */
1294 DOMPurify.removed = [];
1295
1296 /* Check if dirty is correctly typed for IN_PLACE */
1297 if (typeof dirty === 'string') {
1298 IN_PLACE = false;
1299 }
1300 if (IN_PLACE) {
1301 /* Do some early pre-sanitization to avoid unsafe root nodes */
1302 if (dirty.nodeName) {
1303 const tagName = transformCaseFunc(dirty.nodeName);
1304 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
1305 throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
1306 }
1307 }
1308 } else if (dirty instanceof Node) {
1309 /* If dirty is a DOM element, append to an empty document to avoid
1310 elements being stripped by the parser */
1311 body = _initDocument('<!---->');
1312 importedNode = body.ownerDocument.importNode(dirty, true);
1313 if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') {
1314 /* Node is already a body, use as is */
1315 body = importedNode;
1316 } else if (importedNode.nodeName === 'HTML') {
1317 body = importedNode;
1318 } else {
1319 // eslint-disable-next-line unicorn/prefer-dom-node-append
1320 body.appendChild(importedNode);
1321 }
1322 } else {
1323 /* Exit directly if we have nothing to do */
1324 if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
1325 // eslint-disable-next-line unicorn/prefer-includes
1326 dirty.indexOf('<') === -1) {
1327 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
1328 }
1329
1330 /* Initialize the document to work on */
1331 body = _initDocument(dirty);
1332
1333 /* Check we have a DOM node from the data */
1334 if (!body) {
1335 return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';
1336 }
1337 }
1338
1339 /* Remove first element node (ours) if FORCE_BODY is set */
1340 if (body && FORCE_BODY) {
1341 _forceRemove(body.firstChild);
1342 }
1343
1344 /* Get node iterator */
1345 const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
1346
1347 /* Now start iterating over the created document */
1348 while (currentNode = nodeIterator.nextNode()) {
1349 /* Sanitize tags and elements */
1350 if (_sanitizeElements(currentNode)) {
1351 continue;
1352 }
1353
1354 /* Shadow DOM detected, sanitize it */
1355 if (currentNode.content instanceof DocumentFragment) {
1356 _sanitizeShadowDOM(currentNode.content);
1357 }
1358
1359 /* Check attributes, sanitize if necessary */
1360 _sanitizeAttributes(currentNode);
1361 }
1362
1363 /* If we sanitized `dirty` in-place, return it. */
1364 if (IN_PLACE) {
1365 return dirty;
1366 }
1367
1368 /* Return sanitized string or DOM */
1369 if (RETURN_DOM) {
1370 if (RETURN_DOM_FRAGMENT) {
1371 returnNode = createDocumentFragment.call(body.ownerDocument);
1372 while (body.firstChild) {
1373 // eslint-disable-next-line unicorn/prefer-dom-node-append
1374 returnNode.appendChild(body.firstChild);
1375 }
1376 } else {
1377 returnNode = body;
1378 }
1379 if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {
1380 /*
1381 AdoptNode() is not used because internal state is not reset
1382 (e.g. the past names map of a HTMLFormElement), this is safe
1383 in theory but we would rather not risk another attack vector.
1384 The state that is cloned by importNode() is explicitly defined
1385 by the specs.
1386 */
1387 returnNode = importNode.call(originalDocument, returnNode, true);
1388 }
1389 return returnNode;
1390 }
1391 let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
1392
1393 /* Serialize doctype if allowed */
1394 if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {
1395 serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\n' + serializedHTML;
1396 }
1397
1398 /* Sanitize final string template-safe */
1399 if (SAFE_FOR_TEMPLATES) {
1400 arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1401 serializedHTML = stringReplace(serializedHTML, expr, ' ');
1402 });
1403 }
1404 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
1405 };
1406
1407 /**
1408 * Public method to set the configuration once
1409 * setConfig
1410 *
1411 * @param {Object} cfg configuration object
1412 */
1413 DOMPurify.setConfig = function () {
1414 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1415 _parseConfig(cfg);
1416 SET_CONFIG = true;
1417 };
1418
1419 /**
1420 * Public method to remove the configuration
1421 * clearConfig
1422 *
1423 */
1424 DOMPurify.clearConfig = function () {
1425 CONFIG = null;
1426 SET_CONFIG = false;
1427 };
1428
1429 /**
1430 * Public method to check if an attribute value is valid.
1431 * Uses last set config, if any. Otherwise, uses config defaults.
1432 * isValidAttribute
1433 *
1434 * @param {String} tag Tag name of containing element.
1435 * @param {String} attr Attribute name.
1436 * @param {String} value Attribute value.
1437 * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.
1438 */
1439 DOMPurify.isValidAttribute = function (tag, attr, value) {
1440 /* Initialize shared config vars if necessary. */
1441 if (!CONFIG) {
1442 _parseConfig({});
1443 }
1444 const lcTag = transformCaseFunc(tag);
1445 const lcName = transformCaseFunc(attr);
1446 return _isValidAttribute(lcTag, lcName, value);
1447 };
1448
1449 /**
1450 * AddHook
1451 * Public method to add DOMPurify hooks
1452 *
1453 * @param {String} entryPoint entry point for the hook to add
1454 * @param {Function} hookFunction function to execute
1455 */
1456 DOMPurify.addHook = function (entryPoint, hookFunction) {
1457 if (typeof hookFunction !== 'function') {
1458 return;
1459 }
1460 hooks[entryPoint] = hooks[entryPoint] || [];
1461 arrayPush(hooks[entryPoint], hookFunction);
1462 };
1463
1464 /**
1465 * RemoveHook
1466 * Public method to remove a DOMPurify hook at a given entryPoint
1467 * (pops it from the stack of hooks if more are present)
1468 *
1469 * @param {String} entryPoint entry point for the hook to remove
1470 * @return {Function} removed(popped) hook
1471 */
1472 DOMPurify.removeHook = function (entryPoint) {
1473 if (hooks[entryPoint]) {
1474 return arrayPop(hooks[entryPoint]);
1475 }
1476 };
1477
1478 /**
1479 * RemoveHooks
1480 * Public method to remove all DOMPurify hooks at a given entryPoint
1481 *
1482 * @param {String} entryPoint entry point for the hooks to remove
1483 */
1484 DOMPurify.removeHooks = function (entryPoint) {
1485 if (hooks[entryPoint]) {
1486 hooks[entryPoint] = [];
1487 }
1488 };
1489
1490 /**
1491 * RemoveAllHooks
1492 * Public method to remove all DOMPurify hooks
1493 */
1494 DOMPurify.removeAllHooks = function () {
1495 hooks = {};
1496 };
1497 return DOMPurify;
1498}
1499var purify = createDOMPurify();
1500
1501export { purify as default };
1502//# sourceMappingURL=purify.es.mjs.map
Note: See TracBrowser for help on using the repository browser.