@cognigy/webchat 3.45.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.45.0
3
- * https://github.com/Cognigy/Webchat/tree/v3.45.0
4
- * https://github.com/Cognigy/Webchat/tree/v3.45.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";
@@ -38088,6 +38088,25 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38088
38088
  }
38089
38089
  }
38090
38090
  };
38091
+
38092
+ /**
38093
+ * The webchat window (#webchatWindow) is a fixed-size flex shell with `overflow: hidden`; its
38094
+ * children (header, chat log, input) manage their own scrolling. Safari/WebKit, however, still
38095
+ * scrolls an `overflow: hidden` ancestor to bring a newly focused element into view — so during
38096
+ * (reverse) keyboard navigation it nudges the whole window by a few pixels, shifting the header
38097
+ * up and clipping the layout. Nothing else resets it, so the broken offset sticks (AB#105695).
38098
+ * Snap the window back to its origin whenever it is scrolled. This is browser- and message-type
38099
+ * agnostic and never interferes with the inner scroll containers.
38100
+ */
38101
+ handleWebchatWindowScroll = event => {
38102
+ // Only act on the window's own scroll, never on a descendant scroll container that might
38103
+ // surface here. React 18 does not bubble synthetic scroll events, but this keeps the
38104
+ // target-only intent explicit and resilient to any future change in event delegation.
38105
+ if (event.target !== event.currentTarget) return;
38106
+ const el = event.currentTarget;
38107
+ if (el.scrollTop !== 0) el.scrollTop = 0;
38108
+ if (el.scrollLeft !== 0) el.scrollLeft = 0;
38109
+ };
38091
38110
  handleSendRating = ({
38092
38111
  rating,
38093
38112
  comment,
@@ -38191,10 +38210,20 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38191
38210
  /**
38192
38211
  * Emits the `webchat/close-button` analytics event.
38193
38212
  *
38194
- * Fired only from the close ("X") button handlers — the chat window header
38195
- * (`handleCloseAndReset`) and the disconnect overlay (`handleClose`). It is
38196
- * NOT emitted for the toggle button, the home-screen close (which minimizes),
38197
- * 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.
38198
38227
  *
38199
38228
  * This lives in the component rather than the redux analytics middleware
38200
38229
  * (where `webchat/open|close|minimize` are emitted) on purpose: the store only
@@ -38202,7 +38231,10 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38202
38231
  * the close, so the originating interaction is only known here.
38203
38232
  */
38204
38233
  emitCloseButtonAnalytics = () => {
38205
- this.props.onEmitAnalytics("webchat/close-button");
38234
+ this.props.onEmitAnalytics("webchat/close-button", {
38235
+ connected: this.props.connected,
38236
+ hadConnection: this.state.hadConnection
38237
+ });
38206
38238
  };
38207
38239
  render() {
38208
38240
  const {
@@ -38338,6 +38370,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38338
38370
  className: "webchat",
38339
38371
  id: "webchatWindow",
38340
38372
  ref: this.webchatWindowRef,
38373
+ onScroll: this.handleWebchatWindowScroll,
38341
38374
  chatWindowWidth: this.props.config.settings.layout.chatWindowWidth
38342
38375
  }), !fullscreenMessage ? this.renderRegularLayout(isInforming) : this.renderFullscreenMessageLayout(), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_DisconnectOverlay__WEBPACK_IMPORTED_MODULE_20__/* ["default"] */ .A, {
38343
38376
  isOpen: showDisconnectOverlay,
@@ -38457,6 +38490,17 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38457
38490
  // Restore focus to chat toggle button
38458
38491
  this.chatToggleButtonRef?.current?.focus?.();
38459
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
+ };
38460
38504
  const handleOnGoBack = () => {
38461
38505
  if (!showChatOptionsScreen && !showRatingScreen) {
38462
38506
  onSetShowPrevConversations(false);
@@ -38616,7 +38660,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38616
38660
  onSetShowHomeScreen: onSetShowHomeScreen,
38617
38661
  onStartConversation: this.handleStartConversation,
38618
38662
  onSetShowPrevConversations: onSetShowPrevConversations,
38619
- onClose: handleOnMinimize,
38663
+ onClose: handleHomeScreenClose,
38620
38664
  config: config,
38621
38665
  onEmitAnalytics: onEmitAnalytics,
38622
38666
  onSendActionButtonMessage: this.handleSendActionButtonMessage
@@ -44684,6 +44728,22 @@ class Webchat extends react__WEBPACK_IMPORTED_MODULE_0__.PureComponent {
44684
44728
  connect = async () => {
44685
44729
  this.store.dispatch((0,_store_connection_connection_middleware__WEBPACK_IMPORTED_MODULE_7__/* .connect */ .Ng)());
44686
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
+ };
44687
44747
  sendMessage = (text, data, options) => {
44688
44748
  this.store.dispatch((0,_store_messages_message_middleware__WEBPACK_IMPORTED_MODULE_4__/* .sendMessage */ ._z)({
44689
44749
  text,
@@ -56905,6 +56965,23 @@ a._button_19rd2_1._disabled_19rd2_52:focus {
56905
56965
  height: 100%;
56906
56966
  border-right: 2px solid var(--cc-primary-contrast-color);
56907
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
+ }
56908
56985
  ._buttons_tq8ye_1 {
56909
56986
  align-items: flex-start;
56910
56987
  display: flex;
@@ -57577,23 +57654,6 @@ article ._wrapper_zhv80_1 ._list_zhv80_8 ._listItemRoot_zhv80_8 {
57577
57654
  ._mainButtonWrapper_zhv80_144 {
57578
57655
  padding: 16px;
57579
57656
  }
57580
- ._srOnly_14hnz_1 {
57581
- position: absolute;
57582
- width: 1px;
57583
- height: 1px;
57584
- padding: 0;
57585
- margin: -1px;
57586
- overflow: hidden;
57587
- clip: rect(0, 0, 0, 0);
57588
- white-space: nowrap;
57589
- border: 0;
57590
- }
57591
-
57592
- ._divider_14hnz_13 {
57593
- border-bottom-width: 1px;
57594
- border-bottom-style: solid;
57595
- border-bottom-color: var(--cc-black-80);
57596
- }
57597
57657
  ._slideItem_1prat_1 {
57598
57658
  position: relative;
57599
57659
  width: 206px;
@@ -58913,13 +58973,17 @@ article ._header_21mid_1 ._avatar-name_21mid_27 {
58913
58973
  margin: 0;
58914
58974
  padding: 0;
58915
58975
  }
58916
- article ._avatar_ry0w9_1 {
58976
+ article ._avatar_o5ge0_1 {
58917
58977
  border-radius: 1000px;
58918
58978
  width: 28px;
58919
58979
  height: 28px;
58920
- background-color: var(--cc-primary-color);
58921
58980
  box-sizing: border-box;
58922
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
+ }
58923
58987
  :root {
58924
58988
  --cc-font-family: var(--webchat-font-family, Figtree);
58925
58989
  --cc-action-button-border-radius: var(--webchat-action-button-border-radius, 19px);
@@ -85254,11 +85318,16 @@ const Text = props2 => {
85254
85318
  a: ({
85255
85319
  node: _node,
85256
85320
  ...props22
85257
- }) => /* @__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", {
85258
85327
  target: "_blank",
85259
85328
  rel: "noreferrer",
85260
85329
  ...props22
85261
- }),
85330
+ })),
85262
85331
  p: ({
85263
85332
  node: _node,
85264
85333
  children,
@@ -85908,6 +85977,12 @@ const classes$e = {
85908
85977
  imageDisabled,
85909
85978
  buttonImageContainer
85910
85979
  };
85980
+ const srOnly = "_srOnly_14hnz_1";
85981
+ const divider = "_divider_14hnz_13";
85982
+ const mainClasses = {
85983
+ srOnly,
85984
+ divider
85985
+ };
85911
85986
  const ActionButton = props2 => {
85912
85987
  const {
85913
85988
  button: button2,
@@ -85930,32 +86005,29 @@ const ActionButton = props2 => {
85930
86005
  const customAllowedHtmlTags = config2?.settings?.widgetSettings?.customAllowedHtmlTags;
85931
86006
  const __html = config2?.settings?.layout?.disableHtmlContentSanitization ? buttonLabel : sanitizeHTMLWithConfig(buttonLabel, customAllowedHtmlTags);
85932
86007
  const isPhoneNumber = button2.payload && (buttonType === "phone_number" || buttonType === "user_phone_number");
85933
- const buttonTitle = button2.title || "";
85934
86008
  const isWebURL = "type" in button2 && button2.type === "web_url";
85935
86009
  const isWebURLButtonTargetBlank = isWebURL && button2.target !== "_self";
85936
86010
  const opensInNewTabLabel = config2?.settings?.customTranslations?.ariaLabels?.opensInNewTab || "Opens in new tab";
85937
- const getAriaLabel = () => {
85938
- const isURLInNewTab = isWebURL && isWebURLButtonTargetBlank;
85939
- const newTabURLButtonTitle = `${buttonTitle}. ${opensInNewTabLabel}`;
85940
- const buttonTitleWithTarget = isURLInNewTab ? newTabURLButtonTitle : button2.title;
85941
- if (total > 1) {
85942
- return interpolateString(config2?.settings?.customTranslations?.ariaLabels?.actionButtonPositionText ?? "{position} of {total}", {
85943
- position: position2.toString(),
85944
- total: total.toString()
85945
- }) + ": " + buttonTitleWithTarget;
85946
- } else if (total <= 1 && isURLInNewTab) {
85947
- return newTabURLButtonTitle;
85948
- }
85949
- };
85950
- 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", {
85951
86020
  ...props22,
85952
86021
  href: `tel:${button2.payload}`
85953
- }) : null;
85954
- 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", {
85955
86027
  ...props22,
85956
86028
  href: button2.url,
85957
86029
  target: button2.target
85958
- }) : null;
86030
+ }) : null);
85959
86031
  const Button2 = props22 => /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("button", {
85960
86032
  ...props22,
85961
86033
  disabled: disabled2
@@ -86009,10 +86081,12 @@ const ActionButton = props2 => {
86009
86081
  return /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(Component, {
86010
86082
  onClick: onClick2,
86011
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"),
86012
- "aria-label": getAriaLabel(),
86013
86084
  "aria-disabled": disabled2,
86014
86085
  tabIndex: disabled2 ? -1 : 0,
86015
- 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", {
86016
86090
  className: classes$e.buttonImageContainer,
86017
86091
  children: /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", {
86018
86092
  src: buttonImage2,
@@ -86026,7 +86100,10 @@ const ActionButton = props2 => {
86026
86100
  __html
86027
86101
  },
86028
86102
  className: !!buttonImage2 && classes$e.buttonLabelWithImage
86029
- }), renderIcon()]
86103
+ }), renderIcon(), isWebURL && isWebURLButtonTargetBlank && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
86104
+ className: mainClasses.srOnly,
86105
+ children: `, ${opensInNewTabLabel}`
86106
+ })]
86030
86107
  });
86031
86108
  };
86032
86109
  const buttons$1 = "_buttons_tq8ye_1";
@@ -92711,7 +92788,12 @@ const Controls = props2 => {
92711
92788
  })]
92712
92789
  })
92713
92790
  })]
92714
- }), 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", {
92715
92797
  ref: downloadTranscriptLinkRef,
92716
92798
  href: `data:text/plain;charset=utf-8,${encodeURIComponent(altText)}`,
92717
92799
  download: "audio-transcript.txt",
@@ -92973,12 +93055,6 @@ const classes$9 = {
92973
93055
  listItemButtonWrapper,
92974
93056
  mainButtonWrapper
92975
93057
  };
92976
- const srOnly = "_srOnly_14hnz_1";
92977
- const divider = "_divider_14hnz_13";
92978
- const mainClasses = {
92979
- srOnly,
92980
- divider
92981
- };
92982
93058
  const ListItem = props2 => {
92983
93059
  const {
92984
93060
  action,
@@ -93039,9 +93115,11 @@ const ListItem = props2 => {
93039
93115
  backgroundImage: getBackgroundImage(image_url)
93040
93116
  },
93041
93117
  "data-testid": "regular-image",
93042
- 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", {
93043
93119
  role: "img",
93044
93120
  "aria-label": image_alt_text
93121
+ }) : /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
93122
+ "aria-hidden": "true"
93045
93123
  })
93046
93124
  });
93047
93125
  }, [image_alt_text, image_url]);
@@ -118652,7 +118730,12 @@ const DatePicker = () => {
118652
118730
  "aria-controls": `webchat-plugin-date-picker-${message2.id}`,
118653
118731
  "aria-haspopup": "dialog",
118654
118732
  children: openText
118655
- }), 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", {
118656
118739
  ref: datePickerRef,
118657
118740
  id: `webchat-plugin-date-picker-${message2.id}`,
118658
118741
  className: classnames$1(classes$6.wrapper, "webchat-plugin-date-picker"),
@@ -134709,7 +134792,7 @@ const classes$3 = {
134709
134792
  timestamp,
134710
134793
  "avatar-name": "_avatar-name_21mid_27"
134711
134794
  };
134712
- const avatar = "_avatar_ry0w9_1";
134795
+ const avatar = "_avatar_o5ge0_1";
134713
134796
  const classes$2 = {
134714
134797
  avatar
134715
134798
  };
@@ -134738,11 +134821,13 @@ const Avatar = props2 => {
134738
134821
  setAvatarUrl(botAvatar);
134739
134822
  }
134740
134823
  }, [message2.avatarUrl, message2?.source, overrides.agentAvatarOverrideUrlOnce, overrides.botAvatarOverrideUrlOnce]);
134824
+ const isDefaultAvatar = avatarUrl === botAvatar || avatarUrl === placeholderAvatar;
134741
134825
  const classNames = classnames$1(classes$2.avatar, props2.className, "webchat-avatar", message2?.source);
134742
134826
  return /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", {
134743
134827
  alt: "",
134744
134828
  className: classNames,
134745
134829
  src: avatarUrl,
134830
+ "data-default-avatar": isDefaultAvatar ? "" : void 0,
134746
134831
  onError: () => setAvatarUrl(placeholderAvatar),
134747
134832
  "data-testid": `${message2?.source || "unknown source"}-avatar`
134748
134833
  });
@@ -172236,24 +172321,65 @@ function isUrl(fileUrlOrPath) {
172236
172321
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
172237
172322
  /* harmony export */ A: () => (/* binding */ purify)
172238
172323
  /* harmony export */ });
172239
- /*! @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 */
172240
172325
 
172241
- const {
172242
- entries,
172243
- setPrototypeOf,
172244
- isFrozen,
172245
- getPrototypeOf,
172246
- getOwnPropertyDescriptor
172247
- } = Object;
172248
- let {
172249
- freeze,
172250
- seal,
172251
- create
172252
- } = Object; // eslint-disable-line import/no-mutable-exports
172253
- let {
172254
- apply,
172255
- construct
172256
- } = 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;
172257
172383
  if (!freeze) {
172258
172384
  freeze = function freeze(x) {
172259
172385
  return x;
@@ -172285,13 +172411,19 @@ const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
172285
172411
  const arrayPop = unapply(Array.prototype.pop);
172286
172412
  const arrayPush = unapply(Array.prototype.push);
172287
172413
  const arraySplice = unapply(Array.prototype.splice);
172414
+ const arrayIsArray = Array.isArray;
172288
172415
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
172289
172416
  const stringToString = unapply(String.prototype.toString);
172290
172417
  const stringMatch = unapply(String.prototype.match);
172291
172418
  const stringReplace = unapply(String.prototype.replace);
172292
172419
  const stringIndexOf = unapply(String.prototype.indexOf);
172293
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);
172294
172425
  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
172426
+ const objectToString = unapply(Object.prototype.toString);
172295
172427
  const regExpTest = unapply(RegExp.prototype.test);
172296
172428
  const typeErrorCreate = unconstruct(TypeError);
172297
172429
  /**
@@ -172341,6 +172473,9 @@ function addToSet(set, array) {
172341
172473
  // Prevent prototype setters from intercepting set as a this value.
172342
172474
  setPrototypeOf(set, null);
172343
172475
  }
172476
+ if (!arrayIsArray(array)) {
172477
+ return set;
172478
+ }
172344
172479
  let l = array.length;
172345
172480
  while (l--) {
172346
172481
  let element = array[l];
@@ -172381,10 +172516,13 @@ function cleanArray(array) {
172381
172516
  */
172382
172517
  function clone(object) {
172383
172518
  const newObject = create(null);
172384
- 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];
172385
172523
  const isPropertyExist = objectHasOwnProperty(object, property);
172386
172524
  if (isPropertyExist) {
172387
- if (Array.isArray(value)) {
172525
+ if (arrayIsArray(value)) {
172388
172526
  newObject[property] = cleanArray(value);
172389
172527
  } else if (value && typeof value === 'object' && value.constructor === Object) {
172390
172528
  newObject[property] = clone(value);
@@ -172395,6 +172533,58 @@ function clone(object) {
172395
172533
  }
172396
172534
  return newObject;
172397
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
+ }
172398
172588
  /**
172399
172589
  * This method automatically checks if the prop is function or getter and behaves accordingly.
172400
172590
  *
@@ -172420,6 +172610,14 @@ function lookupGetter(object, prop) {
172420
172610
  }
172421
172611
  return fallbackValue;
172422
172612
  }
172613
+ function isRegex(value) {
172614
+ try {
172615
+ regExpTest(value, '');
172616
+ return true;
172617
+ } catch (_unused) {
172618
+ return false;
172619
+ }
172620
+ }
172423
172621
 
172424
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']);
172425
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']);
@@ -172435,15 +172633,14 @@ const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mgly
172435
172633
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
172436
172634
  const text = freeze(['#text']);
172437
172635
 
172438
- 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']);
172439
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']);
172440
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']);
172441
172639
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
172442
172640
 
172443
- // eslint-disable-next-line unicorn/better-regex
172444
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
172445
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
172446
- 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);
172447
172644
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
172448
172645
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
172449
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
@@ -172453,30 +172650,31 @@ const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205
172453
172650
  );
172454
172651
  const DOCTYPE_NAME = seal(/^html$/i);
172455
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);
172456
172660
 
172457
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
172458
- __proto__: null,
172459
- ARIA_ATTR: ARIA_ATTR,
172460
- ATTR_WHITESPACE: ATTR_WHITESPACE,
172461
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
172462
- DATA_ATTR: DATA_ATTR,
172463
- DOCTYPE_NAME: DOCTYPE_NAME,
172464
- ERB_EXPR: ERB_EXPR,
172465
- IS_ALLOWED_URI: IS_ALLOWED_URI,
172466
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
172467
- MUSTACHE_EXPR: MUSTACHE_EXPR,
172468
- TMPLIT_EXPR: TMPLIT_EXPR
172469
- });
172470
-
172471
- /* eslint-disable @typescript-eslint/indent */
172472
172661
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
172473
172662
  const NODE_TYPE = {
172474
172663
  element: 1,
172664
+ attribute: 2,
172475
172665
  text: 3,
172666
+ cdataSection: 4,
172667
+ entityReference: 5,
172476
172668
  // Deprecated
172477
- progressingInstruction: 7,
172669
+ entityNode: 6,
172670
+ // Deprecated
172671
+ processingInstruction: 7,
172478
172672
  comment: 8,
172479
- document: 9};
172673
+ document: 9,
172674
+ documentType: 10,
172675
+ documentFragment: 11,
172676
+ notation: 12 // Deprecated
172677
+ };
172480
172678
  const getGlobal = function getGlobal() {
172481
172679
  return typeof window === 'undefined' ? null : window;
172482
172680
  };
@@ -172531,10 +172729,25 @@ const _createHooksMap = function _createHooksMap() {
172531
172729
  uponSanitizeShadowNode: []
172532
172730
  };
172533
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
+ };
172534
172747
  function createDOMPurify() {
172535
172748
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
172536
172749
  const DOMPurify = root => createDOMPurify(root);
172537
- DOMPurify.version = '3.4.0';
172750
+ DOMPurify.version = '3.4.11';
172538
172751
  DOMPurify.removed = [];
172539
172752
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
172540
172753
  // Not running in a browser, provide a factory function
@@ -172542,28 +172755,29 @@ function createDOMPurify() {
172542
172755
  DOMPurify.isSupported = false;
172543
172756
  return DOMPurify;
172544
172757
  }
172545
- let {
172546
- document
172547
- } = window;
172758
+ let document = window.document;
172548
172759
  const originalDocument = document;
172549
172760
  const currentScript = originalDocument.currentScript;
172550
- const {
172551
- DocumentFragment,
172552
- HTMLTemplateElement,
172553
- Node,
172554
- Element,
172555
- NodeFilter,
172556
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
172557
- HTMLFormElement,
172558
- DOMParser,
172559
- trustedTypes
172560
- } = 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;
172561
172771
  const ElementPrototype = Element.prototype;
172562
172772
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
172563
172773
  const remove = lookupGetter(ElementPrototype, 'remove');
172564
172774
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
172565
172775
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
172566
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;
172567
172781
  // As per issue #47, the web-components registry is inherited by a
172568
172782
  // new document created via createHTMLDocument. As per the spec
172569
172783
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -172578,33 +172792,74 @@ function createDOMPurify() {
172578
172792
  }
172579
172793
  let trustedTypesPolicy;
172580
172794
  let emptyHTML = '';
172581
- const {
172582
- implementation,
172583
- createNodeIterator,
172584
- createDocumentFragment,
172585
- getElementsByTagName
172586
- } = document;
172587
- const {
172588
- importNode
172589
- } = 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;
172590
172849
  let hooks = _createHooksMap();
172591
172850
  /**
172592
172851
  * Expose whether this browser supports running the full DOMPurify.
172593
172852
  */
172594
172853
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
172595
- const {
172596
- MUSTACHE_EXPR,
172597
- ERB_EXPR,
172598
- TMPLIT_EXPR,
172599
- DATA_ATTR,
172600
- ARIA_ATTR,
172601
- IS_SCRIPT_OR_DATA,
172602
- ATTR_WHITESPACE,
172603
- CUSTOM_ELEMENT
172604
- } = EXPRESSIONS;
172605
- let {
172606
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
172607
- } = 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;
172608
172863
  /**
172609
172864
  * We consider the elements and attributes below to be safe. Ideally
172610
172865
  * don't add any new ones but feel free to remove unwanted ones.
@@ -172681,6 +172936,13 @@ function createDOMPurify() {
172681
172936
  let WHOLE_DOCUMENT = false;
172682
172937
  /* Track whether config is already set on this instance of DOMPurify. */
172683
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;
172684
172946
  /* Decide if all elements (e.g. style, script) must be children of
172685
172947
  * document.body. By default, browsers might move them to document.head */
172686
172948
  let FORCE_BODY = false;
@@ -172723,7 +172985,17 @@ function createDOMPurify() {
172723
172985
  let USE_PROFILES = {};
172724
172986
  /* Tags to ignore content of when KEEP_CONTENT is true */
172725
172987
  let FORBID_CONTENTS = null;
172726
- 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']);
172727
172999
  /* Tags that are safe for data: URIs */
172728
173000
  let DATA_URI_TAGS = null;
172729
173001
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
@@ -172739,8 +173011,10 @@ function createDOMPurify() {
172739
173011
  /* Allowed XHTML+XML namespaces */
172740
173012
  let ALLOWED_NAMESPACES = null;
172741
173013
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
172742
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
172743
- 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);
172744
173018
  // Certain elements are allowed in both SVG and HTML
172745
173019
  // namespace. We need to specify them explicitly
172746
173020
  // so that they don't get erroneously deleted from
@@ -172782,15 +173056,33 @@ function createDOMPurify() {
172782
173056
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
172783
173057
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
172784
173058
  /* Set configuration parameters */
172785
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
172786
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
172787
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
172788
- 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;
172789
- 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;
172790
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
172791
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
172792
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
172793
- 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;
172794
173086
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
172795
173087
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
172796
173088
  ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
@@ -172806,20 +173098,22 @@ function createDOMPurify() {
172806
173098
  SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
172807
173099
  KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
172808
173100
  IN_PLACE = cfg.IN_PLACE || false; // Default false
172809
- IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
172810
- NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
172811
- MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
172812
- HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
172813
- CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || create(null);
172814
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
172815
- CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
172816
- }
172817
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
172818
- CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
172819
- }
172820
- if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
172821
- CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
172822
- }
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);
172823
173117
  if (SAFE_FOR_TEMPLATES) {
172824
173118
  ALLOW_DATA_ATTR = false;
172825
173119
  }
@@ -172855,36 +173149,36 @@ function createDOMPurify() {
172855
173149
  EXTRA_ELEMENT_HANDLING.tagCheck = null;
172856
173150
  EXTRA_ELEMENT_HANDLING.attributeCheck = null;
172857
173151
  /* Merge configuration parameters */
172858
- if (cfg.ADD_TAGS) {
173152
+ if (objectHasOwnProperty(cfg, 'ADD_TAGS')) {
172859
173153
  if (typeof cfg.ADD_TAGS === 'function') {
172860
173154
  EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
172861
- } else {
173155
+ } else if (arrayIsArray(cfg.ADD_TAGS)) {
172862
173156
  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
172863
173157
  ALLOWED_TAGS = clone(ALLOWED_TAGS);
172864
173158
  }
172865
173159
  addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
172866
173160
  }
172867
173161
  }
172868
- if (cfg.ADD_ATTR) {
173162
+ if (objectHasOwnProperty(cfg, 'ADD_ATTR')) {
172869
173163
  if (typeof cfg.ADD_ATTR === 'function') {
172870
173164
  EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
172871
- } else {
173165
+ } else if (arrayIsArray(cfg.ADD_ATTR)) {
172872
173166
  if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
172873
173167
  ALLOWED_ATTR = clone(ALLOWED_ATTR);
172874
173168
  }
172875
173169
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
172876
173170
  }
172877
173171
  }
172878
- if (cfg.ADD_URI_SAFE_ATTR) {
173172
+ if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
172879
173173
  addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
172880
173174
  }
172881
- if (cfg.FORBID_CONTENTS) {
173175
+ if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
172882
173176
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
172883
173177
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
172884
173178
  }
172885
173179
  addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
172886
173180
  }
172887
- if (cfg.ADD_FORBID_CONTENTS) {
173181
+ if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
172888
173182
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
172889
173183
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
172890
173184
  }
@@ -172903,6 +173197,13 @@ function createDOMPurify() {
172903
173197
  addToSet(ALLOWED_TAGS, ['tbody']);
172904
173198
  delete FORBID_TAGS.tbody;
172905
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.
172906
173207
  if (cfg.TRUSTED_TYPES_POLICY) {
172907
173208
  if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
172908
173209
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
@@ -172910,18 +173211,45 @@ function createDOMPurify() {
172910
173211
  if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
172911
173212
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
172912
173213
  }
172913
- // Overwrite existing TrustedTypes policy.
173214
+ // A caller-supplied policy applies to this configuration only.
173215
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
172914
173216
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
172915
- // Sign local variables required by `sanitize`.
172916
- 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 = '';
172917
173237
  } else {
172918
- // 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.)
172919
173244
  if (trustedTypesPolicy === undefined) {
172920
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
173245
+ trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
172921
173246
  }
172922
- // If creating the internal policy succeeded sign internal variables.
172923
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
172924
- 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('');
172925
173253
  }
172926
173254
  }
172927
173255
  // Prevent further manipulation of configuration.
@@ -172936,6 +173264,77 @@ function createDOMPurify() {
172936
173264
  * correctly. */
172937
173265
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
172938
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
+ };
172939
173338
  /**
172940
173339
  * @param element a DOM element whose namespace is being checked
172941
173340
  * @returns Return false if the element has a
@@ -172958,51 +173357,13 @@ function createDOMPurify() {
172958
173357
  return false;
172959
173358
  }
172960
173359
  if (element.namespaceURI === SVG_NAMESPACE) {
172961
- // The only way to switch from HTML namespace to SVG
172962
- // is via <svg>. If it happens via any other tag, then
172963
- // it should be killed.
172964
- if (parent.namespaceURI === HTML_NAMESPACE) {
172965
- return tagName === 'svg';
172966
- }
172967
- // The only way to switch from MathML to SVG is via`
172968
- // svg if parent is either <annotation-xml> or MathML
172969
- // text integration points.
172970
- if (parent.namespaceURI === MATHML_NAMESPACE) {
172971
- return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
172972
- }
172973
- // We only allow elements that are defined in SVG
172974
- // spec. All others are disallowed in SVG namespace.
172975
- return Boolean(ALL_SVG_TAGS[tagName]);
173360
+ return _checkSvgNamespace(tagName, parent, parentTagName);
172976
173361
  }
172977
173362
  if (element.namespaceURI === MATHML_NAMESPACE) {
172978
- // The only way to switch from HTML namespace to MathML
172979
- // is via <math>. If it happens via any other tag, then
172980
- // it should be killed.
172981
- if (parent.namespaceURI === HTML_NAMESPACE) {
172982
- return tagName === 'math';
172983
- }
172984
- // The only way to switch from SVG to MathML is via
172985
- // <math> and HTML integration points
172986
- if (parent.namespaceURI === SVG_NAMESPACE) {
172987
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
172988
- }
172989
- // We only allow elements that are defined in MathML
172990
- // spec. All others are disallowed in MathML namespace.
172991
- return Boolean(ALL_MATHML_TAGS[tagName]);
173363
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
172992
173364
  }
172993
173365
  if (element.namespaceURI === HTML_NAMESPACE) {
172994
- // The only way to switch from SVG to HTML is via
172995
- // HTML integration points, and from MathML to HTML
172996
- // is via MathML text integration points
172997
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
172998
- return false;
172999
- }
173000
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
173001
- return false;
173002
- }
173003
- // We disallow tags that are specific for MathML
173004
- // or SVG and should never appear in HTML namespace
173005
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
173366
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
173006
173367
  }
173007
173368
  // For XHTML and XML documents that support custom namespaces
173008
173369
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -173027,7 +173388,74 @@ function createDOMPurify() {
173027
173388
  // eslint-disable-next-line unicorn/prefer-dom-node-remove
173028
173389
  getParentNode(node).removeChild(node);
173029
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. */
173030
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
+ }
173031
173459
  }
173032
173460
  };
173033
173461
  /**
@@ -173062,6 +173490,72 @@ function createDOMPurify() {
173062
173490
  }
173063
173491
  }
173064
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
+ };
173065
173559
  /**
173066
173560
  * _initDocument
173067
173561
  *
@@ -173083,7 +173577,7 @@ function createDOMPurify() {
173083
173577
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
173084
173578
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
173085
173579
  }
173086
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
173580
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
173087
173581
  /*
173088
173582
  * Use the DOMParser API by default, fallback later if needs be
173089
173583
  * DOMParser not work for svg when has multiple root element.
@@ -173123,29 +173617,254 @@ function createDOMPurify() {
173123
173617
  // eslint-disable-next-line no-bitwise
173124
173618
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
173125
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
+ };
173126
173676
  /**
173127
173677
  * _isClobbered
173128
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
+ *
173129
173685
  * @param element element to check for clobbering attacks
173130
173686
  * @return true if clobbered, false if safe
173131
173687
  */
173132
173688
  const _isClobbered = function _isClobbered(element) {
173133
- 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
+ }
173134
173747
  };
173135
173748
  /**
173136
- * 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.
173137
173755
  *
173138
173756
  * @param value object to check whether it's a DOM node
173139
- * @return true is object is a DOM node
173757
+ * @return true if value is a DOM node from any realm
173140
173758
  */
173141
173759
  const _isNode = function _isNode(value) {
173142
- 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
+ }
173143
173768
  };
173144
173769
  function _executeHooks(hooks, currentNode, data) {
173770
+ if (hooks.length === 0) {
173771
+ return;
173772
+ }
173145
173773
  arrayForEach(hooks, hook => {
173146
173774
  hook.call(DOMPurify, currentNode, data, CONFIG);
173147
173775
  });
173148
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
+ };
173149
173868
  /**
173150
173869
  * _sanitizeElements
173151
173870
  *
@@ -173156,7 +173875,6 @@ function createDOMPurify() {
173156
173875
  * @return true if node was killed, false if left alive
173157
173876
  */
173158
173877
  const _sanitizeElements = function _sanitizeElements(currentNode) {
173159
- let content = null;
173160
173878
  /* Execute a hook if present */
173161
173879
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
173162
173880
  /* Check if element is clobbered or can clobber */
@@ -173165,76 +173883,41 @@ function createDOMPurify() {
173165
173883
  return true;
173166
173884
  }
173167
173885
  /* Now let's check the element's type and name */
173168
- const tagName = transformCaseFunc(currentNode.nodeName);
173886
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
173169
173887
  /* Execute a hook if present */
173170
173888
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
173171
173889
  tagName,
173172
173890
  allowedTags: ALLOWED_TAGS
173173
173891
  });
173174
- /* Detect mXSS attempts abusing namespace confusion */
173175
- if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
173176
- _forceRemove(currentNode);
173177
- return true;
173178
- }
173179
- /* Remove risky CSS construction leading to mXSS */
173180
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
173181
- _forceRemove(currentNode);
173182
- return true;
173183
- }
173184
- /* Remove any occurrence of processing instructions */
173185
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
173186
- _forceRemove(currentNode);
173187
- return true;
173188
- }
173189
- /* Remove any kind of possibly harmful comments */
173190
- 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)) {
173191
173894
  _forceRemove(currentNode);
173192
173895
  return true;
173193
173896
  }
173194
173897
  /* Remove element if anything forbids its presence */
173195
173898
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
173196
- /* Check if we have a custom element to handle */
173197
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
173198
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
173199
- return false;
173200
- }
173201
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
173202
- return false;
173203
- }
173204
- }
173205
- /* Keep content except for bad-listed elements */
173206
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
173207
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
173208
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
173209
- if (childNodes && parentNode) {
173210
- const childCount = childNodes.length;
173211
- for (let i = childCount - 1; i >= 0; --i) {
173212
- const childClone = cloneNode(childNodes[i], true);
173213
- childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
173214
- parentNode.insertBefore(childClone, getNextSibling(currentNode));
173215
- }
173216
- }
173217
- }
173218
- _forceRemove(currentNode);
173219
- return true;
173220
- }
173221
- /* Check whether element has a valid namespace */
173222
- 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)) {
173223
173909
  _forceRemove(currentNode);
173224
173910
  return true;
173225
173911
  }
173226
173912
  /* Make sure that older browsers don't get fallback-tag mXSS */
173227
- 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)) {
173228
173914
  _forceRemove(currentNode);
173229
173915
  return true;
173230
173916
  }
173231
173917
  /* Sanitize element content to be template-safe */
173232
173918
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
173233
173919
  /* Get the element's text content */
173234
- content = currentNode.textContent;
173235
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
173236
- content = stringReplace(content, expr, ' ');
173237
- });
173920
+ const content = _stripTemplateExpressions(currentNode.textContent);
173238
173921
  if (currentNode.textContent !== content) {
173239
173922
  arrayPush(DOMPurify.removed, {
173240
173923
  element: currentNode.cloneNode()
@@ -173264,11 +173947,12 @@ function createDOMPurify() {
173264
173947
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
173265
173948
  return false;
173266
173949
  }
173950
+ const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
173267
173951
  /* Allow valid data-* attributes: At least one character after "-"
173268
173952
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
173269
173953
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
173270
173954
  We don't need to check the value; it's always URI safe. */
173271
- 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) {
173272
173956
  if (
173273
173957
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
173274
173958
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -173280,11 +173964,15 @@ function createDOMPurify() {
173280
173964
  return false;
173281
173965
  }
173282
173966
  /* Check value is safe. First, is attr inert? If so, is safe */
173283
- } 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) {
173284
173968
  return false;
173285
173969
  } else ;
173286
173970
  return true;
173287
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']);
173288
173976
  /**
173289
173977
  * _isBasicCustomElement
173290
173978
  * checks if at least one dash is included in tagName, and it's not the first char
@@ -173294,7 +173982,64 @@ function createDOMPurify() {
173294
173982
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
173295
173983
  */
173296
173984
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
173297
- 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
+ }
173298
174043
  };
173299
174044
  /**
173300
174045
  * _sanitizeAttributes
@@ -173309,9 +174054,7 @@ function createDOMPurify() {
173309
174054
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
173310
174055
  /* Execute a hook if present */
173311
174056
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
173312
- const {
173313
- attributes
173314
- } = currentNode;
174057
+ const attributes = currentNode.attributes;
173315
174058
  /* Check if we have attributes; if not we might have a text node */
173316
174059
  if (!attributes || _isClobbered(currentNode)) {
173317
174060
  return;
@@ -173324,14 +174067,13 @@ function createDOMPurify() {
173324
174067
  forceKeepAttr: undefined
173325
174068
  };
173326
174069
  let l = attributes.length;
174070
+ const lcTag = transformCaseFunc(currentNode.nodeName);
173327
174071
  /* Go backwards over all attributes; safely remove bad ones */
173328
174072
  while (l--) {
173329
174073
  const attr = attributes[l];
173330
- const {
173331
- name,
173332
- namespaceURI,
173333
- value: attrValue
173334
- } = attr;
174074
+ const name = attr.name,
174075
+ namespaceURI = attr.namespaceURI,
174076
+ attrValue = attr.value;
173335
174077
  const lcName = transformCaseFunc(name);
173336
174078
  const initValue = attrValue;
173337
174079
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -173345,12 +174087,14 @@ function createDOMPurify() {
173345
174087
  /* Full DOM Clobbering protection via namespace isolation,
173346
174088
  * Prefix id and name attributes with `user-content-`
173347
174089
  */
173348
- if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
174090
+ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
173349
174091
  // Remove the attribute with this value
173350
174092
  _removeAttribute(name, currentNode);
173351
174093
  // Prefix the value and later re-create the attribute with the sanitized value
173352
174094
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
173353
174095
  }
174096
+ // Else: already prefixed, leave the attribute alone — the prefix is
174097
+ // itself the clobbering protection, and re-applying it is incorrect.
173354
174098
  /* Work around a security issue with comments inside attributes */
173355
174099
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
173356
174100
  _removeAttribute(name, currentNode);
@@ -173361,7 +174105,7 @@ function createDOMPurify() {
173361
174105
  _removeAttribute(name, currentNode);
173362
174106
  continue;
173363
174107
  }
173364
- /* Did the hooks approve of the attribute? */
174108
+ /* Did the hooks force-keep the attribute? */
173365
174109
  if (hookEvent.forceKeepAttr) {
173366
174110
  continue;
173367
174111
  }
@@ -173371,56 +174115,24 @@ function createDOMPurify() {
173371
174115
  continue;
173372
174116
  }
173373
174117
  /* Work around a security issue in jQuery 3.0 */
173374
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
174118
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
173375
174119
  _removeAttribute(name, currentNode);
173376
174120
  continue;
173377
174121
  }
173378
174122
  /* Sanitize attribute content to be template-safe */
173379
174123
  if (SAFE_FOR_TEMPLATES) {
173380
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
173381
- value = stringReplace(value, expr, ' ');
173382
- });
174124
+ value = _stripTemplateExpressions(value);
173383
174125
  }
173384
174126
  /* Is `value` valid for this attribute? */
173385
- const lcTag = transformCaseFunc(currentNode.nodeName);
173386
174127
  if (!_isValidAttribute(lcTag, lcName, value)) {
173387
174128
  _removeAttribute(name, currentNode);
173388
174129
  continue;
173389
174130
  }
173390
174131
  /* Handle attributes that require Trusted Types */
173391
- if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
173392
- if (namespaceURI) ; else {
173393
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
173394
- case 'TrustedHTML':
173395
- {
173396
- value = trustedTypesPolicy.createHTML(value);
173397
- break;
173398
- }
173399
- case 'TrustedScriptURL':
173400
- {
173401
- value = trustedTypesPolicy.createScriptURL(value);
173402
- break;
173403
- }
173404
- }
173405
- }
173406
- }
174132
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
173407
174133
  /* Handle invalid data-* attribute set by try-catching it */
173408
174134
  if (value !== initValue) {
173409
- try {
173410
- if (namespaceURI) {
173411
- currentNode.setAttributeNS(namespaceURI, name, value);
173412
- } else {
173413
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
173414
- currentNode.setAttribute(name, value);
173415
- }
173416
- if (_isClobbered(currentNode)) {
173417
- _forceRemove(currentNode);
173418
- } else {
173419
- arrayPop(DOMPurify.removed);
173420
- }
173421
- } catch (_) {
173422
- _removeAttribute(name, currentNode);
173423
- }
174135
+ _setAttributeValue(currentNode, name, namespaceURI, value);
173424
174136
  }
173425
174137
  }
173426
174138
  /* Execute a hook if present */
@@ -173443,14 +174155,133 @@ function createDOMPurify() {
173443
174155
  _sanitizeElements(shadowNode);
173444
174156
  /* Check attributes next */
173445
174157
  _sanitizeAttributes(shadowNode);
173446
- /* Deep shadow DOM detected */
173447
- 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)) {
173448
174163
  _sanitizeShadowDOM2(shadowNode.content);
173449
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
+ }
173450
174183
  }
173451
174184
  /* Execute a hook if present */
173452
174185
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
173453
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
+ };
173454
174285
  // eslint-disable-next-line complexity
173455
174286
  DOMPurify.sanitize = function (dirty) {
173456
174287
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -173467,13 +174298,9 @@ function createDOMPurify() {
173467
174298
  }
173468
174299
  /* Stringify, in case dirty is an object */
173469
174300
  if (typeof dirty !== 'string' && !_isNode(dirty)) {
173470
- if (typeof dirty.toString === 'function') {
173471
- dirty = dirty.toString();
173472
- if (typeof dirty !== 'string') {
173473
- throw typeErrorCreate('dirty is not a string, aborting');
173474
- }
173475
- } else {
173476
- 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');
173477
174304
  }
173478
174305
  }
173479
174306
  /* Return dirty HTML if DOMPurify cannot run */
@@ -173481,24 +174308,78 @@ function createDOMPurify() {
173481
174308
  return dirty;
173482
174309
  }
173483
174310
  /* Assign config vars */
173484
- 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 {
173485
174319
  _parseConfig(cfg);
173486
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
+ }
173487
174336
  /* Clean up removed elements */
173488
174337
  DOMPurify.removed = [];
173489
- /* Check if dirty is correctly typed for IN_PLACE */
173490
- if (typeof dirty === 'string') {
173491
- IN_PLACE = false;
173492
- }
173493
- if (IN_PLACE) {
173494
- /* Do some early pre-sanitization to avoid unsafe root nodes */
173495
- if (dirty.nodeName) {
173496
- 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);
173497
174354
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
173498
174355
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
173499
174356
  }
173500
174357
  }
173501
- } 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)) {
173502
174383
  /* If dirty is a DOM element, append to an empty document to avoid
173503
174384
  elements being stripped by the parser */
173504
174385
  body = _initDocument('<!---->');
@@ -173512,12 +174393,18 @@ function createDOMPurify() {
173512
174393
  // eslint-disable-next-line unicorn/prefer-dom-node-append
173513
174394
  body.appendChild(importedNode);
173514
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);
173515
174402
  } else {
173516
174403
  /* Exit directly if we have nothing to do */
173517
174404
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
173518
174405
  // eslint-disable-next-line unicorn/prefer-includes
173519
174406
  dirty.indexOf('<') === -1) {
173520
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
174407
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
173521
174408
  }
173522
174409
  /* Initialize the document to work on */
173523
174410
  body = _initDocument(dirty);
@@ -173531,31 +174418,59 @@ function createDOMPurify() {
173531
174418
  _forceRemove(body.firstChild);
173532
174419
  }
173533
174420
  /* Get node iterator */
173534
- const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
173535
- /* Now start iterating over the created document */
173536
- while (currentNode = nodeIterator.nextNode()) {
173537
- /* Sanitize tags and elements */
173538
- _sanitizeElements(currentNode);
173539
- /* Check attributes next */
173540
- _sanitizeAttributes(currentNode);
173541
- /* Shadow DOM detected, sanitize it */
173542
- if (currentNode.content instanceof DocumentFragment) {
173543
- _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);
173544
174448
  }
174449
+ throw error;
173545
174450
  }
173546
174451
  /* If we sanitized `dirty` in-place, return it. */
173547
- 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
+ }
173548
174468
  return dirty;
173549
174469
  }
173550
174470
  /* Return sanitized string or DOM */
173551
174471
  if (RETURN_DOM) {
173552
174472
  if (SAFE_FOR_TEMPLATES) {
173553
- body.normalize();
173554
- let html = body.innerHTML;
173555
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
173556
- html = stringReplace(html, expr, ' ');
173557
- });
173558
- body.innerHTML = html;
174473
+ _scrubTemplateExpressions2(body);
173559
174474
  }
173560
174475
  if (RETURN_DOM_FRAGMENT) {
173561
174476
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -173585,20 +174500,28 @@ function createDOMPurify() {
173585
174500
  }
173586
174501
  /* Sanitize final string template-safe */
173587
174502
  if (SAFE_FOR_TEMPLATES) {
173588
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
173589
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
173590
- });
174503
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
173591
174504
  }
173592
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
174505
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
173593
174506
  };
173594
174507
  DOMPurify.setConfig = function () {
173595
174508
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
173596
174509
  _parseConfig(cfg);
173597
174510
  SET_CONFIG = true;
174511
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
174512
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
173598
174513
  };
173599
174514
  DOMPurify.clearConfig = function () {
173600
174515
  CONFIG = null;
173601
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 = '';
173602
174525
  };
173603
174526
  DOMPurify.isValidAttribute = function (tag, attr, value) {
173604
174527
  /* Initialize shared config vars if necessary. */
@@ -173613,9 +174536,19 @@ function createDOMPurify() {
173613
174536
  if (typeof hookFunction !== 'function') {
173614
174537
  return;
173615
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
+ }
173616
174546
  arrayPush(hooks[entryPoint], hookFunction);
173617
174547
  };
173618
174548
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
174549
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
174550
+ return undefined;
174551
+ }
173619
174552
  if (hookFunction !== undefined) {
173620
174553
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
173621
174554
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -173623,6 +174556,9 @@ function createDOMPurify() {
173623
174556
  return arrayPop(hooks[entryPoint]);
173624
174557
  };
173625
174558
  DOMPurify.removeHooks = function (entryPoint) {
174559
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
174560
+ return;
174561
+ }
173626
174562
  hooks[entryPoint] = [];
173627
174563
  };
173628
174564
  DOMPurify.removeAllHooks = function () {