@cognigy/webchat 3.46.0 → 3.47.0

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.
@@ -1,7 +1,7 @@
1
1
  /*!
2
- * webchat.esm.js v3.46.0
3
- * https://github.com/Cognigy/Webchat/tree/v3.46.0
4
- * https://github.com/Cognigy/Webchat/tree/v3.46.0/OSS_LICENSES.txt
2
+ * webchat.esm.js v3.47.0
3
+ * https://github.com/Cognigy/Webchat/tree/v3.47.0
4
+ * https://github.com/Cognigy/Webchat/tree/v3.47.0/OSS_LICENSES.txt
5
5
  */
6
6
  import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react";
7
7
  import * as __WEBPACK_EXTERNAL_MODULE_react_dom_7dac9eee__ from "react-dom";
@@ -38210,10 +38210,20 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38210
38210
  /**
38211
38211
  * Emits the `webchat/close-button` analytics event.
38212
38212
  *
38213
- * Fired only from the close ("X") button handlers — the chat window header
38214
- * (`handleCloseAndReset`) and the disconnect overlay (`handleClose`). It is
38215
- * NOT emitted for the toggle button, the home-screen close (which minimizes),
38216
- * or the programmatic `webchat.close()` / `webchat.toggle()` APIs.
38213
+ * Fired from every close ("X") button handler — the chat window header
38214
+ * (`handleCloseAndReset`), the disconnect overlay (`handleClose`) and the home
38215
+ * screen (`handleHomeScreenClose`). It is NOT emitted for the toggle button,
38216
+ * the header minimize button, or the programmatic `webchat.close()` /
38217
+ * `webchat.toggle()` APIs.
38218
+ *
38219
+ * The payload carries the connection status at close time so integrations can
38220
+ * decide whether acting on the close applies: `connected` is the live socket
38221
+ * state, and `hadConnection` is `true` once a session has connected this visit
38222
+ * (and stays `true` after it drops — e.g. the disconnect overlay, or a home
38223
+ * screen returned to after connecting). It is `false` only before the first
38224
+ * connection (a first-visit home screen, the privacy notice, or the
38225
+ * previous-conversations screen), so a documented handler keyed on it leaves
38226
+ * those closes alone.
38217
38227
  *
38218
38228
  * This lives in the component rather than the redux analytics middleware
38219
38229
  * (where `webchat/open|close|minimize` are emitted) on purpose: the store only
@@ -38221,7 +38231,10 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38221
38231
  * the close, so the originating interaction is only known here.
38222
38232
  */
38223
38233
  emitCloseButtonAnalytics = () => {
38224
- this.props.onEmitAnalytics("webchat/close-button");
38234
+ this.props.onEmitAnalytics("webchat/close-button", {
38235
+ connected: this.props.connected,
38236
+ hadConnection: this.state.hadConnection
38237
+ });
38225
38238
  };
38226
38239
  render() {
38227
38240
  const {
@@ -38477,6 +38490,17 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38477
38490
  // Restore focus to chat toggle button
38478
38491
  this.chatToggleButtonRef?.current?.focus?.();
38479
38492
  };
38493
+ const handleHomeScreenClose = () => {
38494
+ // The home screen's close ("X") minimizes rather than closing an active
38495
+ // session, but it is still a header close button, so it emits
38496
+ // "webchat/close-button" for parity with the chat window header and the
38497
+ // disconnect overlay. Its payload's `hadConnection` is false on a
38498
+ // first-visit home screen (nothing to act on) but true if the user
38499
+ // already connected and returned here — so a documented handler keyed on
38500
+ // `hadConnection` acts only when a session was actually started.
38501
+ handleOnMinimize();
38502
+ this.emitCloseButtonAnalytics();
38503
+ };
38480
38504
  const handleOnGoBack = () => {
38481
38505
  if (!showChatOptionsScreen && !showRatingScreen) {
38482
38506
  onSetShowPrevConversations(false);
@@ -38636,7 +38660,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38636
38660
  onSetShowHomeScreen: onSetShowHomeScreen,
38637
38661
  onStartConversation: this.handleStartConversation,
38638
38662
  onSetShowPrevConversations: onSetShowPrevConversations,
38639
- onClose: handleOnMinimize,
38663
+ onClose: handleHomeScreenClose,
38640
38664
  config: config,
38641
38665
  onEmitAnalytics: onEmitAnalytics,
38642
38666
  onSendActionButtonMessage: this.handleSendActionButtonMessage
@@ -44704,6 +44728,22 @@ class Webchat extends react__WEBPACK_IMPORTED_MODULE_0__.PureComponent {
44704
44728
  connect = async () => {
44705
44729
  this.store.dispatch((0,_store_connection_connection_middleware__WEBPACK_IMPORTED_MODULE_7__/* .connect */ .Ng)());
44706
44730
  };
44731
+
44732
+ /**
44733
+ * Closes the socket connection and cancels any in-flight automatic
44734
+ * reconnection attempts.
44735
+ *
44736
+ * Unlike `close()` (which only collapses the widget, leaving the socket and
44737
+ * its reconnection loop running) and `endSession()` (which switches to a
44738
+ * fresh session and immediately reconnects), this leaves the Webchat
44739
+ * disconnected. A new connection is established again when the conversation
44740
+ * resumes (a message is sent or the chat screen is shown) or when
44741
+ * connectivity is restored (the tab becomes visible or the network comes back
44742
+ * online).
44743
+ */
44744
+ disconnect = () => {
44745
+ this.store.dispatch((0,_store_connection_connection_middleware__WEBPACK_IMPORTED_MODULE_7__/* .disconnect */ .Zf)());
44746
+ };
44707
44747
  sendMessage = (text, data, options) => {
44708
44748
  this.store.dispatch((0,_store_messages_message_middleware__WEBPACK_IMPORTED_MODULE_4__/* .sendMessage */ ._z)({
44709
44749
  text,
@@ -56925,6 +56965,23 @@ a._button_19rd2_1._disabled_19rd2_52:focus {
56925
56965
  height: 100%;
56926
56966
  border-right: 2px solid var(--cc-primary-contrast-color);
56927
56967
  }
56968
+ ._srOnly_14hnz_1 {
56969
+ position: absolute;
56970
+ width: 1px;
56971
+ height: 1px;
56972
+ padding: 0;
56973
+ margin: -1px;
56974
+ overflow: hidden;
56975
+ clip: rect(0, 0, 0, 0);
56976
+ white-space: nowrap;
56977
+ border: 0;
56978
+ }
56979
+
56980
+ ._divider_14hnz_13 {
56981
+ border-bottom-width: 1px;
56982
+ border-bottom-style: solid;
56983
+ border-bottom-color: var(--cc-black-80);
56984
+ }
56928
56985
  ._buttons_tq8ye_1 {
56929
56986
  align-items: flex-start;
56930
56987
  display: flex;
@@ -57597,23 +57654,6 @@ article ._wrapper_zhv80_1 ._list_zhv80_8 ._listItemRoot_zhv80_8 {
57597
57654
  ._mainButtonWrapper_zhv80_144 {
57598
57655
  padding: 16px;
57599
57656
  }
57600
- ._srOnly_14hnz_1 {
57601
- position: absolute;
57602
- width: 1px;
57603
- height: 1px;
57604
- padding: 0;
57605
- margin: -1px;
57606
- overflow: hidden;
57607
- clip: rect(0, 0, 0, 0);
57608
- white-space: nowrap;
57609
- border: 0;
57610
- }
57611
-
57612
- ._divider_14hnz_13 {
57613
- border-bottom-width: 1px;
57614
- border-bottom-style: solid;
57615
- border-bottom-color: var(--cc-black-80);
57616
- }
57617
57657
  ._slideItem_1prat_1 {
57618
57658
  position: relative;
57619
57659
  width: 206px;
@@ -58933,13 +58973,17 @@ article ._header_21mid_1 ._avatar-name_21mid_27 {
58933
58973
  margin: 0;
58934
58974
  padding: 0;
58935
58975
  }
58936
- article ._avatar_ry0w9_1 {
58976
+ article ._avatar_o5ge0_1 {
58937
58977
  border-radius: 1000px;
58938
58978
  width: 28px;
58939
58979
  height: 28px;
58940
- background-color: var(--cc-primary-color);
58941
58980
  box-sizing: border-box;
58942
58981
  }
58982
+
58983
+ /* Background only for the bundled default avatars (see Avatar.tsx); custom images stay transparent (AB#90506). */
58984
+ article ._avatar_o5ge0_1[data-default-avatar] {
58985
+ background-color: var(--cc-primary-color);
58986
+ }
58943
58987
  :root {
58944
58988
  --cc-font-family: var(--webchat-font-family, Figtree);
58945
58989
  --cc-action-button-border-radius: var(--webchat-action-button-border-radius, 19px);
@@ -85274,11 +85318,16 @@ const Text = props2 => {
85274
85318
  a: ({
85275
85319
  node: _node,
85276
85320
  ...props22
85277
- }) => /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("a", {
85321
+ }) => (
85322
+ /* eslint-disable-next-line jsx-a11y/anchor-has-content -- react-markdown
85323
+ component override: the link text always arrives as children via the
85324
+ {...props} spread from the markdown AST; the rule cannot see it. */
85325
+ /* @__PURE__ */
85326
+ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("a", {
85278
85327
  target: "_blank",
85279
85328
  rel: "noreferrer",
85280
85329
  ...props22
85281
- }),
85330
+ })),
85282
85331
  p: ({
85283
85332
  node: _node,
85284
85333
  children,
@@ -85928,6 +85977,12 @@ const classes$e = {
85928
85977
  imageDisabled,
85929
85978
  buttonImageContainer
85930
85979
  };
85980
+ const srOnly = "_srOnly_14hnz_1";
85981
+ const divider = "_divider_14hnz_13";
85982
+ const mainClasses = {
85983
+ srOnly,
85984
+ divider
85985
+ };
85931
85986
  const ActionButton = props2 => {
85932
85987
  const {
85933
85988
  button: button2,
@@ -85950,32 +86005,29 @@ const ActionButton = props2 => {
85950
86005
  const customAllowedHtmlTags = config2?.settings?.widgetSettings?.customAllowedHtmlTags;
85951
86006
  const __html = config2?.settings?.layout?.disableHtmlContentSanitization ? buttonLabel : sanitizeHTMLWithConfig(buttonLabel, customAllowedHtmlTags);
85952
86007
  const isPhoneNumber = button2.payload && (buttonType === "phone_number" || buttonType === "user_phone_number");
85953
- const buttonTitle = button2.title || "";
85954
86008
  const isWebURL = "type" in button2 && button2.type === "web_url";
85955
86009
  const isWebURLButtonTargetBlank = isWebURL && button2.target !== "_self";
85956
86010
  const opensInNewTabLabel = config2?.settings?.customTranslations?.ariaLabels?.opensInNewTab || "Opens in new tab";
85957
- const getAriaLabel = () => {
85958
- const isURLInNewTab = isWebURL && isWebURLButtonTargetBlank;
85959
- const newTabURLButtonTitle = `${buttonTitle}. ${opensInNewTabLabel}`;
85960
- const buttonTitleWithTarget = isURLInNewTab ? newTabURLButtonTitle : button2.title;
85961
- if (total > 1) {
85962
- return interpolateString(config2?.settings?.customTranslations?.ariaLabels?.actionButtonPositionText ?? "{position} of {total}", {
85963
- position: position2.toString(),
85964
- total: total.toString()
85965
- }) + ": " + buttonTitleWithTarget;
85966
- } else if (total <= 1 && isURLInNewTab) {
85967
- return newTabURLButtonTitle;
85968
- }
85969
- };
85970
- const PhoneNumberAnchor = props22 => button2.payload ? /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("a", {
86011
+ const positionText = total > 1 ? interpolateString(config2?.settings?.customTranslations?.ariaLabels?.actionButtonPositionText ?? "{position} of {total}", {
86012
+ position: position2.toString(),
86013
+ total: total.toString()
86014
+ }) + ": " : null;
86015
+ const PhoneNumberAnchor = props22 => (
86016
+ /* eslint-disable-next-line jsx-a11y/anchor-has-content -- render-prop component:
86017
+ the button title always arrives as children via the {...props} spread; the rule
86018
+ cannot see through the indirection. */
86019
+ button2.payload ? /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("a", {
85971
86020
  ...props22,
85972
86021
  href: `tel:${button2.payload}`
85973
- }) : null;
85974
- const Anchor2 = props22 => isWebURL ? /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("a", {
86022
+ }) : null);
86023
+ const Anchor2 = props22 => (
86024
+ /* eslint-disable-next-line jsx-a11y/anchor-has-content -- same render-prop pattern
86025
+ as PhoneNumberAnchor: children come in via the {...props} spread. */
86026
+ isWebURL ? /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("a", {
85975
86027
  ...props22,
85976
86028
  href: button2.url,
85977
86029
  target: button2.target
85978
- }) : null;
86030
+ }) : null);
85979
86031
  const Button2 = props22 => /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("button", {
85980
86032
  ...props22,
85981
86033
  disabled: disabled2
@@ -86029,10 +86081,12 @@ const ActionButton = props2 => {
86029
86081
  return /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(Component, {
86030
86082
  onClick: onClick2,
86031
86083
  className: classnames$1(classes$e.button, isWebURL && classes$e.url, props2.className, disabled2 && classes$e.disabled, disabled2 && "disabled", isPhoneNumber && "phone-number-or-url-anchor", isWebURL && "phone-number-or-url-anchor"),
86032
- "aria-label": getAriaLabel(),
86033
86084
  "aria-disabled": disabled2,
86034
86085
  tabIndex: disabled2 ? -1 : 0,
86035
- children: [!!buttonImage2 && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("div", {
86086
+ children: [positionText && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
86087
+ className: mainClasses.srOnly,
86088
+ children: positionText
86089
+ }), !!buttonImage2 && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("div", {
86036
86090
  className: classes$e.buttonImageContainer,
86037
86091
  children: /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", {
86038
86092
  src: buttonImage2,
@@ -86046,7 +86100,10 @@ const ActionButton = props2 => {
86046
86100
  __html
86047
86101
  },
86048
86102
  className: !!buttonImage2 && classes$e.buttonLabelWithImage
86049
- }), renderIcon()]
86103
+ }), renderIcon(), isWebURL && isWebURLButtonTargetBlank && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
86104
+ className: mainClasses.srOnly,
86105
+ children: `, ${opensInNewTabLabel}`
86106
+ })]
86050
86107
  });
86051
86108
  };
86052
86109
  const buttons$1 = "_buttons_tq8ye_1";
@@ -92731,7 +92788,12 @@ const Controls = props2 => {
92731
92788
  })]
92732
92789
  })
92733
92790
  })]
92734
- }), altText && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("a", {
92791
+ }), altText &&
92792
+ /* eslint-disable-next-line jsx-a11y/anchor-has-content -- intentionally
92793
+ content-less: hidden (display:none, aria-hidden) data-URI download target,
92794
+ activated programmatically by the visible "Download Transcript" button. */
92795
+ /* @__PURE__ */
92796
+ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("a", {
92735
92797
  ref: downloadTranscriptLinkRef,
92736
92798
  href: `data:text/plain;charset=utf-8,${encodeURIComponent(altText)}`,
92737
92799
  download: "audio-transcript.txt",
@@ -92993,12 +93055,6 @@ const classes$9 = {
92993
93055
  listItemButtonWrapper,
92994
93056
  mainButtonWrapper
92995
93057
  };
92996
- const srOnly = "_srOnly_14hnz_1";
92997
- const divider = "_divider_14hnz_13";
92998
- const mainClasses = {
92999
- srOnly,
93000
- divider
93001
- };
93002
93058
  const ListItem = props2 => {
93003
93059
  const {
93004
93060
  action,
@@ -93059,9 +93115,11 @@ const ListItem = props2 => {
93059
93115
  backgroundImage: getBackgroundImage(image_url)
93060
93116
  },
93061
93117
  "data-testid": "regular-image",
93062
- children: /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
93118
+ children: image_alt_text ? /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
93063
93119
  role: "img",
93064
93120
  "aria-label": image_alt_text
93121
+ }) : /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
93122
+ "aria-hidden": "true"
93065
93123
  })
93066
93124
  });
93067
93125
  }, [image_alt_text, image_url]);
@@ -118672,7 +118730,12 @@ const DatePicker = () => {
118672
118730
  "aria-controls": `webchat-plugin-date-picker-${message2.id}`,
118673
118731
  "aria-haspopup": "dialog",
118674
118732
  children: openText
118675
- }), showPicker && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
118733
+ }), showPicker &&
118734
+ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- standard
118735
+ ARIA modal pattern: the keydown listener on the role="dialog" container implements
118736
+ the dialog's focus trap and Escape handling (APG dialog-modal), not a click target. */
118737
+ /* @__PURE__ */
118738
+ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
118676
118739
  ref: datePickerRef,
118677
118740
  id: `webchat-plugin-date-picker-${message2.id}`,
118678
118741
  className: classnames$1(classes$6.wrapper, "webchat-plugin-date-picker"),
@@ -134729,7 +134792,7 @@ const classes$3 = {
134729
134792
  timestamp,
134730
134793
  "avatar-name": "_avatar-name_21mid_27"
134731
134794
  };
134732
- const avatar = "_avatar_ry0w9_1";
134795
+ const avatar = "_avatar_o5ge0_1";
134733
134796
  const classes$2 = {
134734
134797
  avatar
134735
134798
  };
@@ -134758,11 +134821,13 @@ const Avatar = props2 => {
134758
134821
  setAvatarUrl(botAvatar);
134759
134822
  }
134760
134823
  }, [message2.avatarUrl, message2?.source, overrides.agentAvatarOverrideUrlOnce, overrides.botAvatarOverrideUrlOnce]);
134824
+ const isDefaultAvatar = avatarUrl === botAvatar || avatarUrl === placeholderAvatar;
134761
134825
  const classNames = classnames$1(classes$2.avatar, props2.className, "webchat-avatar", message2?.source);
134762
134826
  return /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", {
134763
134827
  alt: "",
134764
134828
  className: classNames,
134765
134829
  src: avatarUrl,
134830
+ "data-default-avatar": isDefaultAvatar ? "" : void 0,
134766
134831
  onError: () => setAvatarUrl(placeholderAvatar),
134767
134832
  "data-testid": `${message2?.source || "unknown source"}-avatar`
134768
134833
  });
@@ -172256,24 +172321,65 @@ function isUrl(fileUrlOrPath) {
172256
172321
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
172257
172322
  /* harmony export */ A: () => (/* binding */ purify)
172258
172323
  /* harmony export */ });
172259
- /*! @license DOMPurify 3.4.0 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.0/LICENSE */
172324
+ /*! @license DOMPurify 3.4.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.11/LICENSE */
172260
172325
 
172261
- const {
172262
- entries,
172263
- setPrototypeOf,
172264
- isFrozen,
172265
- getPrototypeOf,
172266
- getOwnPropertyDescriptor
172267
- } = Object;
172268
- let {
172269
- freeze,
172270
- seal,
172271
- create
172272
- } = Object; // eslint-disable-line import/no-mutable-exports
172273
- let {
172274
- apply,
172275
- construct
172276
- } = typeof Reflect !== 'undefined' && Reflect;
172326
+ function _arrayLikeToArray(r, a) {
172327
+ (null == a || a > r.length) && (a = r.length);
172328
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
172329
+ return n;
172330
+ }
172331
+ function _arrayWithHoles(r) {
172332
+ if (Array.isArray(r)) return r;
172333
+ }
172334
+ function _iterableToArrayLimit(r, l) {
172335
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
172336
+ if (null != t) {
172337
+ var e,
172338
+ n,
172339
+ i,
172340
+ u,
172341
+ a = [],
172342
+ f = true,
172343
+ o = false;
172344
+ try {
172345
+ if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
172346
+ } catch (r) {
172347
+ o = true, n = r;
172348
+ } finally {
172349
+ try {
172350
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
172351
+ } finally {
172352
+ if (o) throw n;
172353
+ }
172354
+ }
172355
+ return a;
172356
+ }
172357
+ }
172358
+ function _nonIterableRest() {
172359
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
172360
+ }
172361
+ function _slicedToArray(r, e) {
172362
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
172363
+ }
172364
+ function _unsupportedIterableToArray(r, a) {
172365
+ if (r) {
172366
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
172367
+ var t = {}.toString.call(r).slice(8, -1);
172368
+ return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
172369
+ }
172370
+ }
172371
+
172372
+ const entries = Object.entries,
172373
+ setPrototypeOf = Object.setPrototypeOf,
172374
+ isFrozen = Object.isFrozen,
172375
+ getPrototypeOf = Object.getPrototypeOf,
172376
+ getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
172377
+ let freeze = Object.freeze,
172378
+ seal = Object.seal,
172379
+ create = Object.create; // eslint-disable-line import/no-mutable-exports
172380
+ let _ref = typeof Reflect !== 'undefined' && Reflect,
172381
+ apply = _ref.apply,
172382
+ construct = _ref.construct;
172277
172383
  if (!freeze) {
172278
172384
  freeze = function freeze(x) {
172279
172385
  return x;
@@ -172305,13 +172411,19 @@ const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
172305
172411
  const arrayPop = unapply(Array.prototype.pop);
172306
172412
  const arrayPush = unapply(Array.prototype.push);
172307
172413
  const arraySplice = unapply(Array.prototype.splice);
172414
+ const arrayIsArray = Array.isArray;
172308
172415
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
172309
172416
  const stringToString = unapply(String.prototype.toString);
172310
172417
  const stringMatch = unapply(String.prototype.match);
172311
172418
  const stringReplace = unapply(String.prototype.replace);
172312
172419
  const stringIndexOf = unapply(String.prototype.indexOf);
172313
172420
  const stringTrim = unapply(String.prototype.trim);
172421
+ const numberToString = unapply(Number.prototype.toString);
172422
+ const booleanToString = unapply(Boolean.prototype.toString);
172423
+ const bigintToString = typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);
172424
+ const symbolToString = typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);
172314
172425
  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
172426
+ const objectToString = unapply(Object.prototype.toString);
172315
172427
  const regExpTest = unapply(RegExp.prototype.test);
172316
172428
  const typeErrorCreate = unconstruct(TypeError);
172317
172429
  /**
@@ -172361,6 +172473,9 @@ function addToSet(set, array) {
172361
172473
  // Prevent prototype setters from intercepting set as a this value.
172362
172474
  setPrototypeOf(set, null);
172363
172475
  }
172476
+ if (!arrayIsArray(array)) {
172477
+ return set;
172478
+ }
172364
172479
  let l = array.length;
172365
172480
  while (l--) {
172366
172481
  let element = array[l];
@@ -172401,10 +172516,13 @@ function cleanArray(array) {
172401
172516
  */
172402
172517
  function clone(object) {
172403
172518
  const newObject = create(null);
172404
- for (const [property, value] of entries(object)) {
172519
+ for (const _ref2 of entries(object)) {
172520
+ var _ref3 = _slicedToArray(_ref2, 2);
172521
+ const property = _ref3[0];
172522
+ const value = _ref3[1];
172405
172523
  const isPropertyExist = objectHasOwnProperty(object, property);
172406
172524
  if (isPropertyExist) {
172407
- if (Array.isArray(value)) {
172525
+ if (arrayIsArray(value)) {
172408
172526
  newObject[property] = cleanArray(value);
172409
172527
  } else if (value && typeof value === 'object' && value.constructor === Object) {
172410
172528
  newObject[property] = clone(value);
@@ -172415,6 +172533,58 @@ function clone(object) {
172415
172533
  }
172416
172534
  return newObject;
172417
172535
  }
172536
+ /**
172537
+ * Convert non-node values into strings without depending on direct property access.
172538
+ *
172539
+ * @param value - The value to stringify.
172540
+ * @returns A string representation of the provided value.
172541
+ */
172542
+ function stringifyValue(value) {
172543
+ switch (typeof value) {
172544
+ case 'string':
172545
+ {
172546
+ return value;
172547
+ }
172548
+ case 'number':
172549
+ {
172550
+ return numberToString(value);
172551
+ }
172552
+ case 'boolean':
172553
+ {
172554
+ return booleanToString(value);
172555
+ }
172556
+ case 'bigint':
172557
+ {
172558
+ return bigintToString ? bigintToString(value) : '0';
172559
+ }
172560
+ case 'symbol':
172561
+ {
172562
+ return symbolToString ? symbolToString(value) : 'Symbol()';
172563
+ }
172564
+ case 'undefined':
172565
+ {
172566
+ return objectToString(value);
172567
+ }
172568
+ case 'function':
172569
+ case 'object':
172570
+ {
172571
+ if (value === null) {
172572
+ return objectToString(value);
172573
+ }
172574
+ const valueAsRecord = value;
172575
+ const valueToString = lookupGetter(valueAsRecord, 'toString');
172576
+ if (typeof valueToString === 'function') {
172577
+ const stringified = valueToString(valueAsRecord);
172578
+ return typeof stringified === 'string' ? stringified : objectToString(stringified);
172579
+ }
172580
+ return objectToString(value);
172581
+ }
172582
+ default:
172583
+ {
172584
+ return objectToString(value);
172585
+ }
172586
+ }
172587
+ }
172418
172588
  /**
172419
172589
  * This method automatically checks if the prop is function or getter and behaves accordingly.
172420
172590
  *
@@ -172440,6 +172610,14 @@ function lookupGetter(object, prop) {
172440
172610
  }
172441
172611
  return fallbackValue;
172442
172612
  }
172613
+ function isRegex(value) {
172614
+ try {
172615
+ regExpTest(value, '');
172616
+ return true;
172617
+ } catch (_unused) {
172618
+ return false;
172619
+ }
172620
+ }
172443
172621
 
172444
172622
  const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
172445
172623
  const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
@@ -172455,15 +172633,14 @@ const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mgly
172455
172633
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
172456
172634
  const text = freeze(['#text']);
172457
172635
 
172458
- const 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', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);
172636
+ const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']);
172459
172637
  const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-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']);
172460
172638
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
172461
172639
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
172462
172640
 
172463
- // eslint-disable-next-line unicorn/better-regex
172464
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
172465
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
172466
- const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
172641
+ const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
172642
+ const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
172643
+ const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
172467
172644
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
172468
172645
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
172469
172646
  const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
@@ -172473,30 +172650,31 @@ const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205
172473
172650
  );
172474
172651
  const DOCTYPE_NAME = seal(/^html$/i);
172475
172652
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
172653
+ // Markup-significant character probes used by _sanitizeElements.
172654
+ // Shared module-level instances are safe despite the sticky /g flags:
172655
+ // unapply() resets lastIndex for RegExp receivers before every call.
172656
+ const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
172657
+ const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
172658
+ const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
172659
+ const SELF_CLOSING_TAG = seal(/\/>/i);
172476
172660
 
172477
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
172478
- __proto__: null,
172479
- ARIA_ATTR: ARIA_ATTR,
172480
- ATTR_WHITESPACE: ATTR_WHITESPACE,
172481
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
172482
- DATA_ATTR: DATA_ATTR,
172483
- DOCTYPE_NAME: DOCTYPE_NAME,
172484
- ERB_EXPR: ERB_EXPR,
172485
- IS_ALLOWED_URI: IS_ALLOWED_URI,
172486
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
172487
- MUSTACHE_EXPR: MUSTACHE_EXPR,
172488
- TMPLIT_EXPR: TMPLIT_EXPR
172489
- });
172490
-
172491
- /* eslint-disable @typescript-eslint/indent */
172492
172661
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
172493
172662
  const NODE_TYPE = {
172494
172663
  element: 1,
172664
+ attribute: 2,
172495
172665
  text: 3,
172666
+ cdataSection: 4,
172667
+ entityReference: 5,
172496
172668
  // Deprecated
172497
- progressingInstruction: 7,
172669
+ entityNode: 6,
172670
+ // Deprecated
172671
+ processingInstruction: 7,
172498
172672
  comment: 8,
172499
- document: 9};
172673
+ document: 9,
172674
+ documentType: 10,
172675
+ documentFragment: 11,
172676
+ notation: 12 // Deprecated
172677
+ };
172500
172678
  const getGlobal = function getGlobal() {
172501
172679
  return typeof window === 'undefined' ? null : window;
172502
172680
  };
@@ -172551,10 +172729,25 @@ const _createHooksMap = function _createHooksMap() {
172551
172729
  uponSanitizeShadowNode: []
172552
172730
  };
172553
172731
  };
172732
+ /**
172733
+ * Resolve a set-valued configuration option: a fresh set built from
172734
+ * cfg[key] when it is an own array property (seeded with a clone of
172735
+ * options.base when given, case-normalized via options.transform),
172736
+ * the fallback set otherwise.
172737
+ *
172738
+ * @param cfg the cloned, prototype-free configuration object
172739
+ * @param key the configuration property to read
172740
+ * @param fallback the set to use when the option is absent or not an array
172741
+ * @param options transform and optional base set to merge into
172742
+ * @returns the resolved set
172743
+ */
172744
+ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
172745
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
172746
+ };
172554
172747
  function createDOMPurify() {
172555
172748
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
172556
172749
  const DOMPurify = root => createDOMPurify(root);
172557
- DOMPurify.version = '3.4.0';
172750
+ DOMPurify.version = '3.4.11';
172558
172751
  DOMPurify.removed = [];
172559
172752
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
172560
172753
  // Not running in a browser, provide a factory function
@@ -172562,28 +172755,29 @@ function createDOMPurify() {
172562
172755
  DOMPurify.isSupported = false;
172563
172756
  return DOMPurify;
172564
172757
  }
172565
- let {
172566
- document
172567
- } = window;
172758
+ let document = window.document;
172568
172759
  const originalDocument = document;
172569
172760
  const currentScript = originalDocument.currentScript;
172570
- const {
172571
- DocumentFragment,
172572
- HTMLTemplateElement,
172573
- Node,
172574
- Element,
172575
- NodeFilter,
172576
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
172577
- HTMLFormElement,
172578
- DOMParser,
172579
- trustedTypes
172580
- } = window;
172761
+ window.DocumentFragment;
172762
+ const HTMLTemplateElement = window.HTMLTemplateElement,
172763
+ Node = window.Node,
172764
+ Element = window.Element,
172765
+ NodeFilter = window.NodeFilter,
172766
+ _window$NamedNodeMap = window.NamedNodeMap;
172767
+ _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap;
172768
+ window.HTMLFormElement;
172769
+ const DOMParser = window.DOMParser,
172770
+ trustedTypes = window.trustedTypes;
172581
172771
  const ElementPrototype = Element.prototype;
172582
172772
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
172583
172773
  const remove = lookupGetter(ElementPrototype, 'remove');
172584
172774
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
172585
172775
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
172586
172776
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
172777
+ const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
172778
+ const getAttributes = lookupGetter(ElementPrototype, 'attributes');
172779
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
172780
+ const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
172587
172781
  // As per issue #47, the web-components registry is inherited by a
172588
172782
  // new document created via createHTMLDocument. As per the spec
172589
172783
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -172598,33 +172792,74 @@ function createDOMPurify() {
172598
172792
  }
172599
172793
  let trustedTypesPolicy;
172600
172794
  let emptyHTML = '';
172601
- const {
172602
- implementation,
172603
- createNodeIterator,
172604
- createDocumentFragment,
172605
- getElementsByTagName
172606
- } = document;
172607
- const {
172608
- importNode
172609
- } = originalDocument;
172795
+ // The instance's own internal Trusted Types policy. Unlike a caller-supplied
172796
+ // `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
172797
+ // on duplicate policy names — and is the only policy allowed to persist
172798
+ // across configurations and survive `clearConfig()`.
172799
+ let defaultTrustedTypesPolicy;
172800
+ let defaultTrustedTypesPolicyResolved = false;
172801
+ // Tracks whether we are already inside a call to the configured Trusted Types
172802
+ // policy (`createHTML` or `createScriptURL`). If a supplied policy callback
172803
+ // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
172804
+ // re-enter the policy and recurse until the stack overflows. We detect that
172805
+ // re-entry and throw a clear, actionable error instead. The guard is shared
172806
+ // across both callbacks, because either one re-entering `sanitize` triggers
172807
+ // the same unbounded recursion.
172808
+ let IN_TRUSTED_TYPES_POLICY = 0;
172809
+ const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {
172810
+ if (IN_TRUSTED_TYPES_POLICY > 0) {
172811
+ throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or ' + 'createScriptURL) must not call DOMPurify.sanitize, as that causes ' + 'infinite recursion. Do not pass a policy whose callbacks wrap ' + 'DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted ' + 'Types" section of the README.');
172812
+ }
172813
+ };
172814
+ const _createTrustedHTML = function _createTrustedHTML(html) {
172815
+ _assertNotInTrustedTypesPolicy();
172816
+ IN_TRUSTED_TYPES_POLICY++;
172817
+ try {
172818
+ return trustedTypesPolicy.createHTML(html);
172819
+ } finally {
172820
+ IN_TRUSTED_TYPES_POLICY--;
172821
+ }
172822
+ };
172823
+ const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {
172824
+ _assertNotInTrustedTypesPolicy();
172825
+ IN_TRUSTED_TYPES_POLICY++;
172826
+ try {
172827
+ return trustedTypesPolicy.createScriptURL(scriptUrl);
172828
+ } finally {
172829
+ IN_TRUSTED_TYPES_POLICY--;
172830
+ }
172831
+ };
172832
+ // Lazily resolve (and cache) the instance's internal default policy.
172833
+ // Resolution is attempted at most once: a successful `createPolicy` cannot be
172834
+ // repeated (Trusted Types throws on duplicate names), and a failed or
172835
+ // unsupported attempt must not be retried on every parse.
172836
+ const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() {
172837
+ if (!defaultTrustedTypesPolicyResolved) {
172838
+ defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
172839
+ defaultTrustedTypesPolicyResolved = true;
172840
+ }
172841
+ return defaultTrustedTypesPolicy;
172842
+ };
172843
+ const _document = document,
172844
+ implementation = _document.implementation,
172845
+ createNodeIterator = _document.createNodeIterator,
172846
+ createDocumentFragment = _document.createDocumentFragment,
172847
+ getElementsByTagName = _document.getElementsByTagName;
172848
+ const importNode = originalDocument.importNode;
172610
172849
  let hooks = _createHooksMap();
172611
172850
  /**
172612
172851
  * Expose whether this browser supports running the full DOMPurify.
172613
172852
  */
172614
172853
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
172615
- const {
172616
- MUSTACHE_EXPR,
172617
- ERB_EXPR,
172618
- TMPLIT_EXPR,
172619
- DATA_ATTR,
172620
- ARIA_ATTR,
172621
- IS_SCRIPT_OR_DATA,
172622
- ATTR_WHITESPACE,
172623
- CUSTOM_ELEMENT
172624
- } = EXPRESSIONS;
172625
- let {
172626
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
172627
- } = EXPRESSIONS;
172854
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
172855
+ ERB_EXPR$1 = ERB_EXPR,
172856
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
172857
+ DATA_ATTR$1 = DATA_ATTR,
172858
+ ARIA_ATTR$1 = ARIA_ATTR,
172859
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
172860
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
172861
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
172862
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
172628
172863
  /**
172629
172864
  * We consider the elements and attributes below to be safe. Ideally
172630
172865
  * don't add any new ones but feel free to remove unwanted ones.
@@ -172701,6 +172936,13 @@ function createDOMPurify() {
172701
172936
  let WHOLE_DOCUMENT = false;
172702
172937
  /* Track whether config is already set on this instance of DOMPurify. */
172703
172938
  let SET_CONFIG = false;
172939
+ /* Pristine allowlist bindings captured at setConfig() time. On the
172940
+ * persistent-config path sanitize() restores the sets from these before
172941
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
172942
+ * carry across calls. Null until setConfig() is called; reset by
172943
+ * clearConfig(). */
172944
+ let SET_CONFIG_ALLOWED_TAGS = null;
172945
+ let SET_CONFIG_ALLOWED_ATTR = null;
172704
172946
  /* Decide if all elements (e.g. style, script) must be children of
172705
172947
  * document.body. By default, browsers might move them to document.head */
172706
172948
  let FORCE_BODY = false;
@@ -172743,7 +172985,17 @@ function createDOMPurify() {
172743
172985
  let USE_PROFILES = {};
172744
172986
  /* Tags to ignore content of when KEEP_CONTENT is true */
172745
172987
  let FORBID_CONTENTS = null;
172746
- 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']);
172988
+ const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script',
172989
+ // <selectedcontent> mirrors the selected <option>'s subtree, cloned by
172990
+ // the UA (customizable <select>) — including any on* handlers — and the
172991
+ // engine re-mirrors synchronously whenever a removal changes which
172992
+ // option/selectedcontent is current, even inside DOMPurify's inert
172993
+ // DOMParser document. Hoisting its children on removal re-inserts a fresh
172994
+ // mirror target ahead of the walk, which the engine refills, looping
172995
+ // forever (DoS) and amplifying output. Dropping its content on removal
172996
+ // (rather than hoisting) breaks that cascade; the content is a duplicate
172997
+ // of the option, which is sanitized on its own. See campaign-3 F1/F6.
172998
+ 'selectedcontent', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
172747
172999
  /* Tags that are safe for data: URIs */
172748
173000
  let DATA_URI_TAGS = null;
172749
173001
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
@@ -172759,8 +173011,10 @@ function createDOMPurify() {
172759
173011
  /* Allowed XHTML+XML namespaces */
172760
173012
  let ALLOWED_NAMESPACES = null;
172761
173013
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
172762
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
172763
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
173014
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
173015
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
173016
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
173017
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
172764
173018
  // Certain elements are allowed in both SVG and HTML
172765
173019
  // namespace. We need to specify them explicitly
172766
173020
  // so that they don't get erroneously deleted from
@@ -172802,15 +173056,33 @@ function createDOMPurify() {
172802
173056
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
172803
173057
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
172804
173058
  /* Set configuration parameters */
172805
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
172806
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
172807
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
172808
- URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
172809
- DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
172810
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
172811
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
172812
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
172813
- USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
173059
+ ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
173060
+ transform: transformCaseFunc
173061
+ });
173062
+ ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
173063
+ transform: transformCaseFunc
173064
+ });
173065
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
173066
+ transform: stringToString
173067
+ });
173068
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
173069
+ transform: transformCaseFunc,
173070
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
173071
+ });
173072
+ DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
173073
+ transform: transformCaseFunc,
173074
+ base: DEFAULT_DATA_URI_TAGS
173075
+ });
173076
+ FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
173077
+ transform: transformCaseFunc
173078
+ });
173079
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
173080
+ transform: transformCaseFunc
173081
+ });
173082
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
173083
+ transform: transformCaseFunc
173084
+ });
173085
+ USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
172814
173086
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
172815
173087
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
172816
173088
  ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
@@ -172826,20 +173098,22 @@ function createDOMPurify() {
172826
173098
  SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
172827
173099
  KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
172828
173100
  IN_PLACE = cfg.IN_PLACE || false; // Default false
172829
- IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
172830
- NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
172831
- MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
172832
- HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
172833
- CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || create(null);
172834
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
172835
- CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
172836
- }
172837
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
172838
- CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
172839
- }
172840
- if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
172841
- CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
172842
- }
173101
+ IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
173102
+ NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
173103
+ MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
173104
+ HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
173105
+ const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
173106
+ CUSTOM_ELEMENT_HANDLING = create(null);
173107
+ if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
173108
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
173109
+ }
173110
+ if (objectHasOwnProperty(customElementHandling, 'attributeNameCheck') && isRegexOrFunction(customElementHandling.attributeNameCheck)) {
173111
+ CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; // Default undefined
173112
+ }
173113
+ if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
173114
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
173115
+ }
173116
+ seal(CUSTOM_ELEMENT_HANDLING);
172843
173117
  if (SAFE_FOR_TEMPLATES) {
172844
173118
  ALLOW_DATA_ATTR = false;
172845
173119
  }
@@ -172875,36 +173149,36 @@ function createDOMPurify() {
172875
173149
  EXTRA_ELEMENT_HANDLING.tagCheck = null;
172876
173150
  EXTRA_ELEMENT_HANDLING.attributeCheck = null;
172877
173151
  /* Merge configuration parameters */
172878
- if (cfg.ADD_TAGS) {
173152
+ if (objectHasOwnProperty(cfg, 'ADD_TAGS')) {
172879
173153
  if (typeof cfg.ADD_TAGS === 'function') {
172880
173154
  EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
172881
- } else {
173155
+ } else if (arrayIsArray(cfg.ADD_TAGS)) {
172882
173156
  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
172883
173157
  ALLOWED_TAGS = clone(ALLOWED_TAGS);
172884
173158
  }
172885
173159
  addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
172886
173160
  }
172887
173161
  }
172888
- if (cfg.ADD_ATTR) {
173162
+ if (objectHasOwnProperty(cfg, 'ADD_ATTR')) {
172889
173163
  if (typeof cfg.ADD_ATTR === 'function') {
172890
173164
  EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
172891
- } else {
173165
+ } else if (arrayIsArray(cfg.ADD_ATTR)) {
172892
173166
  if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
172893
173167
  ALLOWED_ATTR = clone(ALLOWED_ATTR);
172894
173168
  }
172895
173169
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
172896
173170
  }
172897
173171
  }
172898
- if (cfg.ADD_URI_SAFE_ATTR) {
173172
+ if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
172899
173173
  addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
172900
173174
  }
172901
- if (cfg.FORBID_CONTENTS) {
173175
+ if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
172902
173176
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
172903
173177
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
172904
173178
  }
172905
173179
  addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
172906
173180
  }
172907
- if (cfg.ADD_FORBID_CONTENTS) {
173181
+ if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
172908
173182
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
172909
173183
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
172910
173184
  }
@@ -172923,6 +173197,13 @@ function createDOMPurify() {
172923
173197
  addToSet(ALLOWED_TAGS, ['tbody']);
172924
173198
  delete FORBID_TAGS.tbody;
172925
173199
  }
173200
+ // Re-derive the active Trusted Types policy from this configuration on
173201
+ // every parse. The active policy must never be sticky closure state that
173202
+ // outlives the config that set it: a caller-supplied policy left in place
173203
+ // after `clearConfig()` — or after a later call that supplied none, or
173204
+ // `TRUSTED_TYPES_POLICY: null` — could sign a subsequent "default"
173205
+ // `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy.
173206
+ // See GHSA-vxr8-fq34-vvx9.
172926
173207
  if (cfg.TRUSTED_TYPES_POLICY) {
172927
173208
  if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
172928
173209
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
@@ -172930,18 +173211,45 @@ function createDOMPurify() {
172930
173211
  if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
172931
173212
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
172932
173213
  }
172933
- // Overwrite existing TrustedTypes policy.
173214
+ // A caller-supplied policy applies to this configuration only.
173215
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
172934
173216
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
172935
- // Sign local variables required by `sanitize`.
172936
- emptyHTML = trustedTypesPolicy.createHTML('');
173217
+ // Sign local variables required by `sanitize`. If the supplied policy's
173218
+ // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this
173219
+ // throws via the re-entrancy guard. Restore the previous policy first so
173220
+ // the instance is not left in a poisoned state. See #1422.
173221
+ try {
173222
+ emptyHTML = _createTrustedHTML('');
173223
+ } catch (error) {
173224
+ trustedTypesPolicy = previousTrustedTypesPolicy;
173225
+ throw error;
173226
+ }
173227
+ } else if (cfg.TRUSTED_TYPES_POLICY === null) {
173228
+ // Explicit opt-out for this call: perform no Trusted Types signing and
173229
+ // create nothing (so a strict `trusted-types` CSP that disallows a
173230
+ // `dompurify` policy can still call `sanitize` from inside its own
173231
+ // policy — see #1422). Resetting to `undefined` rather than a sticky
173232
+ // `null` also drops any previously retained caller policy, so it cannot
173233
+ // resurface on a later call, while still allowing the next config-less
173234
+ // call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9.
173235
+ trustedTypesPolicy = undefined;
173236
+ emptyHTML = '';
172937
173237
  } else {
172938
- // Uninitialized policy, attempt to initialize the internal dompurify policy.
173238
+ // No policy supplied: keep the currently active policy if one is set — a
173239
+ // previously supplied policy is intentionally sticky across config-less
173240
+ // calls — otherwise fall back to the instance's own internal policy,
173241
+ // created at most once. (A policy supplied for a *single* call still
173242
+ // lingers by design; what must not linger is a policy whose configuration
173243
+ // has been torn down via `clearConfig()`, which restores the default.)
172939
173244
  if (trustedTypesPolicy === undefined) {
172940
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
173245
+ trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
172941
173246
  }
172942
- // If creating the internal policy succeeded sign internal variables.
172943
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
172944
- emptyHTML = trustedTypesPolicy.createHTML('');
173247
+ // Sign internal variables only when a policy is active. A falsy policy
173248
+ // (Trusted Types unsupported, creation failed, or an explicit opt-out)
173249
+ // leaves `emptyHTML` as a plain string, so we never call `.createHTML` on
173250
+ // a non-policy and throw. See #1422.
173251
+ if (trustedTypesPolicy && typeof emptyHTML === 'string') {
173252
+ emptyHTML = _createTrustedHTML('');
172945
173253
  }
172946
173254
  }
172947
173255
  // Prevent further manipulation of configuration.
@@ -172956,6 +173264,77 @@ function createDOMPurify() {
172956
173264
  * correctly. */
172957
173265
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
172958
173266
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
173267
+ /**
173268
+ * Namespace rules for an element in the SVG namespace.
173269
+ *
173270
+ * @param tagName the element's lowercase tag name
173271
+ * @param parent the (possibly simulated) parent node
173272
+ * @param parentTagName the parent's lowercase tag name
173273
+ * @returns true if a spec-compliant parser could produce this element
173274
+ */
173275
+ const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
173276
+ // The only way to switch from HTML namespace to SVG
173277
+ // is via <svg>. If it happens via any other tag, then
173278
+ // it should be killed.
173279
+ if (parent.namespaceURI === HTML_NAMESPACE) {
173280
+ return tagName === 'svg';
173281
+ }
173282
+ // The only way to switch from MathML to SVG is via <svg>
173283
+ // if the parent is either <annotation-xml> or a MathML
173284
+ // text integration point.
173285
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
173286
+ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
173287
+ }
173288
+ // We only allow elements that are defined in SVG
173289
+ // spec. All others are disallowed in SVG namespace.
173290
+ return Boolean(ALL_SVG_TAGS[tagName]);
173291
+ };
173292
+ /**
173293
+ * Namespace rules for an element in the MathML namespace.
173294
+ *
173295
+ * @param tagName the element's lowercase tag name
173296
+ * @param parent the (possibly simulated) parent node
173297
+ * @param parentTagName the parent's lowercase tag name
173298
+ * @returns true if a spec-compliant parser could produce this element
173299
+ */
173300
+ const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
173301
+ // The only way to switch from HTML namespace to MathML
173302
+ // is via <math>. If it happens via any other tag, then
173303
+ // it should be killed.
173304
+ if (parent.namespaceURI === HTML_NAMESPACE) {
173305
+ return tagName === 'math';
173306
+ }
173307
+ // The only way to switch from SVG to MathML is via
173308
+ // <math> and HTML integration points
173309
+ if (parent.namespaceURI === SVG_NAMESPACE) {
173310
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
173311
+ }
173312
+ // We only allow elements that are defined in MathML
173313
+ // spec. All others are disallowed in MathML namespace.
173314
+ return Boolean(ALL_MATHML_TAGS[tagName]);
173315
+ };
173316
+ /**
173317
+ * Namespace rules for an element in the HTML namespace.
173318
+ *
173319
+ * @param tagName the element's lowercase tag name
173320
+ * @param parent the (possibly simulated) parent node
173321
+ * @param parentTagName the parent's lowercase tag name
173322
+ * @returns true if a spec-compliant parser could produce this element
173323
+ */
173324
+ const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
173325
+ // The only way to switch from SVG to HTML is via
173326
+ // HTML integration points, and from MathML to HTML
173327
+ // is via MathML text integration points
173328
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
173329
+ return false;
173330
+ }
173331
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
173332
+ return false;
173333
+ }
173334
+ // We disallow tags that are specific for MathML
173335
+ // or SVG and should never appear in HTML namespace
173336
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
173337
+ };
172959
173338
  /**
172960
173339
  * @param element a DOM element whose namespace is being checked
172961
173340
  * @returns Return false if the element has a
@@ -172978,51 +173357,13 @@ function createDOMPurify() {
172978
173357
  return false;
172979
173358
  }
172980
173359
  if (element.namespaceURI === SVG_NAMESPACE) {
172981
- // The only way to switch from HTML namespace to SVG
172982
- // is via <svg>. If it happens via any other tag, then
172983
- // it should be killed.
172984
- if (parent.namespaceURI === HTML_NAMESPACE) {
172985
- return tagName === 'svg';
172986
- }
172987
- // The only way to switch from MathML to SVG is via`
172988
- // svg if parent is either <annotation-xml> or MathML
172989
- // text integration points.
172990
- if (parent.namespaceURI === MATHML_NAMESPACE) {
172991
- return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
172992
- }
172993
- // We only allow elements that are defined in SVG
172994
- // spec. All others are disallowed in SVG namespace.
172995
- return Boolean(ALL_SVG_TAGS[tagName]);
173360
+ return _checkSvgNamespace(tagName, parent, parentTagName);
172996
173361
  }
172997
173362
  if (element.namespaceURI === MATHML_NAMESPACE) {
172998
- // The only way to switch from HTML namespace to MathML
172999
- // is via <math>. If it happens via any other tag, then
173000
- // it should be killed.
173001
- if (parent.namespaceURI === HTML_NAMESPACE) {
173002
- return tagName === 'math';
173003
- }
173004
- // The only way to switch from SVG to MathML is via
173005
- // <math> and HTML integration points
173006
- if (parent.namespaceURI === SVG_NAMESPACE) {
173007
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
173008
- }
173009
- // We only allow elements that are defined in MathML
173010
- // spec. All others are disallowed in MathML namespace.
173011
- return Boolean(ALL_MATHML_TAGS[tagName]);
173363
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
173012
173364
  }
173013
173365
  if (element.namespaceURI === HTML_NAMESPACE) {
173014
- // The only way to switch from SVG to HTML is via
173015
- // HTML integration points, and from MathML to HTML
173016
- // is via MathML text integration points
173017
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
173018
- return false;
173019
- }
173020
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
173021
- return false;
173022
- }
173023
- // We disallow tags that are specific for MathML
173024
- // or SVG and should never appear in HTML namespace
173025
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
173366
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
173026
173367
  }
173027
173368
  // For XHTML and XML documents that support custom namespaces
173028
173369
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -173047,7 +173388,74 @@ function createDOMPurify() {
173047
173388
  // eslint-disable-next-line unicorn/prefer-dom-node-remove
173048
173389
  getParentNode(node).removeChild(node);
173049
173390
  } catch (_) {
173391
+ /* The normal detach failed — this is reached for a parentless node
173392
+ (getParentNode() is null, so .removeChild throws). Element.prototype
173393
+ .remove() is itself a spec no-op on a parentless node, so a recorded
173394
+ "removal" would otherwise hand the caller back an intact,
173395
+ payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or
173396
+ the style-with-element-child rule decided to kill). Fail closed by
173397
+ throwing — exactly as a clobbered root does at the IN_PLACE entry —
173398
+ rather than trying to "neutralize" the node via its own methods.
173399
+ Neutralizing would mean calling getAttributeNames()/removeAttribute()
173400
+ on the node, both of which a <form> root can clobber via a named child
173401
+ (and _isClobbered does not even probe getAttributeNames), so the
173402
+ neutralize step could itself be silently defeated, leaving the payload
173403
+ intact. A throw touches only the cached, clobber-safe remove() and
173404
+ getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root)
173405
+ to every root-kill reason. REPORT-3.
173406
+ This lives inside the catch, so it never fires for a normally-removed
173407
+ in-tree node: those have a parent, removeChild() succeeds, and the
173408
+ catch is not entered. Only a kept (parentless) root reaches here. */
173050
173409
  remove(node);
173410
+ if (!getParentNode(node)) {
173411
+ throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place');
173412
+ }
173413
+ }
173414
+ };
173415
+ /**
173416
+ * _neutralizeRoot
173417
+ *
173418
+ * Fail-closed teardown of an in-place root after the sanitize walk aborts
173419
+ * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
173420
+ * custom element's reaction detaches a node so `_forceRemove`'s deliberate
173421
+ * parentless guard throws, or any other re-entrant engine mutation — would
173422
+ * otherwise leave the caller's *live* tree half-sanitized, with everything
173423
+ * after the abort point still carrying its handlers. There is no safe way
173424
+ * to resume the walk (the tree mutated under us), so we strip the root bare:
173425
+ * remove every child and every attribute, then let the caller's catch see
173426
+ * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
173427
+ * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
173428
+ *
173429
+ * @param root the in-place root to empty
173430
+ */
173431
+ const _neutralizeRoot = function _neutralizeRoot(root) {
173432
+ const childNodes = getChildNodes(root);
173433
+ if (childNodes) {
173434
+ const snapshot = [];
173435
+ arrayForEach(childNodes, child => {
173436
+ arrayPush(snapshot, child);
173437
+ });
173438
+ arrayForEach(snapshot, child => {
173439
+ try {
173440
+ remove(child);
173441
+ } catch (_) {
173442
+ /* Best-effort teardown; a still-attached child is handled below */
173443
+ }
173444
+ });
173445
+ }
173446
+ const attributes = getAttributes(root);
173447
+ if (attributes) {
173448
+ for (let i = attributes.length - 1; i >= 0; --i) {
173449
+ const attribute = attributes[i];
173450
+ const name = attribute && attribute.name;
173451
+ if (typeof name === 'string') {
173452
+ try {
173453
+ root.removeAttribute(name);
173454
+ } catch (_) {
173455
+ /* Clobbered removeAttribute — ignore (fail-closed best effort) */
173456
+ }
173457
+ }
173458
+ }
173051
173459
  }
173052
173460
  };
173053
173461
  /**
@@ -173082,6 +173490,72 @@ function createDOMPurify() {
173082
173490
  }
173083
173491
  }
173084
173492
  };
173493
+ /**
173494
+ * _stripDisallowedAttributes
173495
+ *
173496
+ * Removes every attribute the active configuration does not allow from a
173497
+ * single element, using the same allowlist as the main attribute pass (so
173498
+ * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
173499
+ * neutralise nodes that are being discarded from an in-place tree.
173500
+ *
173501
+ * @param element the element to strip
173502
+ */
173503
+ const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
173504
+ const attributes = getAttributes(element);
173505
+ if (!attributes) {
173506
+ return;
173507
+ }
173508
+ for (let i = attributes.length - 1; i >= 0; --i) {
173509
+ const attribute = attributes[i];
173510
+ const name = attribute && attribute.name;
173511
+ if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
173512
+ continue;
173513
+ }
173514
+ try {
173515
+ element.removeAttribute(name);
173516
+ } catch (_) {
173517
+ /* Clobbered removeAttribute on a doomed node — ignore */
173518
+ }
173519
+ }
173520
+ };
173521
+ /**
173522
+ * _neutralizeSubtree
173523
+ *
173524
+ * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
173525
+ * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
173526
+ * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
173527
+ * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
173528
+ * those dropped nodes are detached from the caller's LIVE tree but a
173529
+ * handler-bearing original among them (an `<img onerror>`/`<video>` that was
173530
+ * loading) keeps its queued resource event, which fires in page scope after
173531
+ * sanitize returns. This walks a removed subtree and strips every attribute
173532
+ * the active configuration does not allow — so `on*` handlers are cancelled
173533
+ * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
173534
+ * blocklist. Run synchronously before sanitize returns, i.e. before any
173535
+ * queued event can fire. Hook-free by design: these nodes leave the output,
173536
+ * so firing attribute hooks for them would be surprising. Clobber-safe reads;
173537
+ * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
173538
+ * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
173539
+ * the `<img>`, are reached and scrubbed).
173540
+ *
173541
+ * @param root the root of a removed subtree to neutralise
173542
+ */
173543
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
173544
+ const stack = [root];
173545
+ while (stack.length > 0) {
173546
+ const node = stack.pop();
173547
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
173548
+ if (nodeType === NODE_TYPE.element) {
173549
+ _stripDisallowedAttributes(node);
173550
+ }
173551
+ const childNodes = getChildNodes(node);
173552
+ if (childNodes) {
173553
+ for (let i = childNodes.length - 1; i >= 0; --i) {
173554
+ stack.push(childNodes[i]);
173555
+ }
173556
+ }
173557
+ }
173558
+ };
173085
173559
  /**
173086
173560
  * _initDocument
173087
173561
  *
@@ -173103,7 +173577,7 @@ function createDOMPurify() {
173103
173577
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
173104
173578
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
173105
173579
  }
173106
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
173580
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
173107
173581
  /*
173108
173582
  * Use the DOMParser API by default, fallback later if needs be
173109
173583
  * DOMParser not work for svg when has multiple root element.
@@ -173143,29 +173617,254 @@ function createDOMPurify() {
173143
173617
  // eslint-disable-next-line no-bitwise
173144
173618
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
173145
173619
  };
173620
+ /**
173621
+ * Replace template expression syntax (mustache, ERB, template
173622
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
173623
+ * sites. Order matters: mustache, then ERB, then template literal.
173624
+ *
173625
+ * @param value the string to scrub
173626
+ * @returns the scrubbed string
173627
+ */
173628
+ const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
173629
+ value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
173630
+ value = stringReplace(value, ERB_EXPR$1, ' ');
173631
+ value = stringReplace(value, TMPLIT_EXPR$1, ' ');
173632
+ return value;
173633
+ };
173634
+ /**
173635
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
173636
+ * character data of an element subtree. Used as the final safety net for
173637
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
173638
+ * which only form after text-node normalization (e.g. fragments split across
173639
+ * stripped elements) cannot survive into a template-evaluating framework.
173640
+ *
173641
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
173642
+ * in place rather than round-tripping through innerHTML. This preserves
173643
+ * descendant node references (important for IN_PLACE callers), avoids a
173644
+ * serialize/reparse cycle, and reads literal character data — which means
173645
+ * `<%...%>` in text content matches the ERB regex against its real bytes
173646
+ * instead of the HTML-entity-escaped form innerHTML would produce.
173647
+ *
173648
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
173649
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
173650
+ *
173651
+ * @param node The root element whose character data should be scrubbed.
173652
+ */
173653
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
173654
+ var _node$querySelectorAl;
173655
+ node.normalize();
173656
+ const walker = createNodeIterator.call(node.ownerDocument || node, node,
173657
+ // eslint-disable-next-line no-bitwise
173658
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
173659
+ let currentNode = walker.nextNode();
173660
+ while (currentNode) {
173661
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
173662
+ currentNode = walker.nextNode();
173663
+ }
173664
+ // NodeIterator does not descend into <template>.content per the DOM spec,
173665
+ // so we must explicitly recurse into each template's content fragment,
173666
+ // mirroring the approach used by _sanitizeShadowDOM.
173667
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
173668
+ if (templates) {
173669
+ arrayForEach(templates, tmpl => {
173670
+ if (_isDocumentFragment(tmpl.content)) {
173671
+ _scrubTemplateExpressions2(tmpl.content);
173672
+ }
173673
+ });
173674
+ }
173675
+ };
173146
173676
  /**
173147
173677
  * _isClobbered
173148
173678
  *
173679
+ * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
173680
+ * interface with [LegacyOverrideBuiltIns]; a descendant element with a
173681
+ * `name` attribute matching a prototype property shadows that property
173682
+ * on direct reads. We use this check at the IN_PLACE entry-point and
173683
+ * during attribute sanitization to refuse clobbered forms.
173684
+ *
173149
173685
  * @param element element to check for clobbering attacks
173150
173686
  * @return true if clobbered, false if safe
173151
173687
  */
173152
173688
  const _isClobbered = function _isClobbered(element) {
173153
- return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function');
173689
+ // Realm-independent tag-name probe. If we can't determine the tag
173690
+ // name at all, we can't reason about clobbering — return false
173691
+ // (the caller's other defences still apply).
173692
+ const realTagName = getNodeName ? getNodeName(element) : null;
173693
+ if (typeof realTagName !== 'string') {
173694
+ return false;
173695
+ }
173696
+ if (transformCaseFunc(realTagName) !== 'form') {
173697
+ return false;
173698
+ }
173699
+ return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' ||
173700
+ // Realm-safe NamedNodeMap detection: equality against the cached
173701
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
173702
+ // makes the direct read diverge from the cached read; a clean form
173703
+ // (same-realm OR foreign-realm) has both reads pointing at the same
173704
+ // canonical NamedNodeMap.
173705
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
173706
+ // NodeType clobbering probe. Cached Node.prototype.nodeType getter
173707
+ // returns the integer 1 for any Element regardless of realm; direct
173708
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
173709
+ // the named child element. Cheap addition — nodeType is read from
173710
+ // an internal slot, no serialization cost — and removes a residual
173711
+ // clobbering surface used by several mXSS / PI / comment branches
173712
+ // in _sanitizeElements that compare currentNode.nodeType directly.
173713
+ element.nodeType !== getNodeType(element) ||
173714
+ // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
173715
+ // "childNodes" shadows the prototype getter. Direct reads of
173716
+ // form.childNodes from a clobbered form return the named child
173717
+ // instead of the real NodeList, so any walk that reads it directly
173718
+ // skips the form's real children. Compare the direct read to the
173719
+ // cached Node.prototype getter — when the form's named-property
173720
+ // getter intercepts the read, the two values differ and we flag
173721
+ // the form. This catches every clobbering child type (input,
173722
+ // select, etc.) regardless of whether the named child happens to
173723
+ // carry a numeric .length, which a typeof-based probe would miss
173724
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
173725
+ element.childNodes !== getChildNodes(element);
173726
+ };
173727
+ /**
173728
+ * Checks whether the given value is a DocumentFragment from any realm.
173729
+ *
173730
+ * The realm-independent replacement reads `nodeType` through the cached
173731
+ * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
173732
+ * constant (11). nodeType is a numeric value resolved from the node's
173733
+ * internal slot, identical across realms for the same kind of node.
173734
+ *
173735
+ * @param value object to check
173736
+ * @return true if value is a DocumentFragment-shaped node from any realm
173737
+ */
173738
+ const _isDocumentFragment = function _isDocumentFragment(value) {
173739
+ if (!getNodeType || typeof value !== 'object' || value === null) {
173740
+ return false;
173741
+ }
173742
+ try {
173743
+ return getNodeType(value) === NODE_TYPE.documentFragment;
173744
+ } catch (_) {
173745
+ return false;
173746
+ }
173154
173747
  };
173155
173748
  /**
173156
- * Checks whether the given object is a DOM node.
173749
+ * Checks whether the given object is a DOM node, including nodes that
173750
+ * originate from a different window/realm (e.g. an iframe's
173751
+ * contentDocument). The previous `value instanceof Node` check was
173752
+ * realm-bound: nodes from a different window failed it, causing
173753
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
173754
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
173157
173755
  *
173158
173756
  * @param value object to check whether it's a DOM node
173159
- * @return true is object is a DOM node
173757
+ * @return true if value is a DOM node from any realm
173160
173758
  */
173161
173759
  const _isNode = function _isNode(value) {
173162
- return typeof Node === 'function' && value instanceof Node;
173760
+ if (!getNodeType || typeof value !== 'object' || value === null) {
173761
+ return false;
173762
+ }
173763
+ try {
173764
+ return typeof getNodeType(value) === 'number';
173765
+ } catch (_) {
173766
+ return false;
173767
+ }
173163
173768
  };
173164
173769
  function _executeHooks(hooks, currentNode, data) {
173770
+ if (hooks.length === 0) {
173771
+ return;
173772
+ }
173165
173773
  arrayForEach(hooks, hook => {
173166
173774
  hook.call(DOMPurify, currentNode, data, CONFIG);
173167
173775
  });
173168
173776
  }
173777
+ /**
173778
+ * Structural-threat checks that condemn a node regardless of the
173779
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
173780
+ * processing instructions, markup-bearing comments. Pure predicate;
173781
+ * the caller removes. Check order is load-bearing.
173782
+ *
173783
+ * @param currentNode the node to inspect
173784
+ * @param tagName the node's transformCaseFunc'd tag name
173785
+ * @return true if the node must be removed
173786
+ */
173787
+ const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
173788
+ /* Detect mXSS attempts abusing namespace confusion */
173789
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
173790
+ return true;
173791
+ }
173792
+ /* Remove risky CSS construction leading to mXSS */
173793
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
173794
+ return true;
173795
+ }
173796
+ /* Remove any occurrence of processing instructions */
173797
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
173798
+ return true;
173799
+ }
173800
+ /* Remove any kind of possibly harmful comments */
173801
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
173802
+ return true;
173803
+ }
173804
+ return false;
173805
+ };
173806
+ /**
173807
+ * Handle a node whose tag is forbidden or not allowlisted: keep
173808
+ * allowed custom elements (false return exits _sanitizeElements
173809
+ * early - namespace/fallback checks and the afterSanitizeElements
173810
+ * hook are intentionally skipped for kept custom elements), else
173811
+ * hoist content per KEEP_CONTENT and remove.
173812
+ *
173813
+ * @param currentNode the disallowed node
173814
+ * @param tagName the node's transformCaseFunc'd tag name
173815
+ * @return true if the node was removed, false if kept
173816
+ */
173817
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
173818
+ /* Check if we have a custom element to handle */
173819
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
173820
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
173821
+ return false;
173822
+ }
173823
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
173824
+ return false;
173825
+ }
173826
+ }
173827
+ /* Keep content except for bad-listed elements.
173828
+ Use the cached prototype getters exclusively — the previous code
173829
+ had `|| currentNode.parentNode` / `|| currentNode.childNodes`
173830
+ fallbacks, but the cached getters always return the canonical
173831
+ value (or null for a real parent-less node), so the fallback
173832
+ path was dead in safe cases and a clobbering surface in unsafe
173833
+ ones. Falsy cached results stay falsy; the `if (childNodes &&
173834
+ parentNode)` check already gates correctly. */
173835
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
173836
+ const parentNode = getParentNode(currentNode);
173837
+ const childNodes = getChildNodes(currentNode);
173838
+ if (childNodes && parentNode) {
173839
+ const childCount = childNodes.length;
173840
+ /* In-place: hoist the *original* children so the iterator visits
173841
+ and sanitises them through the same allowlist pass as every other
173842
+ node. The caller built the tree in the live document, so the
173843
+ originals carry already-queued resource events (`<img onerror>`,
173844
+ `<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
173845
+ those originals detached but still armed, firing in page scope
173846
+ while the returned tree looked clean. Moving is safe in-place: the
173847
+ root is pre-validated as an allowed tag and so is never the node
173848
+ being removed, which keeps `parentNode` inside the iterator root
173849
+ and the relocated child inside the serialised tree.
173850
+ Otherwise (string / DOM-copy paths): clone. The iterator is rooted
173851
+ at — and the result serialised from — `body`, so a restrictive
173852
+ ALLOWED_TAGS that removes `body` itself must leave its content in
173853
+ place, which only cloning does; and those paths parse into an
173854
+ inert document, so their discarded originals never had a queued
173855
+ event to neutralise.
173856
+ `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
173857
+ valid whether we move (drops the trailing entry) or clone (leaves
173858
+ the list intact). */
173859
+ for (let i = childCount - 1; i >= 0; --i) {
173860
+ const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
173861
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
173862
+ }
173863
+ }
173864
+ }
173865
+ _forceRemove(currentNode);
173866
+ return true;
173867
+ };
173169
173868
  /**
173170
173869
  * _sanitizeElements
173171
173870
  *
@@ -173176,7 +173875,6 @@ function createDOMPurify() {
173176
173875
  * @return true if node was killed, false if left alive
173177
173876
  */
173178
173877
  const _sanitizeElements = function _sanitizeElements(currentNode) {
173179
- let content = null;
173180
173878
  /* Execute a hook if present */
173181
173879
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
173182
173880
  /* Check if element is clobbered or can clobber */
@@ -173185,76 +173883,41 @@ function createDOMPurify() {
173185
173883
  return true;
173186
173884
  }
173187
173885
  /* Now let's check the element's type and name */
173188
- const tagName = transformCaseFunc(currentNode.nodeName);
173886
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
173189
173887
  /* Execute a hook if present */
173190
173888
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
173191
173889
  tagName,
173192
173890
  allowedTags: ALLOWED_TAGS
173193
173891
  });
173194
- /* Detect mXSS attempts abusing namespace confusion */
173195
- if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
173196
- _forceRemove(currentNode);
173197
- return true;
173198
- }
173199
- /* Remove risky CSS construction leading to mXSS */
173200
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
173201
- _forceRemove(currentNode);
173202
- return true;
173203
- }
173204
- /* Remove any occurrence of processing instructions */
173205
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
173206
- _forceRemove(currentNode);
173207
- return true;
173208
- }
173209
- /* Remove any kind of possibly harmful comments */
173210
- if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
173892
+ /* Remove mXSS vectors, processing instructions and risky comments */
173893
+ if (_isUnsafeNode(currentNode, tagName)) {
173211
173894
  _forceRemove(currentNode);
173212
173895
  return true;
173213
173896
  }
173214
173897
  /* Remove element if anything forbids its presence */
173215
173898
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
173216
- /* Check if we have a custom element to handle */
173217
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
173218
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
173219
- return false;
173220
- }
173221
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
173222
- return false;
173223
- }
173224
- }
173225
- /* Keep content except for bad-listed elements */
173226
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
173227
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
173228
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
173229
- if (childNodes && parentNode) {
173230
- const childCount = childNodes.length;
173231
- for (let i = childCount - 1; i >= 0; --i) {
173232
- const childClone = cloneNode(childNodes[i], true);
173233
- childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
173234
- parentNode.insertBefore(childClone, getNextSibling(currentNode));
173235
- }
173236
- }
173237
- }
173238
- _forceRemove(currentNode);
173239
- return true;
173240
- }
173241
- /* Check whether element has a valid namespace */
173242
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
173899
+ return _sanitizeDisallowedNode(currentNode, tagName);
173900
+ }
173901
+ /* Check whether element has a valid namespace.
173902
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
173903
+ nodeType getter rather than `instanceof Element`, which is realm-
173904
+ bound and short-circuits to false for any node minted in a different
173905
+ realm — letting a foreign-realm element with a forbidden namespace
173906
+ slip past the namespace check entirely. */
173907
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
173908
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
173243
173909
  _forceRemove(currentNode);
173244
173910
  return true;
173245
173911
  }
173246
173912
  /* Make sure that older browsers don't get fallback-tag mXSS */
173247
- if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
173913
+ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
173248
173914
  _forceRemove(currentNode);
173249
173915
  return true;
173250
173916
  }
173251
173917
  /* Sanitize element content to be template-safe */
173252
173918
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
173253
173919
  /* Get the element's text content */
173254
- content = currentNode.textContent;
173255
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
173256
- content = stringReplace(content, expr, ' ');
173257
- });
173920
+ const content = _stripTemplateExpressions(currentNode.textContent);
173258
173921
  if (currentNode.textContent !== content) {
173259
173922
  arrayPush(DOMPurify.removed, {
173260
173923
  element: currentNode.cloneNode()
@@ -173284,11 +173947,12 @@ function createDOMPurify() {
173284
173947
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
173285
173948
  return false;
173286
173949
  }
173950
+ const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
173287
173951
  /* Allow valid data-* attributes: At least one character after "-"
173288
173952
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
173289
173953
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
173290
173954
  We don't need to check the value; it's always URI safe. */
173291
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
173955
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
173292
173956
  if (
173293
173957
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
173294
173958
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -173300,11 +173964,15 @@ function createDOMPurify() {
173300
173964
  return false;
173301
173965
  }
173302
173966
  /* Check value is safe. First, is attr inert? If so, is safe */
173303
- } 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) {
173967
+ } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; 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$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if (value) {
173304
173968
  return false;
173305
173969
  } else ;
173306
173970
  return true;
173307
173971
  };
173972
+ /* Names the HTML spec reserves from valid-custom-element-name; these must
173973
+ * never be treated as basic custom elements even when a permissive
173974
+ * CUSTOM_ELEMENT_HANDLING.tagNameCheck is configured. */
173975
+ const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, ['annotation-xml', 'color-profile', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'missing-glyph']);
173308
173976
  /**
173309
173977
  * _isBasicCustomElement
173310
173978
  * checks if at least one dash is included in tagName, and it's not the first char
@@ -173314,7 +173982,64 @@ function createDOMPurify() {
173314
173982
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
173315
173983
  */
173316
173984
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
173317
- return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
173985
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
173986
+ };
173987
+ /**
173988
+ * Wrap an attribute value in the matching Trusted Types object when
173989
+ * the active policy requires it. Namespaced attributes pass through
173990
+ * unchanged (no TT support yet, see
173991
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
173992
+ *
173993
+ * @param lcTag lowercase tag name of the containing element
173994
+ * @param lcName lowercase attribute name
173995
+ * @param namespaceURI the attribute's namespace, if any
173996
+ * @param value the attribute value to wrap
173997
+ * @return the value, wrapped when Trusted Types demand it
173998
+ */
173999
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
174000
+ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
174001
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
174002
+ case 'TrustedHTML':
174003
+ {
174004
+ return _createTrustedHTML(value);
174005
+ }
174006
+ case 'TrustedScriptURL':
174007
+ {
174008
+ return _createTrustedScriptURL(value);
174009
+ }
174010
+ }
174011
+ }
174012
+ return value;
174013
+ };
174014
+ /**
174015
+ * Write a modified attribute value back onto the element. On
174016
+ * success, re-probe for clobbering introduced by the new value and
174017
+ * remove the element when found; otherwise pop the removal entry
174018
+ * recorded by the earlier _removeAttribute (long-standing pairing
174019
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
174020
+ * failure, remove the attribute instead.
174021
+ *
174022
+ * @param currentNode the element carrying the attribute
174023
+ * @param name the attribute name as present on the element
174024
+ * @param namespaceURI the attribute's namespace, if any
174025
+ * @param value the new attribute value
174026
+ */
174027
+ const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
174028
+ try {
174029
+ if (namespaceURI) {
174030
+ currentNode.setAttributeNS(namespaceURI, name, value);
174031
+ } else {
174032
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
174033
+ currentNode.setAttribute(name, value);
174034
+ }
174035
+ if (_isClobbered(currentNode)) {
174036
+ _forceRemove(currentNode);
174037
+ } else {
174038
+ arrayPop(DOMPurify.removed);
174039
+ }
174040
+ } catch (_) {
174041
+ _removeAttribute(name, currentNode);
174042
+ }
173318
174043
  };
173319
174044
  /**
173320
174045
  * _sanitizeAttributes
@@ -173329,9 +174054,7 @@ function createDOMPurify() {
173329
174054
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
173330
174055
  /* Execute a hook if present */
173331
174056
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
173332
- const {
173333
- attributes
173334
- } = currentNode;
174057
+ const attributes = currentNode.attributes;
173335
174058
  /* Check if we have attributes; if not we might have a text node */
173336
174059
  if (!attributes || _isClobbered(currentNode)) {
173337
174060
  return;
@@ -173344,14 +174067,13 @@ function createDOMPurify() {
173344
174067
  forceKeepAttr: undefined
173345
174068
  };
173346
174069
  let l = attributes.length;
174070
+ const lcTag = transformCaseFunc(currentNode.nodeName);
173347
174071
  /* Go backwards over all attributes; safely remove bad ones */
173348
174072
  while (l--) {
173349
174073
  const attr = attributes[l];
173350
- const {
173351
- name,
173352
- namespaceURI,
173353
- value: attrValue
173354
- } = attr;
174074
+ const name = attr.name,
174075
+ namespaceURI = attr.namespaceURI,
174076
+ attrValue = attr.value;
173355
174077
  const lcName = transformCaseFunc(name);
173356
174078
  const initValue = attrValue;
173357
174079
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -173365,12 +174087,14 @@ function createDOMPurify() {
173365
174087
  /* Full DOM Clobbering protection via namespace isolation,
173366
174088
  * Prefix id and name attributes with `user-content-`
173367
174089
  */
173368
- if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
174090
+ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
173369
174091
  // Remove the attribute with this value
173370
174092
  _removeAttribute(name, currentNode);
173371
174093
  // Prefix the value and later re-create the attribute with the sanitized value
173372
174094
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
173373
174095
  }
174096
+ // Else: already prefixed, leave the attribute alone — the prefix is
174097
+ // itself the clobbering protection, and re-applying it is incorrect.
173374
174098
  /* Work around a security issue with comments inside attributes */
173375
174099
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
173376
174100
  _removeAttribute(name, currentNode);
@@ -173381,7 +174105,7 @@ function createDOMPurify() {
173381
174105
  _removeAttribute(name, currentNode);
173382
174106
  continue;
173383
174107
  }
173384
- /* Did the hooks approve of the attribute? */
174108
+ /* Did the hooks force-keep the attribute? */
173385
174109
  if (hookEvent.forceKeepAttr) {
173386
174110
  continue;
173387
174111
  }
@@ -173391,56 +174115,24 @@ function createDOMPurify() {
173391
174115
  continue;
173392
174116
  }
173393
174117
  /* Work around a security issue in jQuery 3.0 */
173394
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
174118
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
173395
174119
  _removeAttribute(name, currentNode);
173396
174120
  continue;
173397
174121
  }
173398
174122
  /* Sanitize attribute content to be template-safe */
173399
174123
  if (SAFE_FOR_TEMPLATES) {
173400
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
173401
- value = stringReplace(value, expr, ' ');
173402
- });
174124
+ value = _stripTemplateExpressions(value);
173403
174125
  }
173404
174126
  /* Is `value` valid for this attribute? */
173405
- const lcTag = transformCaseFunc(currentNode.nodeName);
173406
174127
  if (!_isValidAttribute(lcTag, lcName, value)) {
173407
174128
  _removeAttribute(name, currentNode);
173408
174129
  continue;
173409
174130
  }
173410
174131
  /* Handle attributes that require Trusted Types */
173411
- if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
173412
- if (namespaceURI) ; else {
173413
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
173414
- case 'TrustedHTML':
173415
- {
173416
- value = trustedTypesPolicy.createHTML(value);
173417
- break;
173418
- }
173419
- case 'TrustedScriptURL':
173420
- {
173421
- value = trustedTypesPolicy.createScriptURL(value);
173422
- break;
173423
- }
173424
- }
173425
- }
173426
- }
174132
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
173427
174133
  /* Handle invalid data-* attribute set by try-catching it */
173428
174134
  if (value !== initValue) {
173429
- try {
173430
- if (namespaceURI) {
173431
- currentNode.setAttributeNS(namespaceURI, name, value);
173432
- } else {
173433
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
173434
- currentNode.setAttribute(name, value);
173435
- }
173436
- if (_isClobbered(currentNode)) {
173437
- _forceRemove(currentNode);
173438
- } else {
173439
- arrayPop(DOMPurify.removed);
173440
- }
173441
- } catch (_) {
173442
- _removeAttribute(name, currentNode);
173443
- }
174135
+ _setAttributeValue(currentNode, name, namespaceURI, value);
173444
174136
  }
173445
174137
  }
173446
174138
  /* Execute a hook if present */
@@ -173463,14 +174155,133 @@ function createDOMPurify() {
173463
174155
  _sanitizeElements(shadowNode);
173464
174156
  /* Check attributes next */
173465
174157
  _sanitizeAttributes(shadowNode);
173466
- /* Deep shadow DOM detected */
173467
- if (shadowNode.content instanceof DocumentFragment) {
174158
+ /* Deep shadow DOM detected.
174159
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
174160
+ DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we
174161
+ recurse into <template>.content from foreign realms too. */
174162
+ if (_isDocumentFragment(shadowNode.content)) {
173468
174163
  _sanitizeShadowDOM2(shadowNode.content);
173469
174164
  }
174165
+ /* An element iterated here may itself host an attached
174166
+ shadow root. The default NodeIterator does not enter shadow
174167
+ trees, so a shadow root nested inside template.content was
174168
+ previously reached by no walk at all (the pre-pass at
174169
+ _sanitizeAttachedShadowRoots descends via childNodes, which
174170
+ doesn't enter template.content; the template-content recursion
174171
+ above iterates the content but never inspected shadowRoot).
174172
+ Walk it explicitly. The nodeType guard avoids reading
174173
+ shadowRoot off text / comment / CDATA / PI nodes that the
174174
+ iterator also surfaces. */
174175
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
174176
+ if (shadowNodeType === NODE_TYPE.element) {
174177
+ const innerSr = getShadowRoot(shadowNode);
174178
+ if (_isDocumentFragment(innerSr)) {
174179
+ _sanitizeAttachedShadowRoots(innerSr);
174180
+ _sanitizeShadowDOM2(innerSr);
174181
+ }
174182
+ }
173470
174183
  }
173471
174184
  /* Execute a hook if present */
173472
174185
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
173473
174186
  };
174187
+ /**
174188
+ * _sanitizeAttachedShadowRoots
174189
+ *
174190
+ * Walks `root` and feeds every attached shadow root we encounter into
174191
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
174192
+ * does not descend into shadow trees, so nodes inside an attached
174193
+ * shadow root would otherwise be skipped entirely.
174194
+ *
174195
+ * Two real input paths put attached shadow roots in front of us:
174196
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
174197
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
174198
+ * shadow root because it was created with `clonable: true`.
174199
+ *
174200
+ * This pass runs once, up front, so the main iteration loop (and the
174201
+ * existing _sanitizeShadowDOM template-content recursion) stay
174202
+ * untouched — string-input paths are not affected.
174203
+ *
174204
+ * @param root the subtree root to walk for attached shadow roots
174205
+ */
174206
+ const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) {
174207
+ /* Iterative (explicit stack) rather than per-child recursion. DOM APIs
174208
+ impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data
174209
+ built straight into the DOM — the IN_PLACE surface) deeper than the JS
174210
+ call-stack budget would otherwise overflow native recursion here and
174211
+ throw at the IN_PLACE entry pre-pass, before a single node is
174212
+ sanitized, leaving the caller's live tree untouched (fail-open). See
174213
+ campaign-3 F4. A heap stack keeps depth off the call stack.
174214
+ Each work item is either a node to descend into, or a deferred
174215
+ `_sanitizeShadowDOM` for an already-walked shadow root. The deferred
174216
+ form preserves the original post-order discipline: a shadow root's
174217
+ nested shadow roots are discovered before the outer shadow is
174218
+ sanitized (which may remove hosts). Pushes are in reverse of the
174219
+ desired processing order (LIFO): template content, then children, then
174220
+ the shadow-sanitize, then the shadow walk — so the order matches the
174221
+ previous recursion exactly. */
174222
+ const stack = [{
174223
+ node: root,
174224
+ shadow: null
174225
+ }];
174226
+ while (stack.length > 0) {
174227
+ const item = stack.pop();
174228
+ /* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */
174229
+ if (item.shadow) {
174230
+ _sanitizeShadowDOM2(item.shadow);
174231
+ continue;
174232
+ }
174233
+ const node = item.node;
174234
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
174235
+ const isElement = nodeType === NODE_TYPE.element;
174236
+ /* (pushed last → processed first) Children, snapshotted in reverse so
174237
+ the first child is processed first. Snapshotting matters because a
174238
+ hook may detach siblings mid-walk. */
174239
+ const childNodes = getChildNodes(node);
174240
+ if (childNodes) {
174241
+ for (let i = childNodes.length - 1; i >= 0; --i) {
174242
+ stack.push({
174243
+ node: childNodes[i],
174244
+ shadow: null
174245
+ });
174246
+ }
174247
+ }
174248
+ /* (pushed before children → processed after them, matching the old
174249
+ "template content last" order) When the node is a <template>,
174250
+ descend into its content. */
174251
+ if (isElement) {
174252
+ const rootName = getNodeName ? getNodeName(node) : null;
174253
+ if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') {
174254
+ const content = node.content;
174255
+ if (_isDocumentFragment(content)) {
174256
+ stack.push({
174257
+ node: content,
174258
+ shadow: null
174259
+ });
174260
+ }
174261
+ }
174262
+ }
174263
+ /* Shadow root (processed first): walk its subtree, then sanitise it.
174264
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
174265
+ rather than `instanceof DocumentFragment`, which is realm-bound and
174266
+ silently skipped foreign-realm shadow roots (e.g.
174267
+ iframe.contentDocument attachShadow). */
174268
+ if (isElement) {
174269
+ const sr = getShadowRoot(node);
174270
+ if (_isDocumentFragment(sr)) {
174271
+ /* Push the deferred sanitise first so it pops after the shadow
174272
+ walk we push next, i.e. nested shadow roots are discovered
174273
+ before this one is sanitised. */
174274
+ stack.push({
174275
+ node: null,
174276
+ shadow: sr
174277
+ }, {
174278
+ node: sr,
174279
+ shadow: null
174280
+ });
174281
+ }
174282
+ }
174283
+ }
174284
+ };
173474
174285
  // eslint-disable-next-line complexity
173475
174286
  DOMPurify.sanitize = function (dirty) {
173476
174287
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -173487,13 +174298,9 @@ function createDOMPurify() {
173487
174298
  }
173488
174299
  /* Stringify, in case dirty is an object */
173489
174300
  if (typeof dirty !== 'string' && !_isNode(dirty)) {
173490
- if (typeof dirty.toString === 'function') {
173491
- dirty = dirty.toString();
173492
- if (typeof dirty !== 'string') {
173493
- throw typeErrorCreate('dirty is not a string, aborting');
173494
- }
173495
- } else {
173496
- throw typeErrorCreate('toString is not a function');
174301
+ dirty = stringifyValue(dirty);
174302
+ if (typeof dirty !== 'string') {
174303
+ throw typeErrorCreate('dirty is not a string, aborting');
173497
174304
  }
173498
174305
  }
173499
174306
  /* Return dirty HTML if DOMPurify cannot run */
@@ -173501,24 +174308,78 @@ function createDOMPurify() {
173501
174308
  return dirty;
173502
174309
  }
173503
174310
  /* Assign config vars */
173504
- if (!SET_CONFIG) {
174311
+ if (SET_CONFIG) {
174312
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
174313
+ * not re-derived per call. Restore them from the pristine bindings
174314
+ * captured at setConfig() time so a previous call's hook clone (mutated
174315
+ * below) does not carry over. */
174316
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
174317
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
174318
+ } else {
173505
174319
  _parseConfig(cfg);
173506
174320
  }
174321
+ /* Clone the hook-mutable allowlists before the walk whenever an
174322
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
174323
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
174324
+ * a hook that widens them would otherwise mutate the shared set
174325
+ * permanently: across later calls and across every element. Cloning per
174326
+ * walk keeps documented in-call widening working while scoping it to the
174327
+ * call. A single guard for both config paths - the per-call path rebinds
174328
+ * the sets in _parseConfig each call, the persistent path restores them
174329
+ * from the captured bindings just above - so the two cannot diverge. */
174330
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
174331
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
174332
+ }
174333
+ if (hooks.uponSanitizeAttribute.length > 0) {
174334
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
174335
+ }
173507
174336
  /* Clean up removed elements */
173508
174337
  DOMPurify.removed = [];
173509
- /* Check if dirty is correctly typed for IN_PLACE */
173510
- if (typeof dirty === 'string') {
173511
- IN_PLACE = false;
173512
- }
173513
- if (IN_PLACE) {
173514
- /* Do some early pre-sanitization to avoid unsafe root nodes */
173515
- if (dirty.nodeName) {
173516
- const tagName = transformCaseFunc(dirty.nodeName);
174338
+ /* Resolve IN_PLACE for this call without mutating persistent config.
174339
+ Writing the IN_PLACE closure variable here leaks under setConfig(),
174340
+ where _parseConfig is skipped on later calls: a single string call would
174341
+ disable in-place mode for every subsequent node call, returning a
174342
+ sanitized copy while leaving the caller's node — which in-place callers
174343
+ keep using and whose return value they ignore — unsanitized. REPORT-2. */
174344
+ const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
174345
+ if (inPlace) {
174346
+ /* Do some early pre-sanitization to avoid unsafe root nodes.
174347
+ Read nodeName through the cached prototype getter — a clobbering
174348
+ child named "nodeName" on the form root would otherwise shadow
174349
+ the property and let this check skip the root-allowlist
174350
+ validation entirely. */
174351
+ const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
174352
+ if (typeof nn === 'string') {
174353
+ const tagName = transformCaseFunc(nn);
173517
174354
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
173518
174355
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
173519
174356
  }
173520
174357
  }
173521
- } else if (dirty instanceof Node) {
174358
+ /* Pre-flight the root through _isClobbered. The iterator-driven
174359
+ removal path can not detach a parent-less root: _forceRemove
174360
+ falls through to Element.prototype.remove(), which per spec
174361
+ is a no-op on a node with no parent. A clobbered root would
174362
+ then survive the main loop with its attributes uninspected,
174363
+ because _sanitizeAttributes early-returns on _isClobbered. The
174364
+ result would be an attacker-controlled form, complete with any
174365
+ event-handler attributes the caller passed in, handed back to
174366
+ the application unsanitized. Refuse to sanitize such a root
174367
+ the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
174368
+ if (_isClobbered(dirty)) {
174369
+ throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
174370
+ }
174371
+ /* Sanitize attached shadow roots before the main iterator runs.
174372
+ The iterator does not descend into shadow trees. Same fail-closed
174373
+ barrier as the main walk (campaign-3 F2): a custom-element reaction
174374
+ inside a shadow root could abort this pre-pass before the walk runs,
174375
+ which would otherwise leave the entire live tree unsanitized. */
174376
+ try {
174377
+ _sanitizeAttachedShadowRoots(dirty);
174378
+ } catch (error) {
174379
+ _neutralizeRoot(dirty);
174380
+ throw error;
174381
+ }
174382
+ } else if (_isNode(dirty)) {
173522
174383
  /* If dirty is a DOM element, append to an empty document to avoid
173523
174384
  elements being stripped by the parser */
173524
174385
  body = _initDocument('<!---->');
@@ -173532,12 +174393,18 @@ function createDOMPurify() {
173532
174393
  // eslint-disable-next-line unicorn/prefer-dom-node-append
173533
174394
  body.appendChild(importedNode);
173534
174395
  }
174396
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
174397
+ them before the main iterator runs, since the iterator does not
174398
+ descend into shadow trees. The walk routes every read through a
174399
+ cached prototype getter so clobbering descendants on a form root
174400
+ cannot hide a shadow host from this pass. */
174401
+ _sanitizeAttachedShadowRoots(importedNode);
173535
174402
  } else {
173536
174403
  /* Exit directly if we have nothing to do */
173537
174404
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
173538
174405
  // eslint-disable-next-line unicorn/prefer-includes
173539
174406
  dirty.indexOf('<') === -1) {
173540
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
174407
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
173541
174408
  }
173542
174409
  /* Initialize the document to work on */
173543
174410
  body = _initDocument(dirty);
@@ -173551,31 +174418,59 @@ function createDOMPurify() {
173551
174418
  _forceRemove(body.firstChild);
173552
174419
  }
173553
174420
  /* Get node iterator */
173554
- const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
173555
- /* Now start iterating over the created document */
173556
- while (currentNode = nodeIterator.nextNode()) {
173557
- /* Sanitize tags and elements */
173558
- _sanitizeElements(currentNode);
173559
- /* Check attributes next */
173560
- _sanitizeAttributes(currentNode);
173561
- /* Shadow DOM detected, sanitize it */
173562
- if (currentNode.content instanceof DocumentFragment) {
173563
- _sanitizeShadowDOM2(currentNode.content);
174421
+ const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
174422
+ /* Now start iterating over the created document.
174423
+ The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
174424
+ engine/custom-element mutation can detach a node mid-walk so
174425
+ `_forceRemove`'s parentless guard throws, aborting the loop. Without the
174426
+ barrier the caller's in-place tree would be left half-sanitized with the
174427
+ unvisited tail still armed. On any throw we fail closed — strip the
174428
+ in-place root bare — then rethrow so the existing throw contract is
174429
+ preserved. (String/DOM-copy paths never return the partial body, so the
174430
+ propagating throw is already fail-closed there.) */
174431
+ try {
174432
+ while (currentNode = nodeIterator.nextNode()) {
174433
+ /* Sanitize tags and elements */
174434
+ _sanitizeElements(currentNode);
174435
+ /* Check attributes next */
174436
+ _sanitizeAttributes(currentNode);
174437
+ /* Shadow DOM detected, sanitize it.
174438
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
174439
+ instead of instanceof, so foreign-realm <template>.content is
174440
+ walked correctly. */
174441
+ if (_isDocumentFragment(currentNode.content)) {
174442
+ _sanitizeShadowDOM2(currentNode.content);
174443
+ }
174444
+ }
174445
+ } catch (error) {
174446
+ if (inPlace) {
174447
+ _neutralizeRoot(dirty);
173564
174448
  }
174449
+ throw error;
173565
174450
  }
173566
174451
  /* If we sanitized `dirty` in-place, return it. */
173567
- if (IN_PLACE) {
174452
+ if (inPlace) {
174453
+ /* Fail-closed completion of the audit-5 F1 fix: every node removed from
174454
+ the caller's live tree is detached but may still hold a queued
174455
+ resource-event handler that fires in page scope after we return. The
174456
+ move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the
174457
+ non-allow-listed attributes off every other removed subtree (clobber,
174458
+ mXSS, namespace, comments, KEEP_CONTENT:false, …) so those handlers are
174459
+ cancelled before any event can fire. Runs synchronously, pre-return. */
174460
+ arrayForEach(DOMPurify.removed, entry => {
174461
+ if (entry.element) {
174462
+ _neutralizeSubtree(entry.element);
174463
+ }
174464
+ });
174465
+ if (SAFE_FOR_TEMPLATES) {
174466
+ _scrubTemplateExpressions2(dirty);
174467
+ }
173568
174468
  return dirty;
173569
174469
  }
173570
174470
  /* Return sanitized string or DOM */
173571
174471
  if (RETURN_DOM) {
173572
174472
  if (SAFE_FOR_TEMPLATES) {
173573
- body.normalize();
173574
- let html = body.innerHTML;
173575
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
173576
- html = stringReplace(html, expr, ' ');
173577
- });
173578
- body.innerHTML = html;
174473
+ _scrubTemplateExpressions2(body);
173579
174474
  }
173580
174475
  if (RETURN_DOM_FRAGMENT) {
173581
174476
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -173605,20 +174500,28 @@ function createDOMPurify() {
173605
174500
  }
173606
174501
  /* Sanitize final string template-safe */
173607
174502
  if (SAFE_FOR_TEMPLATES) {
173608
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
173609
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
173610
- });
174503
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
173611
174504
  }
173612
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
174505
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
173613
174506
  };
173614
174507
  DOMPurify.setConfig = function () {
173615
174508
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
173616
174509
  _parseConfig(cfg);
173617
174510
  SET_CONFIG = true;
174511
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
174512
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
173618
174513
  };
173619
174514
  DOMPurify.clearConfig = function () {
173620
174515
  CONFIG = null;
173621
174516
  SET_CONFIG = false;
174517
+ SET_CONFIG_ALLOWED_TAGS = null;
174518
+ SET_CONFIG_ALLOWED_ATTR = null;
174519
+ // Drop any caller-supplied Trusted Types policy so it cannot poison later
174520
+ // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
174521
+ // never recreated — Trusted Types throws on duplicate names) is restored by
174522
+ // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9.
174523
+ trustedTypesPolicy = defaultTrustedTypesPolicy;
174524
+ emptyHTML = '';
173622
174525
  };
173623
174526
  DOMPurify.isValidAttribute = function (tag, attr, value) {
173624
174527
  /* Initialize shared config vars if necessary. */
@@ -173633,9 +174536,19 @@ function createDOMPurify() {
173633
174536
  if (typeof hookFunction !== 'function') {
173634
174537
  return;
173635
174538
  }
174539
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
174540
+ * '__proto__') indexes off the prototype chain rather than a real
174541
+ * hook array, and arrayPush then writes to Object.prototype. Guard
174542
+ * with an own-property check against the known hook names. */
174543
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
174544
+ return;
174545
+ }
173636
174546
  arrayPush(hooks[entryPoint], hookFunction);
173637
174547
  };
173638
174548
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
174549
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
174550
+ return undefined;
174551
+ }
173639
174552
  if (hookFunction !== undefined) {
173640
174553
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
173641
174554
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -173643,6 +174556,9 @@ function createDOMPurify() {
173643
174556
  return arrayPop(hooks[entryPoint]);
173644
174557
  };
173645
174558
  DOMPurify.removeHooks = function (entryPoint) {
174559
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
174560
+ return;
174561
+ }
173646
174562
  hooks[entryPoint] = [];
173647
174563
  };
173648
174564
  DOMPurify.removeAllHooks = function () {