104 lines
2.3 KiB
TypeScript
104 lines
2.3 KiB
TypeScript
import sanitizeHtml from 'sanitize-html'
|
|
|
|
const ALLOWED_REL_VALUES = new Set(['nofollow', 'noopener', 'noreferrer'])
|
|
|
|
const ALLOWED_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:'])
|
|
|
|
const URL_KEYS = new Set(['href', 'url', 'externalUrl'])
|
|
|
|
const RICH_TEXT_OPTIONS: sanitizeHtml.IOptions = {
|
|
allowedTags: [
|
|
'a',
|
|
'b',
|
|
'blockquote',
|
|
'br',
|
|
'code',
|
|
'em',
|
|
'h1',
|
|
'h2',
|
|
'h3',
|
|
'h4',
|
|
'h5',
|
|
'h6',
|
|
'hr',
|
|
'i',
|
|
'li',
|
|
'ol',
|
|
'p',
|
|
'pre',
|
|
's',
|
|
'span',
|
|
'strong',
|
|
'u',
|
|
'ul',
|
|
],
|
|
allowedAttributes: {
|
|
'*': ['class', 'title'],
|
|
'a': ['href', 'target', 'rel'],
|
|
},
|
|
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
|
|
allowProtocolRelative: true,
|
|
disallowedTagsMode: 'discard',
|
|
nonTextTags: [
|
|
'script',
|
|
'style',
|
|
'textarea',
|
|
'option',
|
|
'iframe',
|
|
'object',
|
|
'embed',
|
|
'form',
|
|
'svg',
|
|
'math',
|
|
],
|
|
transformTags: {
|
|
a: (tagName, attribs) => {
|
|
if (attribs.target !== undefined && !['_blank', '_self'].includes(attribs.target)) {
|
|
delete attribs.target
|
|
}
|
|
if (attribs.rel !== undefined) {
|
|
const rel = attribs.rel
|
|
.split(/\s+/)
|
|
.filter((value) => ALLOWED_REL_VALUES.has(value))
|
|
.join(' ')
|
|
if (rel) attribs.rel = rel
|
|
else delete attribs.rel
|
|
}
|
|
if (attribs.target === '_blank') attribs.rel = 'noopener noreferrer'
|
|
return { tagName, attribs }
|
|
},
|
|
},
|
|
}
|
|
|
|
export function sanitizeRichText(value: string) {
|
|
return sanitizeHtml(value, RICH_TEXT_OPTIONS)
|
|
}
|
|
|
|
export function sanitizeUrl(value: string) {
|
|
try {
|
|
const parsed = new URL(value, 'https://relative.invalid')
|
|
return ALLOWED_URL_PROTOCOLS.has(parsed.protocol) ? value : ''
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
export function sanitizePayload(value: unknown, key = ''): unknown {
|
|
if (typeof value === 'string') {
|
|
return URL_KEYS.has(key) ? sanitizeUrl(value) : sanitizeRichText(value)
|
|
}
|
|
|
|
if (Array.isArray(value)) return value.map((item) => sanitizePayload(item))
|
|
|
|
if (value && typeof value === 'object') {
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([childKey, childValue]) => [
|
|
childKey,
|
|
sanitizePayload(childValue, childKey),
|
|
])
|
|
)
|
|
}
|
|
|
|
return value
|
|
}
|