Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .eslintrc.react.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ extends:
- plugin:react/recommended
- plugin:react-hooks/recommended

overrides:
- files:
- '__tests__/**/*.js'
- '**/*.spec.jsx'
- '**/*.spec.tsx'
- '**/*.test.jsx'
- '**/*.test.tsx'

env:
jest: true

rules:
'react/forbid-elements': off

parserOptions:
ecmaFeatures:
jsx: true
Expand All @@ -29,6 +43,10 @@ rules:
- error
- forbid:
- id
react/forbid-elements:
- error
- forbid:
- a
react/jsx-boolean-value:
- error
- always
Expand Down
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ overrides:
rules:
'@typescript-eslint/no-require-imports': off
no-magic-numbers: off
'react/forbid-elements': off

rules:
# Only list rules that are not in *:recommended set
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- `styleOptions.hideUploadButton` is being deprecated in favor of `styleOptions.disableFileUpload`. The option will be removed on or after 2027-07-14
- `botframework-directlinespeech-sdk` no longer ponyfill `AbortController`, it is supported by modern browsers, in PR [#5530](https://github.com/microsoft/BotFramework-WebChat/pull/5530)
- `activityMiddleware` is being deprecated in favor of [`polymiddleware`](./docs/MIDDLEWARE.md). It will be removed on or after 2027-08-16, related to PR [#5515](https://github.com/microsoft/BotFramework-WebChat/pull/5515)
- Root-level (unconnected) `Claim` entity is being deprecated, in PR [#5564](https://github.com/microsoft/BotFramework-WebChat/pull/5564), by [@compulim](https://github.com/compulim). It will be removed on or after 2027-08-29
- Use `entities[@id=""][@type="Message"].citation[@type="Claim"]` instead

### Added

Expand Down Expand Up @@ -116,6 +118,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- `@msinternal/botframework-webchat-api-middleware` for middleware branch of API package
- `@msinternal/botframework-webchat-debug-theme` package for enabling debugging scenarios
- `@msinternal/botframework-webchat-react-hooks` for helpers for React hooks
- Added link sanitization and ESLint rules, in PR [#5564](https://github.com/microsoft/BotFramework-WebChat/pull/5564), by [@compulim](https://github.com/compulim)

### Changed

Expand Down Expand Up @@ -291,6 +294,8 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- `useSuggestedActions()` hook is being deprecated in favor of the `useSuggestedActionsHooks().useSuggestedActions()` hook, in PR [#5489](https://github.com/microsoft/BotFramework-WebChat/pull/5489), by [@compulim](https://github.com/compulim)
- Fixed core internal import in legacy CommonJS environments, in [5509](https://github.com/microsoft/BotFramework-WebChat/pull/5509), by [@OEvgeny](https://github.com/OEvgeny)
- `activityMiddleware` is being deprecated in favor of [`polymiddleware`](./docs/MIDDLEWARE.md). It will be removed on or after 2027-08-16, related to PR [#5515](https://github.com/microsoft/BotFramework-WebChat/pull/5515)
- Root-level (unconnected) `Claim` entity is being deprecated, in PR [#5564](https://github.com/microsoft/BotFramework-WebChat/pull/5564), by [@compulim](https://github.com/compulim). It will be removed on or after 2027-08-29
- Use `entities[@id=""][@type="Message"].citation[@type="Claim"]` instead

### Samples

Expand Down
75 changes: 75 additions & 0 deletions __tests__/html2/citation/claimInterpreter/dangerousLink.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
</head>
<body>
<main id="webchat"></main>
<script>
run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();

WebChat.renderWebChat(
{
directLine,
store
},
document.getElementById('webchat')
);

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity({
entities: [
{
'@context': 'https://schema.org',
'@id': '',
'@type': 'Message',
type: 'https://schema.org/Message',
citation: [
{
'@id': ':_doesnt-care-1',
'@type': 'Claim',
appearance: {
'@type': 'DigitalDocument',
encodingFormat: 'application/octet-stream',
url: 'https://aka.ms/claim'
},
claimInterpreter: {
'@type': 'Project',
slogan: 'Surfaced with Azure OpenAI',
url: 'javascript:alert(1)'
},
position: '1'
}
]
}
],
text: `Fugiat excepteur anim irure consectetur ex nisi eu deserunt officia tempor eu et excepteur.[1]

[1]: https://aka.ms/claim
`,
type: 'message'
});

await host.snapshot('local');

const markdownElement = pageElements.activities()[0].querySelector('.webchat__text-content__markdown');
const markdownLinks = markdownElement.querySelectorAll('a');

// The javascript: shouldn't be a link.
expect(markdownLinks).toHaveLength(1);

expect(markdownLinks[0].getAttribute('href')).toBe('https://aka.ms/claim');

const claimInterpreterElement = pageElements.activities()[0].querySelector('.webchat__activity-status__originator');

expect(claimInterpreterElement).toHaveProperty('tagName', 'SPAN');
expect(claimInterpreterElement).toHaveProperty('textContent', 'Surfaced with Azure OpenAI');
});
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 98 additions & 0 deletions __tests__/html2/citation/markdownPreferredOverEntities.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script type="importmap">
{
"imports": {
"jest-mock": "https://esm.sh/jest-mock",
"react": "https://esm.sh/react@18",
"react-dom": "https://esm.sh/react-dom@18",
"react-dom/": "https://esm.sh/react-dom@18/"
}
}
</script>
<script type="module">
import React from 'react';
window.React = React;
</script>
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
</head>
<body>
<main id="webchat"></main>
<script type="module">
import { fn, spyOn } from 'jest-mock';

run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();

WebChat.renderWebChat(
{
directLine,
store
},
document.getElementById('webchat')
);

await pageConditions.uiConnected();

const consoleWarn = fn(console.log.bind(console));

spyOn(console, 'warn').mockImplementationOnce(consoleWarn);

await directLine.emulateIncomingActivity({
entities: [
{
'@context': 'https://schema.org',
'@id': '',
'@type': 'Message',
type: 'https://schema.org/Message',
citation: [
{
'@id': ':_doesnt-care-1',
'@type': 'Claim',
appearance: {
'@type': 'DigitalDocument',
encodingFormat: 'application/octet-stream',
url: 'https://aka.ms/bad-link'
},
position: '1'
}
]
}
],
text: `Ea officia[1] elit laboris[2] reprehenderit laborum elit ipsum qui eiusmod.

[1]: https://aka.ms/correct-link
[2]: javascript:alert(1)
`,
type: 'message'
});

expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn.mock.calls[0][0]).toBe(
'botframework-webchat: When "Message.citation[].url" is set in entities, it must match its corresponding URL in Markdown link reference definition'
);

await host.snapshot('local');

const markdownElement = pageElements.activities()[0].querySelector('.webchat__text-content__markdown');
const markdownClickableLinks = markdownElement.querySelectorAll('a[href]');

// The javascript: shouldn't be a link.
expect(markdownClickableLinks).toHaveLength(1);

expect(markdownClickableLinks[0].getAttribute('href')).toBe('https://aka.ms/correct-link');

const linkDefinitionItems = pageElements.linkDefinitions()[0].querySelectorAll('[role="listitem"] > *');

// THe javascript: link is gone in Markdown, should be ignored in citation as well.
expect(linkDefinitionItems).toHaveLength(1);

expect(linkDefinitionItems[0].getAttribute('href')).toBe('https://aka.ms/correct-link');
});
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 15 additions & 9 deletions docs/CITATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ The activity graph should have a [Message thing](#message-thing).

A [Message thing](https://schema.org/Message) represent the message activity itself and act as the root of the [activity graph](#activity-graph). It must have the following fields:

- `@context` of `"https://schema.org"`
- `@id` of `""` (an empty string means self in JSON-LD fashion)
- `@type` of `"Message"`
- `type` of `"https://schema.org/Message"`
- `@context` of `"https://schema.org"`
- `@id` of `""` (an empty string means self in JSON-LD fashion)
- `@type` of `"Message"`
- `type` of `"https://schema.org/Message"`

### Non-URL citation

Expand All @@ -53,7 +53,7 @@ Bot developers should implement citations as outlined in this section to ensure

Notes:

- The third citation is a non-URL citation, its link `cite:1` is currently ignored
- The third citation is a non-URL citation, its link `cite:1` is ignored and treated as an opaque string

```
Sure, you should override the default proxy settings[1]​[2], when your proxy server requires authentication[3].
Expand All @@ -73,8 +73,10 @@ Sure, you should override the default proxy settings[1]​[2], when your proxy s

Please refer to the graph for details of each fields. Notably:

- Only compact from is supported (i.e. nested objects), other forms and object references are not supported unless stated otherwise
- Subclasses are not supported. If the object is expected to be `Message`, it must not be `EmailMessage` (subclass)
- Only compact from is supported (i.e. nested objects), other forms and object references are not supported unless stated otherwise
- Subclasses are not supported. If the object is expected to be `Message`, it must not be `EmailMessage` (subclass)

> Notes: In some older versions of Web Chat, we were using root-level and unconnected `Claim` thing. This is strictly used internally and its usage is being deprecated.

#### Sample payload

Expand Down Expand Up @@ -193,7 +195,11 @@ We use `position` instead of `@id` to match the link definition in Markdown to t

### Source of truths

If there are deviations of information in Markdown and Message thing, the Message thing should take precedence over the Markdown, given the receiver understood the Message thing.
> This is updated in PR [5564](https://github.com/microsoft/BotFramework-WebChat/pull/5564) in 2025-08-29.

~If there are deviations of information in Markdown and Message thing, the Message thing should take precedence over the Markdown, given the receiver understood the Message thing.~

If there are deviations of information in Markdown and Message thing, the Markdown should take precedence over the Message thing. This is to support plain text channels (text/SMS) as they do not have capacity to display content from the Message thing.

### `usageInfo` on the `Message` thing should be a blank node

Expand All @@ -203,4 +209,4 @@ In JSON-LD, blank node means a node that does not have any contents but `@id` an

## Further reading

- [Microsoft Teams: Bot messages with AI-generated content](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bot-messages-ai-generated-content?tabs=after%2Cbotmessage#citations)
- [Microsoft Teams: Bot messages with AI-generated content](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bot-messages-ai-generated-content?tabs=after%2Cbotmessage#citations)
44 changes: 37 additions & 7 deletions packages/component/src/ActivityStatus/private/Originator.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import { type OrgSchemaProject } from 'botframework-webchat-core';
import React, { memo } from 'react';
import { custom, object, optional, pipe, readonly, safeParse, string, type InferInput } from 'valibot';

type Props = Readonly<{ project: OrgSchemaProject }>;
import useSanitizeHrefCallback from '../../hooks/internal/useSanitizeHrefCallback';

const Originator = memo(({ project }: Props) => {
const { name, slogan, url } = project;
const originatorPropsSchema = pipe(
object({
// TODO: [P1] We should build this schema into `OrgSchemaProject` instead, or build a Schema.org query library.
project: custom<OrgSchemaProject>(
value =>
safeParse(
object({
name: optional(string()),
slogan: optional(string()),
url: optional(string())
}),
value
).success
)
}),
readonly()
);

type OriginatorProps = InferInput<typeof originatorPropsSchema>;

// Regular function is better for React function component.
// eslint-disable-next-line prefer-arrow-callback
const Originator = memo(function Originator(props: OriginatorProps) {
const {
project: { name, slogan, url }
} = validateProps(originatorPropsSchema, props);

const sanitizeHref = useSanitizeHrefCallback();

const { sanitizedHref } = sanitizeHref(url);
const text = slogan || name;

return url ? (
return sanitizedHref ? (
// Link is sanitized.
// eslint-disable-next-line react/forbid-elements
<a
className="webchat__activity-status__originator webchat__activity-status__originator--has-link"
href={url}
href={sanitizedHref}
rel="noopener noreferrer"
target="_blank"
>
Expand All @@ -22,6 +53,5 @@ const Originator = memo(({ project }: Props) => {
);
});

Originator.displayName = 'Originator';

export default Originator;
export { originatorPropsSchema, type OriginatorProps };
11 changes: 7 additions & 4 deletions packages/component/src/Attachment/FileContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const ROOT_STYLE = {
}
};

// TODO: Consider using `useSanitizeHrefCallback`, which underlying use `sanitize-html` or whatever in HTML content transformer.
const ALLOWED_PROTOCOLS = ['blob:', 'data:', 'http:', 'https:'];

function isAllowedProtocol(url) {
Expand Down Expand Up @@ -96,10 +97,10 @@ function FileContent(props: FileContentProps) {

const localizedSize = typeof size === 'number' && localizeBytes(size);

const allowedHref = href && isAllowedProtocol(href) ? href : undefined;
const sanitizedHref = href && isAllowedProtocol(href) ? href : undefined;

const alt = localize(
allowedHref
sanitizedHref
? localizedSize
? 'FILE_CONTENT_DOWNLOADABLE_WITH_SIZE_ALT'
: 'FILE_CONTENT_DOWNLOADABLE_ALT'
Expand All @@ -112,12 +113,14 @@ function FileContent(props: FileContentProps) {

return (
<div className={classNames('webchat__fileContent', rootClassName, fileContentStyleSet + '', className)}>
{allowedHref ? (
{sanitizedHref ? (
// URL is sanitized.
// eslint-disable-next-line react/forbid-elements
<a
aria-label={alt}
className="webchat__fileContent__buttonLink"
download={fileName}
href={allowedHref}
href={sanitizedHref}
rel="noopener noreferrer"
target="_blank"
>
Expand Down
Loading
Loading