source: node_modules/dompurify/dist/purify.cjs.js

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