const schema = require('./schema'); const { sanitizeRichText } = require('./rich-sanitize'); function isPlainObject(v) { return typeof v === 'object' && v !== null && !Array.isArray(v); } function sanitizeItemFields(item, itemFields) { if (!isPlainObject(item)) return item; for (const field of itemFields) { sanitizeField(item, field); } return item; } function sanitizeField(obj, field) { const value = obj[field.key]; switch (field.type) { case 'richtext': if (typeof value === 'string') obj[field.key] = sanitizeRichText(value); break; case 'list-richtext': if (Array.isArray(value)) obj[field.key] = value.map((v) => (typeof v === 'string' ? sanitizeRichText(v) : v)); break; case 'list-object': if (Array.isArray(value)) value.forEach((item) => sanitizeItemFields(item, field.itemFields)); break; case 'group': if (isPlainObject(value)) sanitizeItemFields(value, field.itemFields); break; default: break; } } /** Returns a deep copy of a content.json payload with every richtext / * list-richtext field run through the HTML allowlist sanitizer. Content * fields never trust client-submitted HTML directly - this is the * authoritative sanitization pass before anything is written to disk. */ function sanitizeContentPayload(payload) { const copy = JSON.parse(JSON.stringify(payload)); const contentSections = schema.filter((s) => (s.file || 'content') === 'content'); for (const section of contentSections) { if (section.type === 'array-root') { const arr = copy[section.key]; if (Array.isArray(arr)) arr.forEach((item) => sanitizeItemFields(item, section.itemFields)); } else if (isPlainObject(copy[section.key])) { for (const field of section.fields) { sanitizeField(copy[section.key], field); } } } return copy; } module.exports = { sanitizeContentPayload };