@microsoft/omnichannel-chat-widget 2.0.1-main.38e7390 → 2.0.1-main.74b9796

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,6 +81,7 @@ This hotfix still declares these vulnerable transitive versions:
81
81
  | `swiper` | `8.4.7` |
82
82
  | `valibot` | `1.1.0` |
83
83
  | `uuid` | `3.4.0`, `8.3.2`, and `9.0.1` |
84
+ | `deepmerge` (via `sanitize-html@2.14.0`) | `4.3.1` |
84
85
 
85
86
  The consumer audit permits only the reviewed advisories from this hotfix. It rejects each new advisory.
86
87
 
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.initWebChatComposer = void 0;
6
+ exports.initWebChatComposer = exports.createScreenReaderMarkdownRenderer = void 0;
7
7
  var _Constants = require("../../../common/Constants");
8
8
  var _TelemetryConstants = require("../../../common/telemetry/TelemetryConstants");
9
9
  var _utils = require("../../../common/utils");
@@ -20,6 +20,7 @@ var _attachmentSentAnnouncementMiddleware = _interopRequireDefault(require("../.
20
20
  var _channelDataMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/channelDataMiddleware"));
21
21
  var _activityMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware");
22
22
  var _activityStatusMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityStatusMiddleware");
23
+ var _attachmentForScreenReaderMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentForScreenReaderMiddleware");
23
24
  var _attachmentMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentMiddleware");
24
25
  var _attachmentUploadValidatorMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware"));
25
26
  var _avatarMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/avatarMiddleware");
@@ -48,7 +49,34 @@ var _htmlTextMiddleware = _interopRequireDefault(require("../../webchatcontainer
48
49
  var _preProcessingMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/preProcessingMiddleware"));
49
50
  var _sanitizationMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/sanitizationMiddleware"));
50
51
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
52
+ const markdownSanitizeConfig = {
53
+ FORBID_TAGS: ["form", "button", "script", "div", "input"],
54
+ FORBID_ATTR: ["action"],
55
+ ADD_ATTR: ["target"]
56
+ };
57
+
58
+ /**
59
+ * Renders Markdown for the screen reader text of Adaptive Cards, from the same pieces as the visible card:
60
+ * the host renderMarkdown when set, the hyperlink text override, and the same sanitization.
61
+ * Markdown is always formatted, also when the widget shows Markdown unformatted, so that screen readers do not read the syntax.
62
+ * Styles are removed with their content because the card does not show them.
63
+ */
64
+ const createScreenReaderMarkdownRenderer = webChatContainerProps => {
65
+ var _webChatContainerProp;
66
+ const hostRenderMarkdown = webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp = webChatContainerProps.webChatProps) === null || _webChatContainerProp === void 0 ? void 0 : _webChatContainerProp.renderMarkdown;
67
+ const markdown = (0, _createMarkdown.createMarkdown)(false, (webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.disableNewLineMarkdownSupport) ?? _defaultWebChatContainerStatefulProps.defaultWebChatContainerStatefulProps.disableNewLineMarkdownSupport, webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.opensMarkdownLinksInSameTab);
68
+ const hyperlinkTextOverrideRenderer = new _HyperlinkTextOverrideRenderer.default((webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.hyperlinkTextOverride) ?? _defaultWebChatContainerStatefulProps.defaultWebChatContainerStatefulProps.hyperlinkTextOverride);
69
+ return text => {
70
+ const html = hostRenderMarkdown ? hostRenderMarkdown(text) : markdown.render(text);
71
+ return _dompurify.default.sanitize(hyperlinkTextOverrideRenderer.render(html), {
72
+ ...markdownSanitizeConfig,
73
+ FORBID_TAGS: [...markdownSanitizeConfig.FORBID_TAGS, "style"]
74
+ });
75
+ };
76
+ };
77
+
51
78
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ exports.createScreenReaderMarkdownRenderer = createScreenReaderMarkdownRenderer;
52
80
  const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) => {
53
81
  var _props$webChatContain, _props$webChatContain2, _props$webChatContain3, _props$webChatContain4, _props$webChatContain5, _props$webChatContain6, _state$domainStates$l4, _props$webChatContain0, _state$domainStates$r, _state$domainStates$r2, _props$webChatContain1, _state$domainStates$r3, _props$webChatContain10, _state$domainStates$l5, _props$webChatContain11, _props$webChatContain12, _state$domainStates$r4, _state$domainStates$r5, _props$webChatContain13, _defaultWebChatContai, _props$webChatContain14, _defaultWebChatContai2, _props$webChatContain15, _props$webChatContain16, _props$webChatContain17;
54
82
  // Add a hook to make all links open a new window
@@ -154,12 +182,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
154
182
  });
155
183
 
156
184
  // EXISTING sanitization (continues to work as before)
157
- const config = {
158
- FORBID_TAGS: ["form", "button", "script", "div", "input"],
159
- FORBID_ATTR: ["action"],
160
- ADD_ATTR: ["target"]
161
- };
162
- text = _dompurify.default.sanitize(text, config);
185
+ text = _dompurify.default.sanitize(text, markdownSanitizeConfig);
163
186
 
164
187
  // MONITOR-ONLY: Test what the stricter allowlist would remove (Phase 1)
165
188
  // This does NOT modify the text, only logs telemetry
@@ -363,6 +386,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
363
386
  store: webChatStore,
364
387
  activityMiddleware: (_props$webChatContain0 = props.webChatContainerProps) !== null && _props$webChatContain0 !== void 0 && (_props$webChatContain0 = _props$webChatContain0.renderingMiddlewareProps) !== null && _props$webChatContain0 !== void 0 && _props$webChatContain0.disableActivityMiddleware ? undefined : (0, _activityMiddleware.createActivityMiddleware)(renderMarkdown, (_state$domainStates$r = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r === void 0 ? void 0 : _state$domainStates$r.systemMessageStyleProps, (_state$domainStates$r2 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r2 === void 0 ? void 0 : _state$domainStates$r2.userMessageStyleProps, localizedTexts),
365
388
  attachmentMiddleware: (_props$webChatContain1 = props.webChatContainerProps) !== null && _props$webChatContain1 !== void 0 && (_props$webChatContain1 = _props$webChatContain1.renderingMiddlewareProps) !== null && _props$webChatContain1 !== void 0 && _props$webChatContain1.disableAttachmentMiddleware ? undefined : (0, _attachmentMiddleware.createAttachmentMiddleware)(((_state$domainStates$r3 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r3 === void 0 || (_state$domainStates$r3 = _state$domainStates$r3.attachmentProps) === null || _state$domainStates$r3 === void 0 ? void 0 : _state$domainStates$r3.enableInlinePlaying) ?? _defaultAttachmentProps.defaultAttachmentProps.enableInlinePlaying),
389
+ attachmentForScreenReaderMiddleware: (0, _attachmentForScreenReaderMiddleware.createAttachmentForScreenReaderMiddleware)(createScreenReaderMarkdownRenderer(props.webChatContainerProps)),
366
390
  activityStatusMiddleware: (_props$webChatContain10 = props.webChatContainerProps) !== null && _props$webChatContain10 !== void 0 && (_props$webChatContain10 = _props$webChatContain10.renderingMiddlewareProps) !== null && _props$webChatContain10 !== void 0 && _props$webChatContain10.disableActivityStatusMiddleware ? undefined : (0, _activityStatusMiddleware.createActivityStatusMiddleware)((0, _omnichannelChatSdk.getLocaleStringFromId)((_state$domainStates$l5 = state.domainStates.liveChatConfig) === null || _state$domainStates$l5 === void 0 || (_state$domainStates$l5 = _state$domainStates$l5.ChatWidgetLanguage) === null || _state$domainStates$l5 === void 0 ? void 0 : _state$domainStates$l5.msdyn_localeid)),
367
391
  toastMiddleware: (_props$webChatContain11 = props.webChatContainerProps) !== null && _props$webChatContain11 !== void 0 && (_props$webChatContain11 = _props$webChatContain11.renderingMiddlewareProps) !== null && _props$webChatContain11 !== void 0 && _props$webChatContain11.disableToastMiddleware ? undefined : (0, _toastMiddleware.createToastMiddleware)(props.notificationPaneProps, endChat),
368
392
  renderMarkdown,
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getAdaptiveCardSpeak = exports.createAttachmentForScreenReaderMiddleware = void 0;
7
+ var _react = _interopRequireWildcard(require("react"));
8
+ var _Constants = require("../../../../../common/Constants");
9
+ var _botframeworkWebchat = require("botframework-webchat");
10
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
11
+ function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
12
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
13
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); 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; } }
14
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
15
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
16
+ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } /* eslint-disable @typescript-eslint/no-explicit-any */ /******
17
+ * AttachmentForScreenReaderMiddleware
18
+ *
19
+ * Web Chat reads an Adaptive Card to screen readers only from the card's `speak` property.
20
+ * A card without `speak` is announced as "A card" with no content.
21
+ * This middleware sets `speak` from the visible text of the card, then lets Web Chat render it as usual.
22
+ ******/
23
+ const useAdaptiveCardsPackage = _botframeworkWebchat.hooks.useAdaptiveCardsPackage,
24
+ useStyleOptions = _botframeworkWebchat.hooks.useStyleOptions;
25
+ const ADAPTIVE_CARD_CONTENT_TYPE = "application/vnd.microsoft.card.adaptive";
26
+ const HTML_BLOCK_END = /<\/(p|li|h[1-6]|div|tr|td|th|blockquote|pre)>|<br\s*\/?>/gi;
27
+ // The "opens in a new window" icon of each link is left out: it describes the link target, not the content of the message.
28
+ const htmlToText = html => {
29
+ const body = new DOMParser().parseFromString(html.replace(HTML_BLOCK_END, "$& "), "text/html").body;
30
+ body.querySelectorAll(`script, style, template, img.${_Constants.Constants.OpenLinkIconCssClass}`).forEach(element => element.remove());
31
+ body.querySelectorAll("img").forEach(image => image.replaceWith(` ${image.getAttribute("alt") ?? ""} `));
32
+ return (body.textContent ?? "").replace(/\s+/g, " ").trim();
33
+ };
34
+
35
+ // Renders the element as the visible card does: Adaptive Cards formats {{DATE()}} and {{TIME()}}, then the Markdown is rendered.
36
+ // Web Chat sets the static Markdown handler before each visible render, and this restores the previous handler after rendering.
37
+ const getRenderedText = (element, AdaptiveCard, renderMarkdownAsHTML) => {
38
+ const previousOnProcessMarkdown = AdaptiveCard.onProcessMarkdown;
39
+ try {
40
+ AdaptiveCard.onProcessMarkdown = (text, result) => {
41
+ result.outputHtml = renderMarkdownAsHTML(text);
42
+ result.didProcess = true;
43
+ };
44
+ const renderedElement = element.render();
45
+ return renderedElement ? htmlToText(renderedElement.outerHTML) : "";
46
+ } catch {
47
+ var _element$asString;
48
+ return ((_element$asString = element.asString) === null || _element$asString === void 0 ? void 0 : _element$asString.call(element)) ?? "";
49
+ } finally {
50
+ AdaptiveCard.onProcessMarkdown = previousOnProcessMarkdown;
51
+ }
52
+ };
53
+
54
+ // Collects the visible text of the card in document order, after the parser has applied element fallbacks.
55
+ // Inputs and actions are not collected because Web Chat already lists them after the card text.
56
+ const getAdaptiveCardSpeak = (content, adaptiveCardsPackage, renderMarkdownAsHTML, adaptiveCardsParserMaxVersion) => {
57
+ const AdaptiveCard = adaptiveCardsPackage.AdaptiveCard,
58
+ FactSet = adaptiveCardsPackage.FactSet,
59
+ Image = adaptiveCardsPackage.Image,
60
+ RichTextBlock = adaptiveCardsPackage.RichTextBlock,
61
+ SerializationContext = adaptiveCardsPackage.SerializationContext,
62
+ TextBlock = adaptiveCardsPackage.TextBlock,
63
+ Version = adaptiveCardsPackage.Version;
64
+ const maxVersion = adaptiveCardsParserMaxVersion ? Version.parse(adaptiveCardsParserMaxVersion, new SerializationContext()) : undefined;
65
+ const card = new AdaptiveCard();
66
+ const texts = [];
67
+ card.parse(content, new SerializationContext(maxVersion !== null && maxVersion !== void 0 && maxVersion.isValid ? maxVersion : undefined));
68
+ const getFactText = (factSet, text) => {
69
+ const textBlock = new TextBlock(text);
70
+ textBlock.setParent(factSet);
71
+ return getRenderedText(textBlock, AdaptiveCard, renderMarkdownAsHTML);
72
+ };
73
+ const walk = element => {
74
+ if (!(element !== null && element !== void 0 && element.isVisible)) {
75
+ return;
76
+ }
77
+ if (element instanceof TextBlock || element instanceof RichTextBlock) {
78
+ texts.push(getRenderedText(element, AdaptiveCard, renderMarkdownAsHTML));
79
+ } else if (element instanceof FactSet) {
80
+ element.facts.forEach(_ref => {
81
+ let name = _ref.name,
82
+ value = _ref.value;
83
+ texts.push([name, value].map(text => text ? getFactText(element, text) : "").filter(Boolean).join(": "));
84
+ });
85
+ } else if (element instanceof Image) {
86
+ texts.push(element.altText);
87
+ }
88
+ if (element.getItemAt && element.getItemCount) {
89
+ for (let index = 0, count = element.getItemCount(); index < count; index++) {
90
+ walk(element.getItemAt(index));
91
+ }
92
+ }
93
+ };
94
+ walk(card);
95
+ return texts.filter(text => typeof text === "string" && text).join(" ").replace(/\s+/g, " ").trim();
96
+ };
97
+ exports.getAdaptiveCardSpeak = getAdaptiveCardSpeak;
98
+ const AdaptiveCardForScreenReader = _ref2 => {
99
+ let args = _ref2.args,
100
+ next = _ref2.next,
101
+ renderMarkdownAsHTML = _ref2.renderMarkdownAsHTML;
102
+ const _useAdaptiveCardsPack = useAdaptiveCardsPackage(),
103
+ _useAdaptiveCardsPack2 = _slicedToArray(_useAdaptiveCardsPack, 1),
104
+ adaptiveCardsPackage = _useAdaptiveCardsPack2[0];
105
+ const _useStyleOptions = useStyleOptions(),
106
+ _useStyleOptions2 = _slicedToArray(_useStyleOptions, 1),
107
+ adaptiveCardsParserMaxVersion = _useStyleOptions2[0].adaptiveCardsParserMaxVersion;
108
+ const _args = _slicedToArray(args, 1),
109
+ options = _args[0];
110
+ const attachment = options.attachment;
111
+
112
+ // The widget creates a new Markdown renderer on each render with the same settings, so the text is computed once for each attachment.
113
+ const contentWithSpeak = (0, _react.useMemo)(() => {
114
+ try {
115
+ const content = typeof attachment.content === "string" ? JSON.parse(attachment.content) : attachment.content;
116
+ if (!content || typeof content !== "object" || content.speak) {
117
+ return undefined;
118
+ }
119
+ const speak = getAdaptiveCardSpeak(content, adaptiveCardsPackage, renderMarkdownAsHTML, adaptiveCardsParserMaxVersion);
120
+ return speak ? {
121
+ ...content,
122
+ speak
123
+ } : undefined;
124
+ } catch {
125
+ return undefined;
126
+ }
127
+ }, [adaptiveCardsPackage, adaptiveCardsParserMaxVersion, attachment]);
128
+ const render = contentWithSpeak ? next({
129
+ ...options,
130
+ attachment: {
131
+ ...attachment,
132
+ content: contentWithSpeak
133
+ }
134
+ }, ...args.slice(1)) : next(...args);
135
+ return typeof render === "function" ? render() : null;
136
+ };
137
+ const createAttachmentForScreenReaderMiddleware = renderMarkdownAsHTML => {
138
+ const attachmentForScreenReaderMiddleware = () => next => function () {
139
+ var _args$;
140
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
141
+ args[_key] = arguments[_key];
142
+ }
143
+ const attachment = (_args$ = args[0]) === null || _args$ === void 0 ? void 0 : _args$.attachment;
144
+
145
+ // Web Chat renders nothing for a card without content, so it is left to Web Chat.
146
+ if ((attachment === null || attachment === void 0 ? void 0 : attachment.contentType) !== ADAPTIVE_CARD_CONTENT_TYPE || !attachment.content) {
147
+ return next(...args);
148
+ }
149
+
150
+ // eslint-disable-next-line react/display-name
151
+ return () => /*#__PURE__*/_react.default.createElement(AdaptiveCardForScreenReader, {
152
+ args: args,
153
+ next: next,
154
+ renderMarkdownAsHTML: renderMarkdownAsHTML
155
+ });
156
+ };
157
+ return attachmentForScreenReaderMiddleware;
158
+ };
159
+ exports.createAttachmentForScreenReaderMiddleware = createAttachmentForScreenReaderMiddleware;
@@ -15,6 +15,7 @@ import createAttachmentSentAnnouncementMiddleware from "../../webchatcontainerst
15
15
  import channelDataMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/channelDataMiddleware";
16
16
  import { createActivityMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware";
17
17
  import { createActivityStatusMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityStatusMiddleware";
18
+ import { createAttachmentForScreenReaderMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentForScreenReaderMiddleware";
18
19
  import { createAttachmentMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentMiddleware";
19
20
  import createAttachmentUploadValidatorMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware";
20
21
  import { createAvatarMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/avatarMiddleware";
@@ -42,6 +43,31 @@ import htmlPlayerMiddleware from "../../webchatcontainerstateful/webchatcontroll
42
43
  import htmlTextMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/htmlTextMiddleware";
43
44
  import preProcessingMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/preProcessingMiddleware";
44
45
  import sanitizationMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/sanitizationMiddleware";
46
+ const markdownSanitizeConfig = {
47
+ FORBID_TAGS: ["form", "button", "script", "div", "input"],
48
+ FORBID_ATTR: ["action"],
49
+ ADD_ATTR: ["target"]
50
+ };
51
+
52
+ /**
53
+ * Renders Markdown for the screen reader text of Adaptive Cards, from the same pieces as the visible card:
54
+ * the host renderMarkdown when set, the hyperlink text override, and the same sanitization.
55
+ * Markdown is always formatted, also when the widget shows Markdown unformatted, so that screen readers do not read the syntax.
56
+ * Styles are removed with their content because the card does not show them.
57
+ */
58
+ export const createScreenReaderMarkdownRenderer = webChatContainerProps => {
59
+ var _webChatContainerProp;
60
+ const hostRenderMarkdown = webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp = webChatContainerProps.webChatProps) === null || _webChatContainerProp === void 0 ? void 0 : _webChatContainerProp.renderMarkdown;
61
+ const markdown = createMarkdown(false, (webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.disableNewLineMarkdownSupport) ?? defaultWebChatContainerStatefulProps.disableNewLineMarkdownSupport, webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.opensMarkdownLinksInSameTab);
62
+ const hyperlinkTextOverrideRenderer = new HyperlinkTextOverrideRenderer((webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.hyperlinkTextOverride) ?? defaultWebChatContainerStatefulProps.hyperlinkTextOverride);
63
+ return text => {
64
+ const html = hostRenderMarkdown ? hostRenderMarkdown(text) : markdown.render(text);
65
+ return DOMPurify.sanitize(hyperlinkTextOverrideRenderer.render(html), {
66
+ ...markdownSanitizeConfig,
67
+ FORBID_TAGS: [...markdownSanitizeConfig.FORBID_TAGS, "style"]
68
+ });
69
+ };
70
+ };
45
71
 
46
72
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
73
  export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) => {
@@ -149,12 +175,7 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
149
175
  });
150
176
 
151
177
  // EXISTING sanitization (continues to work as before)
152
- const config = {
153
- FORBID_TAGS: ["form", "button", "script", "div", "input"],
154
- FORBID_ATTR: ["action"],
155
- ADD_ATTR: ["target"]
156
- };
157
- text = DOMPurify.sanitize(text, config);
178
+ text = DOMPurify.sanitize(text, markdownSanitizeConfig);
158
179
 
159
180
  // MONITOR-ONLY: Test what the stricter allowlist would remove (Phase 1)
160
181
  // This does NOT modify the text, only logs telemetry
@@ -358,6 +379,7 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
358
379
  store: webChatStore,
359
380
  activityMiddleware: (_props$webChatContain0 = props.webChatContainerProps) !== null && _props$webChatContain0 !== void 0 && (_props$webChatContain0 = _props$webChatContain0.renderingMiddlewareProps) !== null && _props$webChatContain0 !== void 0 && _props$webChatContain0.disableActivityMiddleware ? undefined : createActivityMiddleware(renderMarkdown, (_state$domainStates$r = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r === void 0 ? void 0 : _state$domainStates$r.systemMessageStyleProps, (_state$domainStates$r2 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r2 === void 0 ? void 0 : _state$domainStates$r2.userMessageStyleProps, localizedTexts),
360
381
  attachmentMiddleware: (_props$webChatContain1 = props.webChatContainerProps) !== null && _props$webChatContain1 !== void 0 && (_props$webChatContain1 = _props$webChatContain1.renderingMiddlewareProps) !== null && _props$webChatContain1 !== void 0 && _props$webChatContain1.disableAttachmentMiddleware ? undefined : createAttachmentMiddleware(((_state$domainStates$r3 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r3 === void 0 || (_state$domainStates$r3 = _state$domainStates$r3.attachmentProps) === null || _state$domainStates$r3 === void 0 ? void 0 : _state$domainStates$r3.enableInlinePlaying) ?? defaultAttachmentProps.enableInlinePlaying),
382
+ attachmentForScreenReaderMiddleware: createAttachmentForScreenReaderMiddleware(createScreenReaderMarkdownRenderer(props.webChatContainerProps)),
361
383
  activityStatusMiddleware: (_props$webChatContain10 = props.webChatContainerProps) !== null && _props$webChatContain10 !== void 0 && (_props$webChatContain10 = _props$webChatContain10.renderingMiddlewareProps) !== null && _props$webChatContain10 !== void 0 && _props$webChatContain10.disableActivityStatusMiddleware ? undefined : createActivityStatusMiddleware(getLocaleStringFromId((_state$domainStates$l5 = state.domainStates.liveChatConfig) === null || _state$domainStates$l5 === void 0 || (_state$domainStates$l5 = _state$domainStates$l5.ChatWidgetLanguage) === null || _state$domainStates$l5 === void 0 ? void 0 : _state$domainStates$l5.msdyn_localeid)),
362
384
  toastMiddleware: (_props$webChatContain11 = props.webChatContainerProps) !== null && _props$webChatContain11 !== void 0 && (_props$webChatContain11 = _props$webChatContain11.renderingMiddlewareProps) !== null && _props$webChatContain11 !== void 0 && _props$webChatContain11.disableToastMiddleware ? undefined : createToastMiddleware(props.notificationPaneProps, endChat),
363
385
  renderMarkdown,
@@ -0,0 +1,153 @@
1
+ function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
2
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
3
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); 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; } }
4
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
+ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ /******
9
+ * AttachmentForScreenReaderMiddleware
10
+ *
11
+ * Web Chat reads an Adaptive Card to screen readers only from the card's `speak` property.
12
+ * A card without `speak` is announced as "A card" with no content.
13
+ * This middleware sets `speak` from the visible text of the card, then lets Web Chat render it as usual.
14
+ ******/
15
+
16
+ import React, { useMemo } from "react";
17
+ import { Constants } from "../../../../../common/Constants";
18
+ import { hooks } from "botframework-webchat";
19
+ const useAdaptiveCardsPackage = hooks.useAdaptiveCardsPackage,
20
+ useStyleOptions = hooks.useStyleOptions;
21
+ const ADAPTIVE_CARD_CONTENT_TYPE = "application/vnd.microsoft.card.adaptive";
22
+ const HTML_BLOCK_END = /<\/(p|li|h[1-6]|div|tr|td|th|blockquote|pre)>|<br\s*\/?>/gi;
23
+ // The "opens in a new window" icon of each link is left out: it describes the link target, not the content of the message.
24
+ const htmlToText = html => {
25
+ const body = new DOMParser().parseFromString(html.replace(HTML_BLOCK_END, "$& "), "text/html").body;
26
+ body.querySelectorAll(`script, style, template, img.${Constants.OpenLinkIconCssClass}`).forEach(element => element.remove());
27
+ body.querySelectorAll("img").forEach(image => image.replaceWith(` ${image.getAttribute("alt") ?? ""} `));
28
+ return (body.textContent ?? "").replace(/\s+/g, " ").trim();
29
+ };
30
+
31
+ // Renders the element as the visible card does: Adaptive Cards formats {{DATE()}} and {{TIME()}}, then the Markdown is rendered.
32
+ // Web Chat sets the static Markdown handler before each visible render, and this restores the previous handler after rendering.
33
+ const getRenderedText = (element, AdaptiveCard, renderMarkdownAsHTML) => {
34
+ const previousOnProcessMarkdown = AdaptiveCard.onProcessMarkdown;
35
+ try {
36
+ AdaptiveCard.onProcessMarkdown = (text, result) => {
37
+ result.outputHtml = renderMarkdownAsHTML(text);
38
+ result.didProcess = true;
39
+ };
40
+ const renderedElement = element.render();
41
+ return renderedElement ? htmlToText(renderedElement.outerHTML) : "";
42
+ } catch {
43
+ var _element$asString;
44
+ return ((_element$asString = element.asString) === null || _element$asString === void 0 ? void 0 : _element$asString.call(element)) ?? "";
45
+ } finally {
46
+ AdaptiveCard.onProcessMarkdown = previousOnProcessMarkdown;
47
+ }
48
+ };
49
+
50
+ // Collects the visible text of the card in document order, after the parser has applied element fallbacks.
51
+ // Inputs and actions are not collected because Web Chat already lists them after the card text.
52
+ export const getAdaptiveCardSpeak = (content, adaptiveCardsPackage, renderMarkdownAsHTML, adaptiveCardsParserMaxVersion) => {
53
+ const AdaptiveCard = adaptiveCardsPackage.AdaptiveCard,
54
+ FactSet = adaptiveCardsPackage.FactSet,
55
+ Image = adaptiveCardsPackage.Image,
56
+ RichTextBlock = adaptiveCardsPackage.RichTextBlock,
57
+ SerializationContext = adaptiveCardsPackage.SerializationContext,
58
+ TextBlock = adaptiveCardsPackage.TextBlock,
59
+ Version = adaptiveCardsPackage.Version;
60
+ const maxVersion = adaptiveCardsParserMaxVersion ? Version.parse(adaptiveCardsParserMaxVersion, new SerializationContext()) : undefined;
61
+ const card = new AdaptiveCard();
62
+ const texts = [];
63
+ card.parse(content, new SerializationContext(maxVersion !== null && maxVersion !== void 0 && maxVersion.isValid ? maxVersion : undefined));
64
+ const getFactText = (factSet, text) => {
65
+ const textBlock = new TextBlock(text);
66
+ textBlock.setParent(factSet);
67
+ return getRenderedText(textBlock, AdaptiveCard, renderMarkdownAsHTML);
68
+ };
69
+ const walk = element => {
70
+ if (!(element !== null && element !== void 0 && element.isVisible)) {
71
+ return;
72
+ }
73
+ if (element instanceof TextBlock || element instanceof RichTextBlock) {
74
+ texts.push(getRenderedText(element, AdaptiveCard, renderMarkdownAsHTML));
75
+ } else if (element instanceof FactSet) {
76
+ element.facts.forEach(_ref => {
77
+ let name = _ref.name,
78
+ value = _ref.value;
79
+ texts.push([name, value].map(text => text ? getFactText(element, text) : "").filter(Boolean).join(": "));
80
+ });
81
+ } else if (element instanceof Image) {
82
+ texts.push(element.altText);
83
+ }
84
+ if (element.getItemAt && element.getItemCount) {
85
+ for (let index = 0, count = element.getItemCount(); index < count; index++) {
86
+ walk(element.getItemAt(index));
87
+ }
88
+ }
89
+ };
90
+ walk(card);
91
+ return texts.filter(text => typeof text === "string" && text).join(" ").replace(/\s+/g, " ").trim();
92
+ };
93
+ const AdaptiveCardForScreenReader = _ref2 => {
94
+ let args = _ref2.args,
95
+ next = _ref2.next,
96
+ renderMarkdownAsHTML = _ref2.renderMarkdownAsHTML;
97
+ const _useAdaptiveCardsPack = useAdaptiveCardsPackage(),
98
+ _useAdaptiveCardsPack2 = _slicedToArray(_useAdaptiveCardsPack, 1),
99
+ adaptiveCardsPackage = _useAdaptiveCardsPack2[0];
100
+ const _useStyleOptions = useStyleOptions(),
101
+ _useStyleOptions2 = _slicedToArray(_useStyleOptions, 1),
102
+ adaptiveCardsParserMaxVersion = _useStyleOptions2[0].adaptiveCardsParserMaxVersion;
103
+ const _args = _slicedToArray(args, 1),
104
+ options = _args[0];
105
+ const attachment = options.attachment;
106
+
107
+ // The widget creates a new Markdown renderer on each render with the same settings, so the text is computed once for each attachment.
108
+ const contentWithSpeak = useMemo(() => {
109
+ try {
110
+ const content = typeof attachment.content === "string" ? JSON.parse(attachment.content) : attachment.content;
111
+ if (!content || typeof content !== "object" || content.speak) {
112
+ return undefined;
113
+ }
114
+ const speak = getAdaptiveCardSpeak(content, adaptiveCardsPackage, renderMarkdownAsHTML, adaptiveCardsParserMaxVersion);
115
+ return speak ? {
116
+ ...content,
117
+ speak
118
+ } : undefined;
119
+ } catch {
120
+ return undefined;
121
+ }
122
+ }, [adaptiveCardsPackage, adaptiveCardsParserMaxVersion, attachment]);
123
+ const render = contentWithSpeak ? next({
124
+ ...options,
125
+ attachment: {
126
+ ...attachment,
127
+ content: contentWithSpeak
128
+ }
129
+ }, ...args.slice(1)) : next(...args);
130
+ return typeof render === "function" ? render() : null;
131
+ };
132
+ export const createAttachmentForScreenReaderMiddleware = renderMarkdownAsHTML => {
133
+ const attachmentForScreenReaderMiddleware = () => next => function () {
134
+ var _args$;
135
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
136
+ args[_key] = arguments[_key];
137
+ }
138
+ const attachment = (_args$ = args[0]) === null || _args$ === void 0 ? void 0 : _args$.attachment;
139
+
140
+ // Web Chat renders nothing for a card without content, so it is left to Web Chat.
141
+ if ((attachment === null || attachment === void 0 ? void 0 : attachment.contentType) !== ADAPTIVE_CARD_CONTENT_TYPE || !attachment.content) {
142
+ return next(...args);
143
+ }
144
+
145
+ // eslint-disable-next-line react/display-name
146
+ return () => /*#__PURE__*/React.createElement(AdaptiveCardForScreenReader, {
147
+ args: args,
148
+ next: next,
149
+ renderMarkdownAsHTML: renderMarkdownAsHTML
150
+ });
151
+ };
152
+ return attachmentForScreenReaderMiddleware;
153
+ };
@@ -4,4 +4,12 @@ import { ILiveChatWidgetAction } from "../../../contexts/common/ILiveChatWidgetA
4
4
  import { ILiveChatWidgetContext } from "../../../contexts/common/ILiveChatWidgetContext";
5
5
  import { ILiveChatWidgetProps } from "../interfaces/ILiveChatWidgetProps";
6
6
  import { IWebChatProps } from "../../webchatcontainerstateful/interfaces/IWebChatProps";
7
+ import { IWebChatContainerStatefulProps } from "../../webchatcontainerstateful/interfaces/IWebChatContainerStatefulProps";
8
+ /**
9
+ * Renders Markdown for the screen reader text of Adaptive Cards, from the same pieces as the visible card:
10
+ * the host renderMarkdown when set, the hyperlink text override, and the same sanitization.
11
+ * Markdown is always formatted, also when the widget shows Markdown unformatted, so that screen readers do not read the syntax.
12
+ * Styles are removed with their content because the card does not show them.
13
+ */
14
+ export declare const createScreenReaderMarkdownRenderer: (webChatContainerProps?: IWebChatContainerStatefulProps) => (text: string) => string;
7
15
  export declare const initWebChatComposer: (props: ILiveChatWidgetProps, state: ILiveChatWidgetContext, dispatch: Dispatch<ILiveChatWidgetAction>, facadeChatSDK: FacadeChatSDK, endChat: any) => IWebChatProps;
@@ -0,0 +1,11 @@
1
+ /******
2
+ * AttachmentForScreenReaderMiddleware
3
+ *
4
+ * Web Chat reads an Adaptive Card to screen readers only from the card's `speak` property.
5
+ * A card without `speak` is announced as "A card" with no content.
6
+ * This middleware sets `speak` from the visible text of the card, then lets Web Chat render it as usual.
7
+ ******/
8
+ type RenderMarkdownAsHTML = (markdown: string) => string;
9
+ export declare const getAdaptiveCardSpeak: (content: any, adaptiveCardsPackage: any, renderMarkdownAsHTML: RenderMarkdownAsHTML, adaptiveCardsParserMaxVersion?: string) => string;
10
+ export declare const createAttachmentForScreenReaderMiddleware: (renderMarkdownAsHTML: RenderMarkdownAsHTML) => () => (next: any) => (...args: any[]) => any;
11
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/omnichannel-chat-widget",
3
- "version": "2.0.1-main.38e7390",
3
+ "version": "2.0.1-main.74b9796",
4
4
  "description": "Microsoft Omnichannel Chat Widget",
5
5
  "main": "lib/cjs/index.js",
6
6
  "types": "lib/types/index.d.ts",
@@ -93,18 +93,18 @@
93
93
  "@azure/core-tracing": "^1.2.0",
94
94
  "@microsoft/applicationinsights-web": "^3.3.6",
95
95
  "@microsoft/omnichannel-chat-components": "1.2.0",
96
- "@microsoft/omnichannel-chat-sdk": "2.0.1",
96
+ "@microsoft/omnichannel-chat-sdk": "2.0.2-main.a1ba517",
97
97
  "@opentelemetry/api": "^1.9.0",
98
98
  "abort-controller": "^3",
99
99
  "botframework-webchat": "4.18.1-hotfix.20260308.b15b405",
100
100
  "core-js-pure": "^3.42.0",
101
- "dompurify": "3.4.16",
102
- "markdown-it": "^14.3.0",
101
+ "dompurify": "3.4.17",
102
+ "markdown-it": "^14.3.3",
103
103
  "markdown-it-attrs": "^4.1.6",
104
104
  "markdown-it-for-inline": "^0.1.1",
105
105
  "md5-typescript": "^1.0.5",
106
106
  "rxjs": "5.5.12",
107
- "sanitize-html": "2.17.7",
107
+ "sanitize-html": "2.18.1",
108
108
  "simple-update-in": "2.2.0",
109
109
  "slack-markdown-it": "^1.0.5"
110
110
  },
@@ -164,7 +164,7 @@
164
164
  "**/@types/trusted-types": "2.0.7",
165
165
  "**/join-images": "1.1.5",
166
166
  "**/valibot": "1.4.2",
167
- "**/nanoid": "3.3.18"
167
+ "**/nanoid": "^3.3.20"
168
168
  },
169
169
  "jest": {
170
170
  "verbose": true,