Spaces:
Paused
Paused
| /** | |
| * Sanitize HTML before v-html. | |
| * | |
| * Two levels: | |
| * - sanitizeHtml() → strict, for user-generated content (chat, profile, writing feedback) | |
| * - sanitizeQuizHtml() → allows style/border on table elements, for trusted admin quiz JSON data | |
| */ | |
| import DOMPurify from 'dompurify' | |
| // Safe CSS properties allowed in quiz content (IELTS table/heading formatting) | |
| const _SAFE_STYLE_PROPS = new Set([ | |
| 'text-align', 'font-weight', 'font-style', 'color', 'background-color', | |
| 'border', 'border-color', 'border-collapse', 'border-style', 'border-width', | |
| 'padding', 'padding-left', 'padding-right', 'padding-top', 'padding-bottom', | |
| 'margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', | |
| 'width', 'max-width', 'height', 'vertical-align', | |
| ]) | |
| // Strip dangerous CSS values (javascript: protocol, expression(), etc.) | |
| function _sanitizeStyleValue(value) { | |
| if (!value) return '' | |
| if (/javascript\s*:/i.test(value)) return '' | |
| if (/expression\s*\(/i.test(value)) return '' | |
| return value | |
| } | |
| // DOMPurify hook: strips unsafe CSS properties from style attributes | |
| function _addStyleHook() { | |
| DOMPurify.addHook('uponSanitizeAttribute', (node, data) => { | |
| if (data.attrName !== 'style') return | |
| const declarations = data.attrValue | |
| .split(';') | |
| .map((d) => d.trim()) | |
| .filter(Boolean) | |
| .filter((d) => { | |
| const [prop] = d.split(':') | |
| return _SAFE_STYLE_PROPS.has((prop || '').trim().toLowerCase()) | |
| }) | |
| .map((d) => { | |
| const idx = d.indexOf(':') | |
| const prop = d.slice(0, idx).trim() | |
| const val = _sanitizeStyleValue(d.slice(idx + 1).trim()) | |
| return val ? `${prop}: ${val}` : null | |
| }) | |
| .filter(Boolean) | |
| data.attrValue = declarations.join('; ') | |
| if (!data.attrValue) data.keepAttr = false | |
| }) | |
| } | |
| _addStyleHook() | |
| const _COMMON_TAGS = [ | |
| 'p', 'br', 'strong', 'em', 'u', 'b', 'i', 'ol', 'ul', 'li', | |
| 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'div', | |
| 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', | |
| 'img', 'sub', 'sup', 'blockquote', 'mark', 'code', 'pre', | |
| ] | |
| // ── Strict config: user content, chat messages, writing feedback ────────────── | |
| const _STRICT_CONFIG = { | |
| ALLOWED_TAGS: _COMMON_TAGS, | |
| ALLOWED_ATTR: ['class', 'alt', 'colspan', 'rowspan', 'title', 'data-question-id'], | |
| FORBID_ATTR: ['style', 'onerror', 'onload', 'onclick', 'onmouseover'], | |
| ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|\/(?:images|audio|media|uploads)\/|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i, | |
| } | |
| // ── Quiz config: trusted admin JSON data (IELTS tables, headings, GAP_FILLING, images) ─ | |
| const _QUIZ_CONFIG = { | |
| ALLOWED_TAGS: _COMMON_TAGS, | |
| ALLOWED_ATTR: [ | |
| 'class', 'style', 'src', 'alt', 'colspan', 'rowspan', 'title', | |
| 'data-question-id', 'border', 'width', 'height', 'cellpadding', 'cellspacing', | |
| 'align', 'valign', 'loading', | |
| ], | |
| FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'href'], | |
| // Allow https:// external images (cms.youpass.vn) and local data-assets paths | |
| ALLOWED_URI_REGEXP: /^(?:(?:https?):|\/(?:images|audio|media|uploads|data-assets)\/|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i, | |
| } | |
| /** | |
| * Strict sanitizer — use for user-generated content (chat, profile, writing feedback). | |
| * Strips all inline styles. | |
| */ | |
| export function sanitizeHtml(dirty) { | |
| if (dirty == null || dirty === '') return '' | |
| return DOMPurify.sanitize(String(dirty), _STRICT_CONFIG) | |
| } | |
| /** | |
| * Quiz-safe sanitizer — use for IELTS question content, GAP_FILLING tables, | |
| * passage titles, and instruction HTML from trusted admin JSON data. | |
| * Allows safe inline styles (text-align, border-color, etc.) but strips JS. | |
| */ | |
| export function sanitizeQuizHtml(dirty) { | |
| if (dirty == null || dirty === '') return '' | |
| return DOMPurify.sanitize(String(dirty), _QUIZ_CONFIG) | |
| } | |