16dddcb9f0
- ReportRoomModal: use mx.reportRoom() SDK method, fix undefined CSS vars
(--mx-surface/border → folds color tokens), add role/aria-modal/aria-labelledby,
accessible select/input labels, per-error-code messages, auto-close on success
- About.tsx: clickable matrix_id + email_address links (Text as="a"), AbortController
cleanup, runtime JSON type guard, loading state, role display for all role values,
remove classList theming hack, use mx.getHomeserverUrl()
- RoomViewHeader: useLocalRoomName for header title, useReportRoomSupported gate,
hide Invite/Settings/Report for server notice rooms, isCreator guard on Report,
FocusTrap returnFocusOnDeactivate on topic overlay, Server Notice chip tooltip
- RoomInput: replace raw <div> with folds <Box> for server notice read-only message
- EditHistoryModal: isRawEditEvent type guard, handle next_batch truncation,
getVersionBody handles formatted_body (strips HTML for text display),
role/aria-modal/aria-labelledby accessibility, guard for undefined eventId,
use config.space tokens (remove var(--mx-spacing-*) strings)
- RoomNavItem: remove duplicate getExistingContent (use exported getLocalRoomNamesContent),
maxLength={255} on rename input, fix FocusTrap nesting (renameDialog state moved to
RoomNavItem_, RenameRoomDialog rendered outside menu, menu closes before dialog opens),
pencil icon opacity via config.opacity.P300
- useRoomMeta: export getLocalRoomNamesContent for reuse
- RoomIntro: useLocalRoomName, formatted topic viewer with Overlay/FocusTrap/RoomTopicViewer
- CallRoomName: useLocalRoomName for consistent rename display in call overlay
- General.tsx: fix #980000/#FF6B00 hardcoded hex → color tokens/CSS vars, URL Preview
capitalization, improved encrypted preview warning text + Warning chip, add
description to plain urlPreview setting
- sanitize.ts: fix hex color regex to support 3/4/6/8 digit hex (CSS4 #RGBA, #RRGGBBAA)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
185 lines
4.0 KiB
TypeScript
185 lines
4.0 KiB
TypeScript
import sanitizeHtml, { Transformer } from 'sanitize-html';
|
|
|
|
const MAX_TAG_NESTING = 100;
|
|
|
|
const permittedHtmlTags = [
|
|
'font',
|
|
'del',
|
|
'h1',
|
|
'h2',
|
|
'h3',
|
|
'h4',
|
|
'h5',
|
|
'h6',
|
|
'blockquote',
|
|
'p',
|
|
'a',
|
|
'ul',
|
|
'ol',
|
|
'sup',
|
|
'sub',
|
|
'li',
|
|
'b',
|
|
'i',
|
|
'u',
|
|
'strong',
|
|
'em',
|
|
'strike',
|
|
's',
|
|
'code',
|
|
'hr',
|
|
'br',
|
|
'div',
|
|
'table',
|
|
'thead',
|
|
'tbody',
|
|
'tr',
|
|
'th',
|
|
'td',
|
|
'caption',
|
|
'pre',
|
|
'span',
|
|
'img',
|
|
'details',
|
|
'summary',
|
|
];
|
|
|
|
const urlSchemes = ['https', 'http', 'ftp', 'mailto', 'magnet'];
|
|
|
|
const permittedTagToAttributes = {
|
|
font: ['style', 'data-mx-bg-color', 'data-mx-color', 'color'],
|
|
span: [
|
|
'style',
|
|
'data-mx-bg-color',
|
|
'data-mx-color',
|
|
'data-mx-spoiler',
|
|
'data-mx-maths',
|
|
'data-mx-pill',
|
|
'data-mx-ping',
|
|
'data-md',
|
|
],
|
|
div: ['data-mx-maths'],
|
|
blockquote: ['data-md'],
|
|
h1: ['data-md'],
|
|
h2: ['data-md'],
|
|
h3: ['data-md'],
|
|
h4: ['data-md'],
|
|
h5: ['data-md'],
|
|
h6: ['data-md'],
|
|
pre: ['data-md', 'class'],
|
|
ol: ['start', 'type', 'data-md'],
|
|
ul: ['data-md'],
|
|
a: ['name', 'target', 'href', 'rel', 'data-md'],
|
|
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
|
|
code: ['class', 'data-md', 'data-label'],
|
|
strong: ['data-md'],
|
|
i: ['data-md'],
|
|
em: ['data-md'],
|
|
u: ['data-md'],
|
|
s: ['data-md'],
|
|
del: ['data-md'],
|
|
};
|
|
|
|
const transformFontTag: Transformer = (tagName, attribs) => ({
|
|
tagName,
|
|
attribs: {
|
|
...attribs,
|
|
style: `${
|
|
attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color'])
|
|
? `background-color: ${attribs['data-mx-bg-color']};`
|
|
: ''
|
|
} ${
|
|
attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color'])
|
|
? `color: ${attribs['data-mx-color']}`
|
|
: ''
|
|
}`.trim(),
|
|
},
|
|
});
|
|
|
|
const transformSpanTag: Transformer = (tagName, attribs) => ({
|
|
tagName,
|
|
attribs: {
|
|
...attribs,
|
|
style: `${
|
|
attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color'])
|
|
? `background-color: ${attribs['data-mx-bg-color']};`
|
|
: ''
|
|
} ${
|
|
attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color'])
|
|
? `color: ${attribs['data-mx-color']}`
|
|
: ''
|
|
}`.trim(),
|
|
},
|
|
});
|
|
|
|
const transformATag: Transformer = (tagName, attribs) => ({
|
|
tagName,
|
|
attribs: {
|
|
...attribs,
|
|
rel: 'noreferrer noopener',
|
|
target: '_blank',
|
|
},
|
|
});
|
|
|
|
const transformImgTag: Transformer = (tagName, attribs) => {
|
|
const { src } = attribs;
|
|
if (typeof src === 'string' && src.startsWith('mxc://') === false) {
|
|
return {
|
|
tagName: 'a',
|
|
attribs: {
|
|
href: src,
|
|
rel: 'noreferrer noopener',
|
|
target: '_blank',
|
|
},
|
|
text: attribs.alt || src,
|
|
};
|
|
}
|
|
return {
|
|
tagName,
|
|
attribs: {
|
|
...attribs,
|
|
},
|
|
};
|
|
};
|
|
|
|
export const sanitizeCustomHtml = (customHtml: string): string =>
|
|
sanitizeHtml(customHtml, {
|
|
allowedTags: permittedHtmlTags,
|
|
allowedAttributes: permittedTagToAttributes,
|
|
disallowedTagsMode: 'discard',
|
|
allowedSchemes: urlSchemes,
|
|
allowedSchemesByTag: {
|
|
a: urlSchemes,
|
|
},
|
|
allowedSchemesAppliedToAttributes: ['href'],
|
|
allowProtocolRelative: false,
|
|
allowedClasses: {
|
|
code: ['language-*'],
|
|
},
|
|
allowedStyles: {
|
|
'*': {
|
|
color: [/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/],
|
|
'background-color': [/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/],
|
|
},
|
|
},
|
|
transformTags: {
|
|
font: transformFontTag,
|
|
span: transformSpanTag,
|
|
a: transformATag,
|
|
img: transformImgTag,
|
|
},
|
|
nonTextTags: ['style', 'script', 'textarea', 'option', 'noscript', 'mx-reply'],
|
|
nestingLimit: MAX_TAG_NESTING,
|
|
});
|
|
|
|
export const sanitizeText = (body: string) => {
|
|
const tagsToReplace: Record<string, string> = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
};
|
|
return body.replace(/[&<>'"]/g, (tag) => tagsToReplace[tag] || tag);
|
|
};
|