# NeoCMS Architecture ## Design Goals NeoCMS adds controlled visual editing to static HTML websites while retaining three properties: * Public pages remain ordinary HTML files served directly by the web server. * Supporting state uses files rather than a database service. * Existing page structure and styling remain under the site's control. ## Request Flow `cms/index.php` renders the authenticated administration shell. It passes the CSRF token and configured editable class to `cms/js/cms.js` through metadata tags. The JavaScript client loads public pages into a same-origin iframe, binds editing behaviour to matching regions, and submits complete serialised documents to `cms/controller/index.php`. The preview iframe uses `sandbox="allow-same-origin"` without `allow-scripts`. Parent-side JavaScript may still inspect and edit the DOM, while scripts belonging to public pages cannot reach the administration window, session, or CSRF token. `CMSController` is the single API dispatcher. It authenticates every request and delegates mutating actions through `requirePost()`, which enforces POST, CSRF validation, and the requested role capability. Image uploads use the separate `cms/image_upload.php` endpoint because TinyMCE sends multipart file data. It applies the same authentication, role, and CSRF requirements before validating image bytes. ## Storage Model Published content stays in the web document root. NeoCMS stores supporting data beneath `cms/data/`: | Path or document | Contents | | --- | --- | | `drafts/` | Complete unpublished HTML documents, named by a hash of their URI. | | `revisions/` | Immutable snapshots of earlier published documents. | | `scheduled/` | Complete HTML staged for future publication. | | `drafts.json` | Human-readable draft ownership and timestamp metadata. | | `revisions.json` | Revision URI, author, reason, and timestamp index. | | `schedules.json` | Publication times and staged-document identifiers. | | `shared.json` | Named global HTML fragments. | | `menus.json` | Named flat menu structures with optional parent labels. | | `media.json` | Reusable image alternative text. | | `activity.json` | The latest 250 dashboard activity entries. | | `login-attempts.json` | Hashed address and identity buckets used for login throttling. | `FileStore` replaces JSON documents atomically by writing a temporary neighbour and renaming it. Public page publication uses the same approach. A revision is created before replacing, deleting, or globally updating a public page. Private directories use mode `0700` and private files use `0600` where supported. Configurable count and byte quotas prevent drafts, schedules, revisions, and uploads from growing without bound. ## Editing Model The configured `editableClass` identifies editable regions. The client edits a region's inner HTML in TinyMCE, then writes it back to the iframe only. Drafting, scheduling, and publishing serialise a clone of the complete iframe document after removing temporary CMS controls. `neo-dupe` marks a repeatable block. `data-neo-shared="key"` marks a globally managed fragment. `data-neo-menu="name"` marks generated navigation that should be replaced whenever its named menu changes. The bundled templates use the default `editable` class. Sites that override `editableClass` must update their templates to match; otherwise the resulting page is valid HTML but, quite reasonably, is not treated as editable by NeoCMS. ## Roles Capabilities are intentionally small and cumulative: | Role | Capabilities | | --- | --- | | `editor` | Save drafts and upload images. | | `publisher` | Editor capabilities plus publish, schedule, and restore revisions. | | `administrator` | Publisher capabilities plus page, media, shared-content, and menu management. | The interface hides controls the user cannot invoke, but server-side checks remain authoritative. Hidden buttons are a convenience, not a security boundary wearing a clever disguise. Active sessions are bound to the current configured password hash. Removing an account or changing its hash invalidates the session on the next request, while roles are re-read from configuration so demotion is immediate. ## Scheduled Publishing Every API request opportunistically processes due jobs. Reliable unattended operation comes from invoking `cms/publish_scheduled.php` through cron. The worker is CLI-only and uses the same publication and revision path as the UI. ## Security Boundaries NeoCMS relies on PHP sessions with strict mode, HTTP-only cookies, SameSite=Lax, and Secure cookies under HTTPS. Mutating requests require a constant-time-checked CSRF token. Page paths are canonicalised and constrained to HTML files beneath the document root, excluding the CMS directory and symbolic links. Uploads are constrained by byte size, detected MIME type, image decoding, dimensions, pixel count, file count, and total storage. Login attempts are throttled by hashed address and address/username buckets. HTML and JSON responses receive Content Security Policy, anti-framing, no-sniffing, referrer, permissions, cross-origin, cache-control, and conditional HSTS headers. CDN scripts and styles are fixed to explicit versions and protected by SRI. The supplied Apache rules protect configuration, data, logs, CLI entry points, and uploaded executable extensions. Other servers must provide equivalent denial rules, and production use should always use HTTPS. Audit entries are locked while appending, normalised to prevent forged lines, rotated at a configurable size, and pruned after a configurable retention period. They remain operational records rather than a tamper-proof ledger.