# 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. Every entry point loads `cms/bootstrap.php`, which reads `config.php` and the untracked `config.local.php`, works out the URL prefix (`basePath`) and site root (`siteRoot`) for subfolder installs, and registers the class autoloader. `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. Media uploads (images, documents, video, audio) use the separate `cms/image_upload.php` endpoint because TinyMCE and the Media library send multipart file data. It applies the same authentication, role, and CSRF requirements before `MediaTypes::inspect()` validates the file (see Media below). ## Components | Class | Responsibility | | --- | --- | | `CMSController` | Action dispatcher and workflows (pages, drafts, publishing, menus, templates, users). | | `Authentication` | Sessions, role capabilities, CSRF tokens, session revocation. | | `UserStore` | Account registry: config accounts plus CMS accounts, invitations, password policy. | | `PagePaths` | URI and path safety (containment, symlinks, existing and new page paths). | | `ContentDom` | HTML transforms: menus, shared regions, link resolution, image and SEO edits. | | `MediaTypes` | The allow-list of media extensions and categories, and the upload safety checks. | | `SiteAnalyser` | Byte-preserving tokenizer used by the site scan and menu tagging. | | `FileStore` | Locked, atomic JSON metadata storage. | | `Activity`, `Logger` | Dashboard activity feed and daily audit log. | | `LoginRateLimiter` | Hashed address and identity throttling, also used for password confirmation and invites. | ## 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. New pages live only here until first published. | | `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 menus: `{key: {items: [{label, url, parent}], updated, title?}}`; seeded by the site scan, never created by the CMS. | | `newpages.json` | Pending (unpublished) pages: title, chosen menu, template, author, created. | | `pagetemplates.json` | Pages offered as templates in the New page section of Pages. | | `pagelocks.json` | Pages an administrator has locked against editor changes. | | `users.json` | CMS-created accounts and profile overlays for config accounts (`{users: {...}, profiles: {...}}`). | | `media.json` | Per-file description (alt text or link text), original filename, and uploader. | | `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 a `