95 lines
3.4 KiB
JavaScript
95 lines
3.4 KiB
JavaScript
import sanitizeHtml from 'sanitize-html';
|
|
|
|
const allowedTags = [
|
|
'p', 'br', 'h2', 'h3', 'h4',
|
|
'strong', 'em', 'u', 's',
|
|
'ul', 'ol', 'li', 'blockquote', 'a',
|
|
'figure', 'figcaption', 'img',
|
|
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td'
|
|
];
|
|
|
|
const blockTags = /<\/?(?:p|h[2-4]|ul|ol|li|blockquote|br|figure|figcaption|table|thead|tbody|tfoot|tr|th|td)\b[^>]*>/gi;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
function decodeTextEntities(value) {
|
|
const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", '#39': "'", nbsp: ' ' };
|
|
const codePoint = (code, radix) => {
|
|
const parsed = Number.parseInt(code, radix);
|
|
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 0x10ffff && !(parsed >= 0xd800 && parsed <= 0xdfff)
|
|
? String.fromCodePoint(parsed)
|
|
: '�';
|
|
};
|
|
return String(value)
|
|
.replace(/&#x([0-9a-f]+);/gi, (_, code) => codePoint(code, 16))
|
|
.replace(/&#(\d+);/g, (_, code) => codePoint(code, 10))
|
|
.replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/gi, (_, name) => named[name.toLowerCase()]);
|
|
}
|
|
|
|
export function sanitizeNoticeContent(value) {
|
|
const source = String(value ?? '').trim().slice(0, 20000);
|
|
return sanitizeHtml(source, {
|
|
allowedTags,
|
|
allowedAttributes: {
|
|
a: ['href', 'target', 'rel'],
|
|
figure: ['class'],
|
|
img: ['src', 'alt'],
|
|
th: ['colspan', 'rowspan'],
|
|
td: ['colspan', 'rowspan']
|
|
},
|
|
allowedClasses: {
|
|
figure: [
|
|
'image', 'table', 'image-style-inline', 'image-style-block', 'image-style-side',
|
|
'image-style-align-left', 'image-style-align-right',
|
|
'image-style-block-align-left', 'image-style-block-align-right'
|
|
]
|
|
},
|
|
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
|
|
allowProtocolRelative: false,
|
|
transformTags: {
|
|
a(tagName, attributes) {
|
|
const safeAttributes = {};
|
|
if (attributes.href) safeAttributes.href = attributes.href;
|
|
if (attributes.target === '_blank') safeAttributes.target = '_blank';
|
|
safeAttributes.rel = 'noopener noreferrer';
|
|
return { tagName, attribs: safeAttributes };
|
|
},
|
|
img(tagName, attributes) {
|
|
const safeAttributes = {};
|
|
if (/^https?:\/\//i.test(attributes.src || '')) safeAttributes.src = attributes.src;
|
|
if (attributes.alt) safeAttributes.alt = attributes.alt;
|
|
return { tagName, attribs: safeAttributes };
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
export function noticePlainText(value) {
|
|
const sanitized = sanitizeNoticeContent(value).replace(blockTags, ' ');
|
|
const withoutTags = sanitizeHtml(sanitized, { allowedTags: [], allowedAttributes: {} });
|
|
return decodeTextEntities(withoutTags).replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
export function noticeContentHtml(value) {
|
|
const source = String(value ?? '').trim();
|
|
if (!source) return '';
|
|
if (!/<\/?(?:p|h[2-4]|strong|em|u|s|ul|ol|li|blockquote|a|br|figure|figcaption|img|table|thead|tbody|tfoot|tr|th|td)\b/i.test(source)) {
|
|
return source
|
|
.split(/\r?\n{2,}/)
|
|
.map(paragraph => `<p>${escapeHtml(paragraph).replace(/\r?\n/g, '<br>')}</p>`)
|
|
.join('');
|
|
}
|
|
return sanitizeNoticeContent(source);
|
|
}
|
|
|
|
export function noticeForClient(notice) {
|
|
return { ...notice, content: sanitizeNoticeContent(notice.content), contentHtml: noticeContentHtml(notice.content) };
|
|
}
|