shinkan-jinkendo/frontend/src/utils/exerciseRichTextSanitize.js
Lars f10c28e5cd
Some checks failed
Deploy Development / deploy (push) Successful in 59s
Test Suite / pytest-backend (push) Failing after 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 17s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Failing after 7m48s
Close medium exposure on reports, registration, uploads, and exercise HTML.
A superadmin can also turn off another account''s two-factor when the authenticator is lost.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 16:47:57 +02:00

80 lines
2.3 KiB
JavaScript

/**
* Sanitizer für Übungs-Rich-HTML inkl. §11 Platzhalter (span data-shinkan-exercise-media).
* DOMPurify mit fester Allowlist.
*/
import DOMPurify from 'dompurify'
import { sanitizeInlineMediaCaption } from './inlineMediaCaption'
const ALLOWED_TAGS = ['p', 'div', 'br', 'ul', 'ol', 'li', 'h3', 'b', 'strong', 'i', 'em', 'u', 'span', 'a']
const ALLOWED_ATTR = [
'href',
'class',
'data-shinkan-exercise-media',
'data-shinkan-exercise-media-size',
'data-shinkan-exercise-media-caption',
]
const SIZE_OK = new Set(['small', 'medium', 'full'])
function hookAttribute(_node, data) {
const name = data.attrName
if (name === 'href') {
const href = String(data.attrValue || '').trim()
if (!/^https?:\/\//i.test(href)) data.keepAttr = false
else data.attrValue = href
return
}
if (name === 'class') {
const keep = String(data.attrValue || '')
.split(/\s+/)
.filter((c) => c === 'shinkan-inline-media')
if (!keep.length) data.keepAttr = false
else data.attrValue = keep.join(' ')
return
}
if (name === 'data-shinkan-exercise-media') {
if (!/^\d+$/.test(String(data.attrValue || '').trim())) data.keepAttr = false
return
}
if (name === 'data-shinkan-exercise-media-size') {
const size = String(data.attrValue || '').trim().toLowerCase()
if (!SIZE_OK.has(size)) data.keepAttr = false
else data.attrValue = size
return
}
if (name === 'data-shinkan-exercise-media-caption') {
const caption = sanitizeInlineMediaCaption(String(data.attrValue || ''))
if (!caption) data.keepAttr = false
else data.attrValue = caption
}
}
const instances = new WeakMap()
function purifyFor(win) {
const existing = instances.get(win)
if (existing) return existing
const purify = DOMPurify(win)
purify.addHook('uponSanitizeAttribute', hookAttribute)
instances.set(win, purify)
return purify
}
/**
* @param {string|null|undefined} html
* @param {Window=} win
* @returns {string}
*/
export function sanitizeExerciseRichDisplayHtml(html, win) {
if (html == null || typeof html !== 'string') return ''
const trimmed = html.trim()
if (!trimmed) return ''
const target = win || (typeof window !== 'undefined' ? window : null)
if (!target) return ''
return purifyFor(target).sanitize(trimmed, {
ALLOWED_TAGS,
ALLOWED_ATTR,
ALLOW_DATA_ATTR: false,
})
}