dompurify 3.4.9 → 3.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/purify.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
2
 
3
- import type {
4
- TrustedHTML,
5
- TrustedTypesWindow,
6
- } from 'trusted-types/lib/index.js';
7
3
  import type { Config, UseProfilesConfig } from './config';
4
+ import type { DOMPurify, HooksMap, HookFunction, WindowLike } from './types';
8
5
  import * as TAGS from './tags.js';
9
6
  import * as ATTRS from './attrs.js';
10
7
  import * as EXPRESSIONS from './regexp.js';
@@ -13,6 +10,7 @@ import {
13
10
  clone,
14
11
  entries,
15
12
  freeze,
13
+ seal,
16
14
  arrayForEach,
17
15
  arrayIsArray,
18
16
  arrayLastIndexOf,
@@ -36,6 +34,21 @@ import {
36
34
 
37
35
  export type { Config } from './config';
38
36
 
37
+ export type {
38
+ DOMPurify,
39
+ RemovedElement,
40
+ RemovedAttribute,
41
+ HookName,
42
+ NodeHook,
43
+ ElementHook,
44
+ DocumentFragmentHook,
45
+ UponSanitizeElementHook,
46
+ UponSanitizeAttributeHook,
47
+ UponSanitizeElementHookEvent,
48
+ UponSanitizeAttributeHookEvent,
49
+ WindowLike,
50
+ } from './types';
51
+
39
52
  declare const VERSION: string;
40
53
 
41
54
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
@@ -46,7 +59,7 @@ const NODE_TYPE = {
46
59
  cdataSection: 4,
47
60
  entityReference: 5, // Deprecated
48
61
  entityNode: 6, // Deprecated
49
- progressingInstruction: 7,
62
+ processingInstruction: 7,
50
63
  comment: 8,
51
64
  document: 9,
52
65
  documentType: 10,
@@ -122,6 +135,36 @@ const _createHooksMap = function (): HooksMap {
122
135
  };
123
136
  };
124
137
 
138
+ /**
139
+ * Resolve a set-valued configuration option: a fresh set built from
140
+ * cfg[key] when it is an own array property (seeded with a clone of
141
+ * options.base when given, case-normalized via options.transform),
142
+ * the fallback set otherwise.
143
+ *
144
+ * @param cfg the cloned, prototype-free configuration object
145
+ * @param key the configuration property to read
146
+ * @param fallback the set to use when the option is absent or not an array
147
+ * @param options transform and optional base set to merge into
148
+ * @returns the resolved set
149
+ */
150
+ const _resolveSetOption = function (
151
+ cfg: Config,
152
+ key: keyof Config,
153
+ fallback: Record<string, boolean>,
154
+ options: {
155
+ transform: Parameters<typeof addToSet>[2];
156
+ base?: Record<string, boolean>;
157
+ }
158
+ ): Record<string, boolean> {
159
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key])
160
+ ? addToSet(
161
+ options.base ? clone(options.base) : {},
162
+ cfg[key] as readonly unknown[],
163
+ options.transform
164
+ )
165
+ : fallback;
166
+ };
167
+
125
168
  function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
126
169
  const DOMPurify: DOMPurify = (root: WindowLike) => createDOMPurify(root);
127
170
 
@@ -526,15 +569,20 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
526
569
  stringToString
527
570
  );
528
571
 
529
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, [
572
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze([
530
573
  'mi',
531
574
  'mo',
532
575
  'mn',
533
576
  'ms',
534
577
  'mtext',
535
578
  ]);
579
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet(
580
+ {},
581
+ DEFAULT_MATHML_TEXT_INTEGRATION_POINTS
582
+ );
536
583
 
537
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
584
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
585
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
538
586
 
539
587
  // Certain elements are allowed in both SVG and HTML
540
588
  // namespace. We need to specify them explicitly
@@ -600,52 +648,48 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
600
648
  : stringToLowerCase;
601
649
 
602
650
  /* Set configuration parameters */
603
- ALLOWED_TAGS =
604
- objectHasOwnProperty(cfg, 'ALLOWED_TAGS') &&
605
- arrayIsArray(cfg.ALLOWED_TAGS)
606
- ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc)
607
- : DEFAULT_ALLOWED_TAGS;
608
- ALLOWED_ATTR =
609
- objectHasOwnProperty(cfg, 'ALLOWED_ATTR') &&
610
- arrayIsArray(cfg.ALLOWED_ATTR)
611
- ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc)
612
- : DEFAULT_ALLOWED_ATTR;
613
- ALLOWED_NAMESPACES =
614
- objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') &&
615
- arrayIsArray(cfg.ALLOWED_NAMESPACES)
616
- ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString)
617
- : DEFAULT_ALLOWED_NAMESPACES;
618
- URI_SAFE_ATTRIBUTES =
619
- objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') &&
620
- arrayIsArray(cfg.ADD_URI_SAFE_ATTR)
621
- ? addToSet(
622
- clone(DEFAULT_URI_SAFE_ATTRIBUTES),
623
- cfg.ADD_URI_SAFE_ATTR,
624
- transformCaseFunc
625
- )
626
- : DEFAULT_URI_SAFE_ATTRIBUTES;
627
- DATA_URI_TAGS =
628
- objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') &&
629
- arrayIsArray(cfg.ADD_DATA_URI_TAGS)
630
- ? addToSet(
631
- clone(DEFAULT_DATA_URI_TAGS),
632
- cfg.ADD_DATA_URI_TAGS,
633
- transformCaseFunc
634
- )
635
- : DEFAULT_DATA_URI_TAGS;
636
- FORBID_CONTENTS =
637
- objectHasOwnProperty(cfg, 'FORBID_CONTENTS') &&
638
- arrayIsArray(cfg.FORBID_CONTENTS)
639
- ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc)
640
- : DEFAULT_FORBID_CONTENTS;
641
- FORBID_TAGS =
642
- objectHasOwnProperty(cfg, 'FORBID_TAGS') && arrayIsArray(cfg.FORBID_TAGS)
643
- ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc)
644
- : clone({});
645
- FORBID_ATTR =
646
- objectHasOwnProperty(cfg, 'FORBID_ATTR') && arrayIsArray(cfg.FORBID_ATTR)
647
- ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc)
648
- : clone({});
651
+ ALLOWED_TAGS = _resolveSetOption(
652
+ cfg,
653
+ 'ALLOWED_TAGS',
654
+ DEFAULT_ALLOWED_TAGS,
655
+ { transform: transformCaseFunc }
656
+ );
657
+ ALLOWED_ATTR = _resolveSetOption(
658
+ cfg,
659
+ 'ALLOWED_ATTR',
660
+ DEFAULT_ALLOWED_ATTR,
661
+ { transform: transformCaseFunc }
662
+ );
663
+ ALLOWED_NAMESPACES = _resolveSetOption(
664
+ cfg,
665
+ 'ALLOWED_NAMESPACES',
666
+ DEFAULT_ALLOWED_NAMESPACES,
667
+ { transform: stringToString }
668
+ );
669
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(
670
+ cfg,
671
+ 'ADD_URI_SAFE_ATTR',
672
+ DEFAULT_URI_SAFE_ATTRIBUTES,
673
+ { transform: transformCaseFunc, base: DEFAULT_URI_SAFE_ATTRIBUTES }
674
+ );
675
+ DATA_URI_TAGS = _resolveSetOption(
676
+ cfg,
677
+ 'ADD_DATA_URI_TAGS',
678
+ DEFAULT_DATA_URI_TAGS,
679
+ { transform: transformCaseFunc, base: DEFAULT_DATA_URI_TAGS }
680
+ );
681
+ FORBID_CONTENTS = _resolveSetOption(
682
+ cfg,
683
+ 'FORBID_CONTENTS',
684
+ DEFAULT_FORBID_CONTENTS,
685
+ { transform: transformCaseFunc }
686
+ );
687
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
688
+ transform: transformCaseFunc,
689
+ });
690
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
691
+ transform: transformCaseFunc,
692
+ });
649
693
  USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES')
650
694
  ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object'
651
695
  ? clone(cfg.USE_PROFILES)
@@ -679,14 +723,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
679
723
  cfg.MATHML_TEXT_INTEGRATION_POINTS &&
680
724
  typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object'
681
725
  ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS)
682
- : addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); // Default built-in map
726
+ : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
683
727
 
684
728
  HTML_INTEGRATION_POINTS =
685
729
  objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') &&
686
730
  cfg.HTML_INTEGRATION_POINTS &&
687
731
  typeof cfg.HTML_INTEGRATION_POINTS === 'object'
688
732
  ? clone(cfg.HTML_INTEGRATION_POINTS)
689
- : addToSet({}, ['annotation-xml']); // Default built-in map
733
+ : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
690
734
 
691
735
  const customElementHandling =
692
736
  objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') &&
@@ -723,6 +767,8 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
723
767
  customElementHandling.allowCustomizedBuiltInElements; // Default undefined
724
768
  }
725
769
 
770
+ seal(CUSTOM_ELEMENT_HANDLING);
771
+
726
772
  if (SAFE_FOR_TEMPLATES) {
727
773
  ALLOW_DATA_ATTR = false;
728
774
  }
@@ -944,6 +990,111 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
944
990
  ...TAGS.mathMlDisallowed,
945
991
  ]);
946
992
 
993
+ /**
994
+ * Namespace rules for an element in the SVG namespace.
995
+ *
996
+ * @param tagName the element's lowercase tag name
997
+ * @param parent the (possibly simulated) parent node
998
+ * @param parentTagName the parent's lowercase tag name
999
+ * @returns true if a spec-compliant parser could produce this element
1000
+ */
1001
+ const _checkSvgNamespace = function (
1002
+ tagName: string,
1003
+ parent: { namespaceURI?: string },
1004
+ parentTagName: string
1005
+ ): boolean {
1006
+ // The only way to switch from HTML namespace to SVG
1007
+ // is via <svg>. If it happens via any other tag, then
1008
+ // it should be killed.
1009
+ if (parent.namespaceURI === HTML_NAMESPACE) {
1010
+ return tagName === 'svg';
1011
+ }
1012
+
1013
+ // The only way to switch from MathML to SVG is via <svg>
1014
+ // if the parent is either <annotation-xml> or a MathML
1015
+ // text integration point.
1016
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
1017
+ return (
1018
+ tagName === 'svg' &&
1019
+ (parentTagName === 'annotation-xml' ||
1020
+ MATHML_TEXT_INTEGRATION_POINTS[parentTagName])
1021
+ );
1022
+ }
1023
+
1024
+ // We only allow elements that are defined in SVG
1025
+ // spec. All others are disallowed in SVG namespace.
1026
+ return Boolean(ALL_SVG_TAGS[tagName]);
1027
+ };
1028
+
1029
+ /**
1030
+ * Namespace rules for an element in the MathML namespace.
1031
+ *
1032
+ * @param tagName the element's lowercase tag name
1033
+ * @param parent the (possibly simulated) parent node
1034
+ * @param parentTagName the parent's lowercase tag name
1035
+ * @returns true if a spec-compliant parser could produce this element
1036
+ */
1037
+ const _checkMathMlNamespace = function (
1038
+ tagName: string,
1039
+ parent: { namespaceURI?: string },
1040
+ parentTagName: string
1041
+ ): boolean {
1042
+ // The only way to switch from HTML namespace to MathML
1043
+ // is via <math>. If it happens via any other tag, then
1044
+ // it should be killed.
1045
+ if (parent.namespaceURI === HTML_NAMESPACE) {
1046
+ return tagName === 'math';
1047
+ }
1048
+
1049
+ // The only way to switch from SVG to MathML is via
1050
+ // <math> and HTML integration points
1051
+ if (parent.namespaceURI === SVG_NAMESPACE) {
1052
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
1053
+ }
1054
+
1055
+ // We only allow elements that are defined in MathML
1056
+ // spec. All others are disallowed in MathML namespace.
1057
+ return Boolean(ALL_MATHML_TAGS[tagName]);
1058
+ };
1059
+
1060
+ /**
1061
+ * Namespace rules for an element in the HTML namespace.
1062
+ *
1063
+ * @param tagName the element's lowercase tag name
1064
+ * @param parent the (possibly simulated) parent node
1065
+ * @param parentTagName the parent's lowercase tag name
1066
+ * @returns true if a spec-compliant parser could produce this element
1067
+ */
1068
+ const _checkHtmlNamespace = function (
1069
+ tagName: string,
1070
+ parent: { namespaceURI?: string },
1071
+ parentTagName: string
1072
+ ): boolean {
1073
+ // The only way to switch from SVG to HTML is via
1074
+ // HTML integration points, and from MathML to HTML
1075
+ // is via MathML text integration points
1076
+ if (
1077
+ parent.namespaceURI === SVG_NAMESPACE &&
1078
+ !HTML_INTEGRATION_POINTS[parentTagName]
1079
+ ) {
1080
+ return false;
1081
+ }
1082
+
1083
+ if (
1084
+ parent.namespaceURI === MATHML_NAMESPACE &&
1085
+ !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]
1086
+ ) {
1087
+ return false;
1088
+ }
1089
+
1090
+ // We disallow tags that are specific for MathML
1091
+ // or SVG and should never appear in HTML namespace
1092
+ return (
1093
+ !ALL_MATHML_TAGS[tagName] &&
1094
+ (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])
1095
+ );
1096
+ };
1097
+
947
1098
  /**
948
1099
  * @param element a DOM element whose namespace is being checked
949
1100
  * @returns Return false if the element has a
@@ -970,72 +1121,15 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
970
1121
  }
971
1122
 
972
1123
  if (element.namespaceURI === SVG_NAMESPACE) {
973
- // The only way to switch from HTML namespace to SVG
974
- // is via <svg>. If it happens via any other tag, then
975
- // it should be killed.
976
- if (parent.namespaceURI === HTML_NAMESPACE) {
977
- return tagName === 'svg';
978
- }
979
-
980
- // The only way to switch from MathML to SVG is via`
981
- // svg if parent is either <annotation-xml> or MathML
982
- // text integration points.
983
- if (parent.namespaceURI === MATHML_NAMESPACE) {
984
- return (
985
- tagName === 'svg' &&
986
- (parentTagName === 'annotation-xml' ||
987
- MATHML_TEXT_INTEGRATION_POINTS[parentTagName])
988
- );
989
- }
990
-
991
- // We only allow elements that are defined in SVG
992
- // spec. All others are disallowed in SVG namespace.
993
- return Boolean(ALL_SVG_TAGS[tagName]);
1124
+ return _checkSvgNamespace(tagName, parent, parentTagName);
994
1125
  }
995
1126
 
996
1127
  if (element.namespaceURI === MATHML_NAMESPACE) {
997
- // The only way to switch from HTML namespace to MathML
998
- // is via <math>. If it happens via any other tag, then
999
- // it should be killed.
1000
- if (parent.namespaceURI === HTML_NAMESPACE) {
1001
- return tagName === 'math';
1002
- }
1003
-
1004
- // The only way to switch from SVG to MathML is via
1005
- // <math> and HTML integration points
1006
- if (parent.namespaceURI === SVG_NAMESPACE) {
1007
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
1008
- }
1009
-
1010
- // We only allow elements that are defined in MathML
1011
- // spec. All others are disallowed in MathML namespace.
1012
- return Boolean(ALL_MATHML_TAGS[tagName]);
1128
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
1013
1129
  }
1014
1130
 
1015
1131
  if (element.namespaceURI === HTML_NAMESPACE) {
1016
- // The only way to switch from SVG to HTML is via
1017
- // HTML integration points, and from MathML to HTML
1018
- // is via MathML text integration points
1019
- if (
1020
- parent.namespaceURI === SVG_NAMESPACE &&
1021
- !HTML_INTEGRATION_POINTS[parentTagName]
1022
- ) {
1023
- return false;
1024
- }
1025
-
1026
- if (
1027
- parent.namespaceURI === MATHML_NAMESPACE &&
1028
- !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]
1029
- ) {
1030
- return false;
1031
- }
1032
-
1033
- // We disallow tags that are specific for MathML
1034
- // or SVG and should never appear in HTML namespace
1035
- return (
1036
- !ALL_MATHML_TAGS[tagName] &&
1037
- (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])
1038
- );
1132
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
1039
1133
  }
1040
1134
 
1041
1135
  // For XHTML and XML documents that support custom namespaces
@@ -1112,9 +1206,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1112
1206
  * @param root the in-place root to empty
1113
1207
  */
1114
1208
  const _neutralizeRoot = function (root: Node): void {
1115
- const childNodes = getChildNodes
1116
- ? getChildNodes(root)
1117
- : (root as Element).childNodes;
1209
+ const childNodes = getChildNodes(root);
1118
1210
  if (childNodes) {
1119
1211
  const snapshot: Node[] = [];
1120
1212
  arrayForEach(childNodes, (child) => {
@@ -1129,7 +1221,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1129
1221
  });
1130
1222
  }
1131
1223
 
1132
- const attributes = getAttributes ? getAttributes(root) : null;
1224
+ const attributes = getAttributes(root);
1133
1225
  if (attributes) {
1134
1226
  for (let i = attributes.length - 1; i >= 0; --i) {
1135
1227
  const attribute = attributes[i];
@@ -1191,9 +1283,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1191
1283
  * @param element the element to strip
1192
1284
  */
1193
1285
  const _stripDisallowedAttributes = function (element: Element): void {
1194
- const attributes = getAttributes
1195
- ? getAttributes(element)
1196
- : element.attributes;
1286
+ const attributes = getAttributes(element);
1197
1287
  if (!attributes) {
1198
1288
  return;
1199
1289
  }
@@ -1246,9 +1336,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1246
1336
  _stripDisallowedAttributes(node as Element);
1247
1337
  }
1248
1338
 
1249
- const childNodes = getChildNodes
1250
- ? getChildNodes(node)
1251
- : (node as Element).childNodes;
1339
+ const childNodes = getChildNodes(node);
1252
1340
  if (childNodes) {
1253
1341
  for (let i = childNodes.length - 1; i >= 0; --i) {
1254
1342
  stack.push(childNodes[i]);
@@ -1350,6 +1438,21 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1350
1438
  );
1351
1439
  };
1352
1440
 
1441
+ /**
1442
+ * Replace template expression syntax (mustache, ERB, template
1443
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
1444
+ * sites. Order matters: mustache, then ERB, then template literal.
1445
+ *
1446
+ * @param value the string to scrub
1447
+ * @returns the scrubbed string
1448
+ */
1449
+ const _stripTemplateExpressions = function (value: string): string {
1450
+ value = stringReplace(value, MUSTACHE_EXPR, ' ');
1451
+ value = stringReplace(value, ERB_EXPR, ' ');
1452
+ value = stringReplace(value, TMPLIT_EXPR, ' ');
1453
+ return value;
1454
+ };
1455
+
1353
1456
  /**
1354
1457
  * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
1355
1458
  * character data of an element subtree. Used as the final safety net for
@@ -1384,23 +1487,21 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1384
1487
 
1385
1488
  let currentNode = walker.nextNode() as CharacterData | null;
1386
1489
  while (currentNode) {
1387
- let data = currentNode.data;
1388
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
1389
- data = stringReplace(data, expr, ' ');
1390
- });
1391
- currentNode.data = data;
1490
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
1392
1491
  currentNode = walker.nextNode() as CharacterData | null;
1393
1492
  }
1394
1493
 
1395
1494
  // NodeIterator does not descend into <template>.content per the DOM spec,
1396
1495
  // so we must explicitly recurse into each template's content fragment,
1397
1496
  // mirroring the approach used by _sanitizeShadowDOM.
1398
- const templates = node.querySelectorAll?.('template') ?? [];
1399
- arrayForEach(Array.from(templates), (tmpl: HTMLTemplateElement) => {
1400
- if (_isDocumentFragment(tmpl.content)) {
1401
- _scrubTemplateExpressions(tmpl.content as unknown as Element);
1402
- }
1403
- });
1497
+ const templates = node.querySelectorAll?.('template');
1498
+ if (templates) {
1499
+ arrayForEach(templates, (tmpl: HTMLTemplateElement) => {
1500
+ if (_isDocumentFragment(tmpl.content)) {
1501
+ _scrubTemplateExpressions(tmpl.content as unknown as Element);
1502
+ }
1503
+ });
1504
+ }
1404
1505
  };
1405
1506
 
1406
1507
  /**
@@ -1517,52 +1618,34 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1517
1618
  currentNode: Parameters<T>[0],
1518
1619
  data: Parameters<T>[1]
1519
1620
  ): void {
1621
+ if (hooks.length === 0) {
1622
+ return;
1623
+ }
1624
+
1520
1625
  arrayForEach(hooks, (hook: T) => {
1521
1626
  hook.call(DOMPurify, currentNode, data, CONFIG);
1522
1627
  });
1523
1628
  }
1524
1629
 
1525
1630
  /**
1526
- * _sanitizeElements
1631
+ * Structural-threat checks that condemn a node regardless of the
1632
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
1633
+ * processing instructions, markup-bearing comments. Pure predicate;
1634
+ * the caller removes. Check order is load-bearing.
1527
1635
  *
1528
- * @protect nodeName
1529
- * @protect textContent
1530
- * @protect removeChild
1531
- * @param currentNode to check for permission to exist
1532
- * @return true if node was killed, false if left alive
1636
+ * @param currentNode the node to inspect
1637
+ * @param tagName the node's transformCaseFunc'd tag name
1638
+ * @return true if the node must be removed
1533
1639
  */
1534
- const _sanitizeElements = function (currentNode: any): boolean {
1535
- let content = null;
1536
-
1537
- /* Execute a hook if present */
1538
- _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1539
-
1540
- /* Check if element is clobbered or can clobber */
1541
- if (_isClobbered(currentNode)) {
1542
- _forceRemove(currentNode);
1543
- return true;
1544
- }
1545
-
1546
- /* Now let's check the element's type and name */
1547
- const tagName = transformCaseFunc(
1548
- getNodeName ? getNodeName(currentNode) : currentNode.nodeName
1549
- );
1550
-
1551
- /* Execute a hook if present */
1552
- _executeHooks(hooks.uponSanitizeElement, currentNode, {
1553
- tagName,
1554
- allowedTags: ALLOWED_TAGS,
1555
- });
1556
-
1640
+ const _isUnsafeNode = function (currentNode: any, tagName: string): boolean {
1557
1641
  /* Detect mXSS attempts abusing namespace confusion */
1558
1642
  if (
1559
1643
  SAFE_FOR_XML &&
1560
1644
  currentNode.hasChildNodes() &&
1561
1645
  !_isNode(currentNode.firstElementChild) &&
1562
- regExpTest(/<[/\w!]/g, currentNode.innerHTML) &&
1563
- regExpTest(/<[/\w!]/g, currentNode.textContent)
1646
+ regExpTest(EXPRESSIONS.ELEMENT_MARKUP_PROBE, currentNode.textContent) &&
1647
+ regExpTest(EXPRESSIONS.ELEMENT_MARKUP_PROBE, currentNode.innerHTML)
1564
1648
  ) {
1565
- _forceRemove(currentNode);
1566
1649
  return true;
1567
1650
  }
1568
1651
 
@@ -1573,13 +1656,11 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1573
1656
  tagName === 'style' &&
1574
1657
  _isNode(currentNode.firstElementChild)
1575
1658
  ) {
1576
- _forceRemove(currentNode);
1577
1659
  return true;
1578
1660
  }
1579
1661
 
1580
1662
  /* Remove any occurrence of processing instructions */
1581
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
1582
- _forceRemove(currentNode);
1663
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
1583
1664
  return true;
1584
1665
  }
1585
1666
 
@@ -1587,39 +1668,47 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1587
1668
  if (
1588
1669
  SAFE_FOR_XML &&
1589
1670
  currentNode.nodeType === NODE_TYPE.comment &&
1590
- regExpTest(/<[/\w]/g, currentNode.data)
1671
+ regExpTest(EXPRESSIONS.COMMENT_MARKUP_PROBE, currentNode.data)
1591
1672
  ) {
1592
- _forceRemove(currentNode);
1593
1673
  return true;
1594
1674
  }
1595
1675
 
1596
- /* Remove element if anything forbids its presence */
1597
- if (
1598
- FORBID_TAGS[tagName] ||
1599
- (!(
1600
- EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function &&
1601
- EXTRA_ELEMENT_HANDLING.tagCheck(tagName)
1602
- ) &&
1603
- !ALLOWED_TAGS[tagName])
1604
- ) {
1605
- /* Check if we have a custom element to handle */
1606
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1607
- if (
1608
- CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
1609
- regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)
1610
- ) {
1611
- return false;
1612
- }
1676
+ return false;
1677
+ };
1613
1678
 
1614
- if (
1615
- CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
1616
- CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)
1617
- ) {
1618
- return false;
1619
- }
1679
+ /**
1680
+ * Handle a node whose tag is forbidden or not allowlisted: keep
1681
+ * allowed custom elements (false return exits _sanitizeElements
1682
+ * early - namespace/fallback checks and the afterSanitizeElements
1683
+ * hook are intentionally skipped for kept custom elements), else
1684
+ * hoist content per KEEP_CONTENT and remove.
1685
+ *
1686
+ * @param currentNode the disallowed node
1687
+ * @param tagName the node's transformCaseFunc'd tag name
1688
+ * @return true if the node was removed, false if kept
1689
+ */
1690
+ const _sanitizeDisallowedNode = function (
1691
+ currentNode: any,
1692
+ tagName: string
1693
+ ): boolean {
1694
+ /* Check if we have a custom element to handle */
1695
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1696
+ if (
1697
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
1698
+ regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)
1699
+ ) {
1700
+ return false;
1620
1701
  }
1621
1702
 
1622
- /* Keep content except for bad-listed elements.
1703
+ if (
1704
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
1705
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)
1706
+ ) {
1707
+ return false;
1708
+ }
1709
+ }
1710
+
1711
+ /* Keep content except for bad-listed elements.
1623
1712
  Use the cached prototype getters exclusively — the previous code
1624
1713
  had `|| currentNode.parentNode` / `|| currentNode.childNodes`
1625
1714
  fallbacks, but the cached getters always return the canonical
@@ -1627,14 +1716,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1627
1716
  path was dead in safe cases and a clobbering surface in unsafe
1628
1717
  ones. Falsy cached results stay falsy; the `if (childNodes &&
1629
1718
  parentNode)` check already gates correctly. */
1630
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1631
- const parentNode = getParentNode(currentNode);
1632
- const childNodes = getChildNodes(currentNode);
1719
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1720
+ const parentNode = getParentNode(currentNode);
1721
+ const childNodes = getChildNodes(currentNode);
1633
1722
 
1634
- if (childNodes && parentNode) {
1635
- const childCount = childNodes.length;
1723
+ if (childNodes && parentNode) {
1724
+ const childCount = childNodes.length;
1636
1725
 
1637
- /* In-place: hoist the *original* children so the iterator visits
1726
+ /* In-place: hoist the *original* children so the iterator visits
1638
1727
  and sanitises them through the same allowlist pass as every other
1639
1728
  node. The caller built the tree in the live document, so the
1640
1729
  originals carry already-queued resource events (`<img onerror>`,
@@ -1655,19 +1744,67 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1655
1744
  `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1656
1745
  valid whether we move (drops the trailing entry) or clone (leaves
1657
1746
  the list intact). */
1658
- for (let i = childCount - 1; i >= 0; --i) {
1659
- const hoisted = IN_PLACE
1660
- ? childNodes[i]
1661
- : cloneNode(childNodes[i], true);
1662
- parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1663
- }
1747
+ for (let i = childCount - 1; i >= 0; --i) {
1748
+ const hoisted = IN_PLACE
1749
+ ? childNodes[i]
1750
+ : cloneNode(childNodes[i], true);
1751
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1664
1752
  }
1665
1753
  }
1754
+ }
1755
+
1756
+ _forceRemove(currentNode);
1757
+ return true;
1758
+ };
1759
+
1760
+ /**
1761
+ * _sanitizeElements
1762
+ *
1763
+ * @protect nodeName
1764
+ * @protect textContent
1765
+ * @protect removeChild
1766
+ * @param currentNode to check for permission to exist
1767
+ * @return true if node was killed, false if left alive
1768
+ */
1769
+ const _sanitizeElements = function (currentNode: any): boolean {
1770
+ /* Execute a hook if present */
1771
+ _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1666
1772
 
1773
+ /* Check if element is clobbered or can clobber */
1774
+ if (_isClobbered(currentNode)) {
1667
1775
  _forceRemove(currentNode);
1668
1776
  return true;
1669
1777
  }
1670
1778
 
1779
+ /* Now let's check the element's type and name */
1780
+ const tagName = transformCaseFunc(
1781
+ getNodeName ? getNodeName(currentNode) : currentNode.nodeName
1782
+ );
1783
+
1784
+ /* Execute a hook if present */
1785
+ _executeHooks(hooks.uponSanitizeElement, currentNode, {
1786
+ tagName,
1787
+ allowedTags: ALLOWED_TAGS,
1788
+ });
1789
+
1790
+ /* Remove mXSS vectors, processing instructions and risky comments */
1791
+ if (_isUnsafeNode(currentNode, tagName)) {
1792
+ _forceRemove(currentNode);
1793
+ return true;
1794
+ }
1795
+
1796
+ /* Remove element if anything forbids its presence */
1797
+ if (
1798
+ FORBID_TAGS[tagName] ||
1799
+ (!(
1800
+ EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function &&
1801
+ EXTRA_ELEMENT_HANDLING.tagCheck(tagName)
1802
+ ) &&
1803
+ !ALLOWED_TAGS[tagName])
1804
+ ) {
1805
+ return _sanitizeDisallowedNode(currentNode, tagName);
1806
+ }
1807
+
1671
1808
  /* Check whether element has a valid namespace.
1672
1809
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
1673
1810
  nodeType getter rather than `instanceof Element`, which is realm-
@@ -1685,7 +1822,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1685
1822
  (tagName === 'noscript' ||
1686
1823
  tagName === 'noembed' ||
1687
1824
  tagName === 'noframes') &&
1688
- regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)
1825
+ regExpTest(EXPRESSIONS.FALLBACK_TAG_CLOSE, currentNode.innerHTML)
1689
1826
  ) {
1690
1827
  _forceRemove(currentNode);
1691
1828
  return true;
@@ -1694,11 +1831,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1694
1831
  /* Sanitize element content to be template-safe */
1695
1832
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1696
1833
  /* Get the element's text content */
1697
- content = currentNode.textContent;
1698
-
1699
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
1700
- content = stringReplace(content, expr, ' ');
1701
- });
1834
+ const content = _stripTemplateExpressions(currentNode.textContent);
1702
1835
 
1703
1836
  if (currentNode.textContent !== content) {
1704
1837
  arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });
@@ -1749,16 +1882,12 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1749
1882
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1750
1883
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1751
1884
  We don't need to check the value; it's always URI safe. */
1752
- if (
1753
- ALLOW_DATA_ATTR &&
1754
- !FORBID_ATTR[lcName] &&
1755
- regExpTest(DATA_ATTR, lcName)
1756
- ) {
1885
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR, lcName)) {
1757
1886
  // This attribute is safe
1758
1887
  } else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) {
1759
1888
  // This attribute is safe
1760
1889
  /* Otherwise, check the name is permitted */
1761
- } else if (!nameIsPermitted || FORBID_ATTR[lcName]) {
1890
+ } else if (!nameIsPermitted) {
1762
1891
  if (
1763
1892
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1764
1893
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1852,6 +1981,85 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1852
1981
  );
1853
1982
  };
1854
1983
 
1984
+ /**
1985
+ * Wrap an attribute value in the matching Trusted Types object when
1986
+ * the active policy requires it. Namespaced attributes pass through
1987
+ * unchanged (no TT support yet, see
1988
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
1989
+ *
1990
+ * @param lcTag lowercase tag name of the containing element
1991
+ * @param lcName lowercase attribute name
1992
+ * @param namespaceURI the attribute's namespace, if any
1993
+ * @param value the attribute value to wrap
1994
+ * @return the value, wrapped when Trusted Types demand it
1995
+ */
1996
+ const _applyTrustedTypesToAttribute = function (
1997
+ lcTag: string,
1998
+ lcName: string,
1999
+ namespaceURI: string | null,
2000
+ value: string
2001
+ ): string {
2002
+ if (
2003
+ trustedTypesPolicy &&
2004
+ typeof trustedTypes === 'object' &&
2005
+ typeof trustedTypes.getAttributeType === 'function' &&
2006
+ !namespaceURI
2007
+ ) {
2008
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
2009
+ case 'TrustedHTML': {
2010
+ return _createTrustedHTML(value);
2011
+ }
2012
+
2013
+ case 'TrustedScriptURL': {
2014
+ return _createTrustedScriptURL(value);
2015
+ }
2016
+
2017
+ default: {
2018
+ break;
2019
+ }
2020
+ }
2021
+ }
2022
+
2023
+ return value;
2024
+ };
2025
+
2026
+ /**
2027
+ * Write a modified attribute value back onto the element. On
2028
+ * success, re-probe for clobbering introduced by the new value and
2029
+ * remove the element when found; otherwise pop the removal entry
2030
+ * recorded by the earlier _removeAttribute (long-standing pairing
2031
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
2032
+ * failure, remove the attribute instead.
2033
+ *
2034
+ * @param currentNode the element carrying the attribute
2035
+ * @param name the attribute name as present on the element
2036
+ * @param namespaceURI the attribute's namespace, if any
2037
+ * @param value the new attribute value
2038
+ */
2039
+ const _setAttributeValue = function (
2040
+ currentNode: Element,
2041
+ name: string,
2042
+ namespaceURI: string | null,
2043
+ value: string
2044
+ ): void {
2045
+ try {
2046
+ if (namespaceURI) {
2047
+ currentNode.setAttributeNS(namespaceURI, name, value);
2048
+ } else {
2049
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
2050
+ currentNode.setAttribute(name, value);
2051
+ }
2052
+
2053
+ if (_isClobbered(currentNode)) {
2054
+ _forceRemove(currentNode);
2055
+ } else {
2056
+ arrayPop(DOMPurify.removed);
2057
+ }
2058
+ } catch (_) {
2059
+ _removeAttribute(name, currentNode);
2060
+ }
2061
+ };
2062
+
1855
2063
  /**
1856
2064
  * _sanitizeAttributes
1857
2065
  *
@@ -1881,6 +2089,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1881
2089
  forceKeepAttr: undefined,
1882
2090
  };
1883
2091
  let l = attributes.length;
2092
+ const lcTag = transformCaseFunc(currentNode.nodeName);
1884
2093
 
1885
2094
  /* Go backwards over all attributes; safely remove bad ones */
1886
2095
  while (l--) {
@@ -1933,7 +2142,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1933
2142
  continue;
1934
2143
  }
1935
2144
 
1936
- /* Did the hooks approve of the attribute? */
2145
+ /* Did the hooks force-keep the attribute? */
1937
2146
  if (hookEvent.forceKeepAttr) {
1938
2147
  continue;
1939
2148
  }
@@ -1945,70 +2154,31 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1945
2154
  }
1946
2155
 
1947
2156
  /* Work around a security issue in jQuery 3.0 */
1948
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
2157
+ if (
2158
+ !ALLOW_SELF_CLOSE_IN_ATTR &&
2159
+ regExpTest(EXPRESSIONS.SELF_CLOSING_TAG, value)
2160
+ ) {
1949
2161
  _removeAttribute(name, currentNode);
1950
2162
  continue;
1951
2163
  }
1952
2164
 
1953
2165
  /* Sanitize attribute content to be template-safe */
1954
2166
  if (SAFE_FOR_TEMPLATES) {
1955
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
1956
- value = stringReplace(value, expr, ' ');
1957
- });
2167
+ value = _stripTemplateExpressions(value);
1958
2168
  }
1959
2169
 
1960
2170
  /* Is `value` valid for this attribute? */
1961
- const lcTag = transformCaseFunc(currentNode.nodeName);
1962
2171
  if (!_isValidAttribute(lcTag, lcName, value)) {
1963
2172
  _removeAttribute(name, currentNode);
1964
2173
  continue;
1965
2174
  }
1966
2175
 
1967
2176
  /* Handle attributes that require Trusted Types */
1968
- if (
1969
- trustedTypesPolicy &&
1970
- typeof trustedTypes === 'object' &&
1971
- typeof trustedTypes.getAttributeType === 'function'
1972
- ) {
1973
- if (namespaceURI) {
1974
- /* Namespaces are not yet supported, see https://bugs.chromium.org/p/chromium/issues/detail?id=1305293 */
1975
- } else {
1976
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1977
- case 'TrustedHTML': {
1978
- value = _createTrustedHTML(value);
1979
- break;
1980
- }
1981
-
1982
- case 'TrustedScriptURL': {
1983
- value = _createTrustedScriptURL(value);
1984
- break;
1985
- }
1986
-
1987
- default: {
1988
- break;
1989
- }
1990
- }
1991
- }
1992
- }
2177
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
1993
2178
 
1994
2179
  /* Handle invalid data-* attribute set by try-catching it */
1995
2180
  if (value !== initValue) {
1996
- try {
1997
- if (namespaceURI) {
1998
- currentNode.setAttributeNS(namespaceURI, name, value);
1999
- } else {
2000
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
2001
- currentNode.setAttribute(name, value);
2002
- }
2003
-
2004
- if (_isClobbered(currentNode)) {
2005
- _forceRemove(currentNode);
2006
- } else {
2007
- arrayPop(DOMPurify.removed);
2008
- }
2009
- } catch (_) {
2010
- _removeAttribute(name, currentNode);
2011
- }
2181
+ _setAttributeValue(currentNode, name, namespaceURI, value);
2012
2182
  }
2013
2183
  }
2014
2184
 
@@ -2060,9 +2230,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2060
2230
  ? getNodeType(shadowNode)
2061
2231
  : shadowNode.nodeType;
2062
2232
  if (shadowNodeType === NODE_TYPE.element) {
2063
- const innerSr = getShadowRoot
2064
- ? getShadowRoot(shadowNode)
2065
- : (shadowNode as Element).shadowRoot;
2233
+ const innerSr = getShadowRoot(shadowNode);
2066
2234
  if (_isDocumentFragment(innerSr)) {
2067
2235
  _sanitizeAttachedShadowRoots(innerSr);
2068
2236
  _sanitizeShadowDOM(innerSr);
@@ -2129,9 +2297,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2129
2297
  /* (pushed last → processed first) Children, snapshotted in reverse so
2130
2298
  the first child is processed first. Snapshotting matters because a
2131
2299
  hook may detach siblings mid-walk. */
2132
- const childNodes = getChildNodes
2133
- ? getChildNodes(node)
2134
- : (node as Element).childNodes;
2300
+ const childNodes = getChildNodes(node);
2135
2301
  if (childNodes) {
2136
2302
  for (let i = childNodes.length - 1; i >= 0; --i) {
2137
2303
  stack.push({ node: childNodes[i], shadow: null });
@@ -2160,9 +2326,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2160
2326
  silently skipped foreign-realm shadow roots (e.g.
2161
2327
  iframe.contentDocument attachShadow). */
2162
2328
  if (isElement) {
2163
- const sr = getShadowRoot
2164
- ? getShadowRoot(node)
2165
- : (node as Element).shadowRoot;
2329
+ const sr = getShadowRoot(node);
2166
2330
  if (_isDocumentFragment(sr)) {
2167
2331
  /* Push the deferred sanitise first so it pops after the shadow
2168
2332
  walk we push next, i.e. nested shadow roots are discovered
@@ -2421,9 +2585,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2421
2585
 
2422
2586
  /* Sanitize final string template-safe */
2423
2587
  if (SAFE_FOR_TEMPLATES) {
2424
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
2425
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
2426
- });
2588
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
2427
2589
  }
2428
2590
 
2429
2591
  return trustedTypesPolicy && RETURN_TRUSTED_TYPE
@@ -2497,352 +2659,3 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2497
2659
  }
2498
2660
 
2499
2661
  export default createDOMPurify();
2500
-
2501
- export interface DOMPurify {
2502
- /**
2503
- * Creates a DOMPurify instance using the given window-like object. Defaults to `window`.
2504
- */
2505
- (root?: WindowLike): DOMPurify;
2506
-
2507
- /**
2508
- * Version label, exposed for easier checks
2509
- * if DOMPurify is up to date or not
2510
- */
2511
- version: string;
2512
-
2513
- /**
2514
- * Array of elements that DOMPurify removed during sanitation.
2515
- * Empty if nothing was removed.
2516
- */
2517
- removed: Array<RemovedElement | RemovedAttribute>;
2518
-
2519
- /**
2520
- * Expose whether this browser supports running the full DOMPurify.
2521
- */
2522
- isSupported: boolean;
2523
-
2524
- /**
2525
- * Set the configuration once.
2526
- *
2527
- * @param cfg configuration object
2528
- */
2529
- setConfig(cfg?: Config): void;
2530
-
2531
- /**
2532
- * Removes the configuration.
2533
- */
2534
- clearConfig(): void;
2535
-
2536
- /**
2537
- * Provides core sanitation functionality.
2538
- *
2539
- * @param dirty string or DOM node
2540
- * @param cfg object
2541
- * @returns Sanitized TrustedHTML.
2542
- */
2543
- sanitize(
2544
- dirty: string | Node,
2545
- cfg: Config & { RETURN_TRUSTED_TYPE: true }
2546
- ): TrustedHTML;
2547
-
2548
- /**
2549
- * Provides core sanitation functionality.
2550
- *
2551
- * @param dirty DOM node
2552
- * @param cfg object
2553
- * @returns Sanitized DOM node.
2554
- */
2555
- sanitize(dirty: Node, cfg: Config & { IN_PLACE: true }): Node;
2556
-
2557
- /**
2558
- * Provides core sanitation functionality.
2559
- *
2560
- * @param dirty string or DOM node
2561
- * @param cfg object
2562
- * @returns Sanitized DOM node.
2563
- */
2564
- sanitize(dirty: string | Node, cfg: Config & { RETURN_DOM: true }): Node;
2565
-
2566
- /**
2567
- * Provides core sanitation functionality.
2568
- *
2569
- * @param dirty string or DOM node
2570
- * @param cfg object
2571
- * @returns Sanitized document fragment.
2572
- */
2573
- sanitize(
2574
- dirty: string | Node,
2575
- cfg: Config & { RETURN_DOM_FRAGMENT: true }
2576
- ): DocumentFragment;
2577
-
2578
- /**
2579
- * Provides core sanitation functionality.
2580
- *
2581
- * @param dirty string or DOM node
2582
- * @param cfg object
2583
- * @returns Sanitized string.
2584
- */
2585
- sanitize(dirty: string | Node, cfg?: Config): string;
2586
-
2587
- /**
2588
- * Checks if an attribute value is valid.
2589
- * Uses last set config, if any. Otherwise, uses config defaults.
2590
- *
2591
- * @param tag Tag name of containing element.
2592
- * @param attr Attribute name.
2593
- * @param value Attribute value.
2594
- * @returns Returns true if `value` is valid. Otherwise, returns false.
2595
- */
2596
- isValidAttribute(tag: string, attr: string, value: string): boolean;
2597
-
2598
- /**
2599
- * Adds a DOMPurify hook.
2600
- *
2601
- * @param entryPoint entry point for the hook to add
2602
- * @param hookFunction function to execute
2603
- */
2604
- addHook(entryPoint: BasicHookName, hookFunction: NodeHook): void;
2605
-
2606
- /**
2607
- * Adds a DOMPurify hook.
2608
- *
2609
- * @param entryPoint entry point for the hook to add
2610
- * @param hookFunction function to execute
2611
- */
2612
- addHook(entryPoint: ElementHookName, hookFunction: ElementHook): void;
2613
-
2614
- /**
2615
- * Adds a DOMPurify hook.
2616
- *
2617
- * @param entryPoint entry point for the hook to add
2618
- * @param hookFunction function to execute
2619
- */
2620
- addHook(
2621
- entryPoint: DocumentFragmentHookName,
2622
- hookFunction: DocumentFragmentHook
2623
- ): void;
2624
-
2625
- /**
2626
- * Adds a DOMPurify hook.
2627
- *
2628
- * @param entryPoint entry point for the hook to add
2629
- * @param hookFunction function to execute
2630
- */
2631
- addHook(
2632
- entryPoint: 'uponSanitizeElement',
2633
- hookFunction: UponSanitizeElementHook
2634
- ): void;
2635
-
2636
- /**
2637
- * Adds a DOMPurify hook.
2638
- *
2639
- * @param entryPoint entry point for the hook to add
2640
- * @param hookFunction function to execute
2641
- */
2642
- addHook(
2643
- entryPoint: 'uponSanitizeAttribute',
2644
- hookFunction: UponSanitizeAttributeHook
2645
- ): void;
2646
-
2647
- /**
2648
- * Remove a DOMPurify hook at a given entryPoint
2649
- * (pops it from the stack of hooks if hook not specified)
2650
- *
2651
- * @param entryPoint entry point for the hook to remove
2652
- * @param hookFunction optional specific hook to remove
2653
- * @returns removed hook
2654
- */
2655
- removeHook(
2656
- entryPoint: BasicHookName,
2657
- hookFunction?: NodeHook
2658
- ): NodeHook | undefined;
2659
-
2660
- /**
2661
- * Remove a DOMPurify hook at a given entryPoint
2662
- * (pops it from the stack of hooks if hook not specified)
2663
- *
2664
- * @param entryPoint entry point for the hook to remove
2665
- * @param hookFunction optional specific hook to remove
2666
- * @returns removed hook
2667
- */
2668
- removeHook(
2669
- entryPoint: ElementHookName,
2670
- hookFunction?: ElementHook
2671
- ): ElementHook | undefined;
2672
-
2673
- /**
2674
- * Remove a DOMPurify hook at a given entryPoint
2675
- * (pops it from the stack of hooks if hook not specified)
2676
- *
2677
- * @param entryPoint entry point for the hook to remove
2678
- * @param hookFunction optional specific hook to remove
2679
- * @returns removed hook
2680
- */
2681
- removeHook(
2682
- entryPoint: DocumentFragmentHookName,
2683
- hookFunction?: DocumentFragmentHook
2684
- ): DocumentFragmentHook | undefined;
2685
-
2686
- /**
2687
- * Remove a DOMPurify hook at a given entryPoint
2688
- * (pops it from the stack of hooks if hook not specified)
2689
- *
2690
- * @param entryPoint entry point for the hook to remove
2691
- * @param hookFunction optional specific hook to remove
2692
- * @returns removed hook
2693
- */
2694
- removeHook(
2695
- entryPoint: 'uponSanitizeElement',
2696
- hookFunction?: UponSanitizeElementHook
2697
- ): UponSanitizeElementHook | undefined;
2698
-
2699
- /**
2700
- * Remove a DOMPurify hook at a given entryPoint
2701
- * (pops it from the stack of hooks if hook not specified)
2702
- *
2703
- * @param entryPoint entry point for the hook to remove
2704
- * @param hookFunction optional specific hook to remove
2705
- * @returns removed hook
2706
- */
2707
- removeHook(
2708
- entryPoint: 'uponSanitizeAttribute',
2709
- hookFunction?: UponSanitizeAttributeHook
2710
- ): UponSanitizeAttributeHook | undefined;
2711
-
2712
- /**
2713
- * Removes all DOMPurify hooks at a given entryPoint
2714
- *
2715
- * @param entryPoint entry point for the hooks to remove
2716
- */
2717
- removeHooks(entryPoint: HookName): void;
2718
-
2719
- /**
2720
- * Removes all DOMPurify hooks.
2721
- */
2722
- removeAllHooks(): void;
2723
- }
2724
-
2725
- /**
2726
- * An element removed by DOMPurify.
2727
- */
2728
- export interface RemovedElement {
2729
- /**
2730
- * The element that was removed.
2731
- */
2732
- element: Node;
2733
- }
2734
-
2735
- /**
2736
- * An element removed by DOMPurify.
2737
- */
2738
- export interface RemovedAttribute {
2739
- /**
2740
- * The attribute that was removed.
2741
- */
2742
- attribute: Attr | null;
2743
-
2744
- /**
2745
- * The element that the attribute was removed.
2746
- */
2747
- from: Node;
2748
- }
2749
-
2750
- type BasicHookName =
2751
- | 'beforeSanitizeElements'
2752
- | 'afterSanitizeElements'
2753
- | 'uponSanitizeShadowNode';
2754
- type ElementHookName = 'beforeSanitizeAttributes' | 'afterSanitizeAttributes';
2755
- type DocumentFragmentHookName =
2756
- | 'beforeSanitizeShadowDOM'
2757
- | 'afterSanitizeShadowDOM';
2758
- type UponSanitizeElementHookName = 'uponSanitizeElement';
2759
- type UponSanitizeAttributeHookName = 'uponSanitizeAttribute';
2760
-
2761
- interface HooksMap {
2762
- beforeSanitizeElements: NodeHook[];
2763
- afterSanitizeElements: NodeHook[];
2764
- beforeSanitizeShadowDOM: DocumentFragmentHook[];
2765
- uponSanitizeShadowNode: NodeHook[];
2766
- afterSanitizeShadowDOM: DocumentFragmentHook[];
2767
- beforeSanitizeAttributes: ElementHook[];
2768
- afterSanitizeAttributes: ElementHook[];
2769
- uponSanitizeElement: UponSanitizeElementHook[];
2770
- uponSanitizeAttribute: UponSanitizeAttributeHook[];
2771
- }
2772
-
2773
- type ArrayElement<T> = T extends Array<infer U> ? U : never;
2774
-
2775
- type HookFunction = ArrayElement<HooksMap[keyof HooksMap]>;
2776
-
2777
- export type HookName =
2778
- | BasicHookName
2779
- | ElementHookName
2780
- | DocumentFragmentHookName
2781
- | UponSanitizeElementHookName
2782
- | UponSanitizeAttributeHookName;
2783
-
2784
- export type NodeHook = (
2785
- this: DOMPurify,
2786
- currentNode: Node,
2787
- hookEvent: null,
2788
- config: Config
2789
- ) => void;
2790
-
2791
- export type ElementHook = (
2792
- this: DOMPurify,
2793
- currentNode: Element,
2794
- hookEvent: null,
2795
- config: Config
2796
- ) => void;
2797
-
2798
- export type DocumentFragmentHook = (
2799
- this: DOMPurify,
2800
- currentNode: DocumentFragment,
2801
- hookEvent: null,
2802
- config: Config
2803
- ) => void;
2804
-
2805
- export type UponSanitizeElementHook = (
2806
- this: DOMPurify,
2807
- currentNode: Node,
2808
- hookEvent: UponSanitizeElementHookEvent,
2809
- config: Config
2810
- ) => void;
2811
-
2812
- export type UponSanitizeAttributeHook = (
2813
- this: DOMPurify,
2814
- currentNode: Element,
2815
- hookEvent: UponSanitizeAttributeHookEvent,
2816
- config: Config
2817
- ) => void;
2818
-
2819
- export interface UponSanitizeElementHookEvent {
2820
- tagName: string;
2821
- allowedTags: Record<string, boolean>;
2822
- }
2823
-
2824
- export interface UponSanitizeAttributeHookEvent {
2825
- attrName: string;
2826
- attrValue: string;
2827
- keepAttr: boolean;
2828
- allowedAttributes: Record<string, boolean>;
2829
- forceKeepAttr: boolean | undefined;
2830
- }
2831
-
2832
- /**
2833
- * A `Window`-like object containing the properties and types that DOMPurify requires.
2834
- */
2835
- export type WindowLike = Pick<
2836
- typeof globalThis,
2837
- | 'DocumentFragment'
2838
- | 'HTMLTemplateElement'
2839
- | 'Node'
2840
- | 'Element'
2841
- | 'NodeFilter'
2842
- | 'NamedNodeMap'
2843
- | 'HTMLFormElement'
2844
- | 'DOMParser'
2845
- > & {
2846
- document?: Document;
2847
- MozNamedAttrMap?: typeof window.NamedNodeMap;
2848
- } & Pick<TrustedTypesWindow, 'trustedTypes'>;