Wiki Phase 1: categories, drafts/publish, HTML sanitization
Foundation & safety phase of the wiki upgrade (see WIKI_UPGRADE.md). Schema (additive, idempotent via ensureSchema): - new wiki_categories table; wiki_pages gains category_id, excerpt, published, published_at, sort_order, and a FULLTEXT index - migration ALTERs guarded with IF NOT EXISTS for existing databases - seed reworked into 4 sections with the 8 starter pages assigned Security: - new utils/sanitizeHtml.js (sanitize-html allowlist); wiki bodies are sanitized on every save, and the article renders through DOMPurify - strips <script>, event handlers (onerror), and javascript: URLs Backend: - public: published-only list with ?category filter + /wiki/categories - admin: extended page CRUD, PATCH publish toggle, category CRUD; drafts visible to admin, hidden from public - all writes logged to activity_log Frontend: - data-driven public wiki index (sections + real descriptions; removed hardcoded blurbs/Roman numerals) with ?category filtering - article: category breadcrumb + sanitized render - admin: Section/Status columns, draft/publish + section + excerpt in the editor, and a Manage sections modal Verified end-to-end against MariaDB 11: migration clean, XSS neutralized, drafts hidden, client builds, server boots. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import DOMPurify from 'dompurify'
|
||||
import PublicLayout from '../../components/PublicLayout.jsx'
|
||||
import { Loading, ErrorState } from '../../components/PageState.jsx'
|
||||
import { useAsync } from '../../lib/useAsync.js'
|
||||
@@ -13,11 +14,13 @@ function slugify(text) {
|
||||
.replace(/(^-|-$)/g, '')
|
||||
}
|
||||
|
||||
// Parse the stored body HTML: assign ids to <h2> headings and collect a TOC.
|
||||
// Parse the stored body HTML: sanitize (defense in depth — the server also
|
||||
// sanitizes on save), then assign ids to <h2> headings and collect a TOC.
|
||||
function buildArticle(body) {
|
||||
if (!body) return { html: '', toc: [] }
|
||||
if (typeof window === 'undefined' || !window.DOMParser) return { html: body, toc: [] }
|
||||
const doc = new DOMParser().parseFromString(body, 'text/html')
|
||||
if (typeof window === 'undefined' || !window.DOMParser) return { html: '', toc: [] }
|
||||
const safe = DOMPurify.sanitize(body)
|
||||
const doc = new DOMParser().parseFromString(safe, 'text/html')
|
||||
const toc = []
|
||||
doc.querySelectorAll('h2').forEach((h, i) => {
|
||||
const id = slugify(h.textContent || '') || `section-${i}`
|
||||
@@ -77,6 +80,17 @@ export default function WikiArticle() {
|
||||
<Link to="/wiki" style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
Wiki
|
||||
</Link>
|
||||
{page.category_title && (
|
||||
<>
|
||||
<span>/</span>
|
||||
<Link
|
||||
to={`/wiki?category=${page.category_slug}`}
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}
|
||||
>
|
||||
{page.category_title}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<span>/</span>
|
||||
<span>{page.title}</span>
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user