Add Users, media library, navigation menus from the scan, new-page wizard and deleted-page recovery

Users (Tools > Users)
- Everyone can update their display name, email and password; administrators can add,
  edit, invite (one-time link), block and delete accounts.
- Accounts in config.local.php stay locked to config (the main administrator); CMS
  accounts live in data/users.json. Sign-in is by login name.
- Roles are now editor and administrator (publisher is an alias of editor).
- Account protection: password hashes only (12-72 characters), administrator password
  confirmation for sensitive actions, rate-limited failures, hashed single-use invite
  tokens, self-lockout guards, sessions of blocked/deleted users end on the next request,
  every account event audited without secrets.
- dataDirectory can move data outside the web root; the dashboard recommends it.

Media library
- Upload images, documents, video and audio, sorted into four categories by extension.
- Extension allow-list; executables, scripts, HTML/SVG, archives, legacy and macro Office
  files refused; content sniffing; files containing code refused; PDFs with scripts refused;
  Office/OpenDocument packages inspected for macros and embedded objects.
- Media library button in the editor inserts an image, document link, video or audio.

Navigation menus, pages and interface
- The site scan detects primary/secondary navigation and seeds Navigation Menus; menus can be
  renamed, edited and deleted but not created by the CMS; a scan button finds new navigation.
- New page wizard (template, name, navigation group) in the Pages dialogue; new pages stay
  private drafts until published; pages can be used as templates.
- Deleted pages can be recovered from the Dashboard.
- Full-width top bar with the user at the top right, device icons, consistent buttons.

Tests and docs updated (tests/run.php, README, ARCHITECTURE, DEVELOPMENT, CHANGELOG).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Geoff Moore
2026-09-24 17:47:34 +01:00
co-authored by Claude Sonnet 5
parent 89d64418de
commit 8946915bfe
20 changed files with 3001 additions and 325 deletions
+115 -15
View File
@@ -17,11 +17,30 @@ behaviour to matching regions, and submits complete serialised documents to `cms
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.
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.
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
@@ -29,15 +48,18 @@ Published content stays in the web document root. NeoCMS stores supporting data
| Path or document | Contents |
| --- | --- |
| `drafts/` | Complete unpublished HTML documents, named by a hash of their URI. |
| `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 flat menu structures with optional parent labels. |
| `media.json` | Reusable image alternative text. |
| `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. |
| `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. |
@@ -53,26 +75,103 @@ it back to the iframe only. Drafting, scheduling, and publishing serialise a clo
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.
marks a `<nav>` whose list is replaced whenever its named menu changes. `data-neo-image` marks an image outside an editable
region that can be replaced through the image dialogue.
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
## Site Scan
`SiteAnalyser` tokenises each page (skipping comments, scripts, styles, and templates) to locate element offsets, then
`apply()` splices markers into the original bytes. It deliberately avoids DOM re-serialisation so an automated pass over
every page cannot rewrite unrelated markup. The browser drives the scan in small batches so it can show a progress bar and page count: `listSitePages` returns the
page list, `analyseSitePages` reports a plan for up to 100 pages per request (the client totals the results), and
`applySiteTagging` reuses `rewritePages()` to write only the pages it is given, after taking a revision of each,
through the same atomic-replace path as publishing. All three need the administrator role. Local asset references are resolved and confined by
`PagePaths::refExists()`.
## Media
`MediaTypes` is the single source of truth for uploads. Its allow-list maps each extension to one of four categories (imagery,
documents, video, audio); the category is derived from the extension whenever the library is listed, and there is no separate
category field to drift. `MediaTypes::inspect()` runs for every upload in `cms/image_upload.php` (the endpoint TinyMCE also uses):
extension allow-list plus a deny list applied to every dot-separated part of the original name, content sniffing with `finfo`
against the extension, refusal of executable signatures, a chunked scan of the whole file for code markers, and per-format
checks (image decoding within the size limits, PDF actions, and a small built-in ZIP directory reader for OOXML and
OpenDocument packages that refuses macros, embedded objects, scripts, and executables without needing the `zip` extension).
Files are stored under `uploads/` as `slug-8hex.ext` (one dot only) or as legacy 32-hex names, both matched by
`MediaTypes::nameRegex()`, which the usage counter, the site scan, and `deleteMedia` all share. `uploads/.htaccess` denies
executable and markup extensions, sets `nosniff`, and serves documents with `Content-Disposition: attachment`. The editor's
**Media library** button opens the same dialog in pick mode and inserts an `<img>`, a document link, or a `<video>`/`<audio>`
element by category.
## New Pages and Templates
`newPageAction` reads the chosen template (a file in `cms/templates/` or `page:<uri>` for a page marked in `pagetemplates.json`),
sets its title, and stores it only as a private draft plus a `newpages.json` record. No file is created, so the page cannot be
served. `publishContent` (used by Publish Now and by scheduled publishing) creates the file on first publication, then
`finishNewPage` adds the link to the chosen menu, propagates it, and removes the pending record. The preview writes the draft into
an `about:blank` frame with an injected `<base>` element so relative assets resolve. Page filenames come from the name via
`uniqueUri`, never from user input.
## Navigation Menus
`SiteAnalyser::navs()` classifies each `<nav>` by its position and label (header, banner and main navigation are `primary`;
footer and legal are `secondary`). The scan tags it with `data-neo-menu` and seeds `menus.json` from the first page that has it
(existing menus are never overwritten). `ContentDom::withMenu()` replaces only the inner list of a marked `<nav>` in the page's
original bytes, sets `aria-current="page"` for the current page, and returns null when nothing changes so the page is not
rewritten. The CMS cannot create a menu: `saveMenu` requires an existing key and may change its items and display `title`.
## Deleted Page Recovery
Deleting a page first stores its HTML as a revision with the reason "Before delete". The dashboard's `deleted` list (editors
and administrators) contains pages whose newest revision has that reason and whose file no longer exists; **Recover** calls
`restoreRevision`, which recreates the file from the snapshot.
## Subfolder Installs
`bootstrap.php` derives `basePath` from the script URL and `siteRoot` from the folder containing `cms/`. Stored menu links and
page URLs carry the prefix, and the session cookie path is `basePath/cms`. Both values can be overridden in configuration.
## 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. |
| `editor` | Save drafts, upload images, publish, schedule, restore revisions, and recover deleted pages (the old `publisher` role is an alias). |
| `administrator` | Editor capabilities plus page, media, shared-content, menu, template, and user 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.
Active sessions are bound to the account's current password hash. Removing, blocking, or re-passwording an account invalidates
its sessions on the next request, while roles are re-read on every request so a demotion is immediate.
## Users and Accounts
`UserStore` merges two sources. Accounts in `config.local.php` are authoritative and locked: they are always active, cannot be
edited, blocked, or deleted from the UI, and cannot be shadowed by a CMS account (only a display name and email overlay is
stored for them). Accounts created in the UI live in `data/users.json` as `{hash, role, name, email, status, invite?}` with status
`active`, `blocked`, or `invited`. `UserStore::authArgs()` supplies `Authentication` with the config accounts plus **active** CMS
accounts only, which is why blocked or deleted users lose their sessions without any extra code. Sign-in uses the login name only; the
email address is contact information (unique, but not a login identifier).
Actions: `users` (GET; administrators also receive the account list, never hashes or tokens), `saveProfile` and `changePassword`
(any signed-in user, acting only on their own account taken from the session), and `saveUser`, `inviteUser`, `blockUser`, and
`deleteUser` (administrators). Sensitive actions and email or password changes require the actor's current password, checked
by `confirmPassword()` and throttled with `LoginRateLimiter`. Invitations are 32 random bytes; only the SHA-256 is stored, the link
works once and expires after 7 days, and acceptance goes through `cms/login/?invite=TOKEN`. Passwords are 12 to 72 bytes
(bcrypt truncates beyond 72), must differ from the username and email, and are stored with `password_hash`. Guard rails in the
store and controller refuse self-block, self-delete, self-demotion, duplicate usernames and emails, and any change to a config
account.
## Interface
The administration shell is a full-height grid: logo and page path on the left, the signed-in user and Log out at the top right,
and the toolbar buttons plus the desktop/tablet/mobile icons beneath them (wrapping to their own row below 1100px). Dialogues are
jQuery UI dialogues built from shared helpers (`dataTable`, `rowButton`), with a snackbar for results. Row buttons are small
secondary buttons, action buttons (Save, Next, Apply, and the editor buttons) are a step larger, and the primary action is blue.
## Scheduled Publishing
@@ -83,9 +182,10 @@ Every API request opportunistically processes due jobs. Reliable unattended oper
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.
document root, excluding the CMS directory and symbolic links. Uploads are constrained by an extension allow-list, byte size, detected MIME type,
code scanning, image decoding, dimensions, pixel count, file count, and total storage. Login attempts are throttled by hashed address and
address/username buckets. Account data is protected by hashing only, withholding hashes and tokens from every response, requiring
the actor's password for sensitive account actions, rate limiting those checks, and auditing every account event without secrets.
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.
+91
View File
@@ -0,0 +1,91 @@
# Changelog
All notable changes made to NeoCMS in this contribution: the `cms/` directory, `uploads/.htaccess`, `README.md`, `docs/`, and the updated `tests/run.php`.
This round builds on pull request #39 (site scan, editor workflow, subfolder support), which is already merged upstream and is summarised at the end.
## Unreleased
### Added
- **Navigation menus from the scan**: the site scan detects each page's header `<nav>` (primary) and footer `<nav>` (secondary), marks it `data-neo-menu` in place, and pre-fills Navigation Menus from it (a saved menu is never overwritten). Menu saves now keep the nav's own attributes, leave other bytes alone, skip unchanged pages and set `aria-current` per page. The menu form only appears after choosing Edit on an existing menu, and the CMS cannot create menus (they come from the site's pages via the scan); the display name can be changed without affecting the menu's key. Menus can be deleted (pages keep their current navigation). Navigation Menus has a "Scan site for new navigation" button that finds navigation blocks not yet attached to a menu (including ones a developer adds later) and exposes them. The Navigation Menus button moved to the main toolbar.
- **New page wizard**: New Page is now three steps (template, page name, navigation group). The filename is generated from the name (`about-us.html`, numbered if taken). The page is saved only as a private draft, so it is not visible on the site until first published; publishing creates the file and adds a link to the chosen menu on every page. Unpublished pages show as "New - unpublished" in Pages and can be discarded. Scheduling works for them too.
- **Recover deleted pages**: the Dashboard lists deleted pages (those whose newest revision is "Before delete" and whose file is gone) with a Recover button for editors, which recreates the page from its snapshot. New `deleted` field in the `dashboard` response.
- **Consistent buttons**: row buttons (Edit, Delete, ...) and other dialogue buttons use one small size (12px, 4px 9px padding, 4px radius); action buttons (editor Save Draft/Schedule/Publish/Cancel, dialogue Save/Next/Back/Apply) are a step larger (13px, 6px 14px). Secondary buttons are white with a border, destructive ones have red text, and primary actions (Save, Next, Apply) are filled blue. Editor buttons keep their Save Draft blue text and green Publish Now.
- **Top bar**: a full-width grid. The logo and page path fill the left; the user name and Log out are always at the top right, with the menu buttons and device icons directly beneath them. Below 1100px the buttons take their own row; below 760px everything stacks but the user block stays top right. The logo URL is now relative so it shows in a subfolder install.
- **Users** (Tools > Users): every user can update their display name and email address and change their password; administrators can add, edit, invite (one-time link, 7 days), block, unblock and delete accounts. Sign-in uses the login name only; the email address is contact information. Accounts in `config.local.php` stay controlled by config (only display name and email can be set for them); CMS accounts live in `data/users.json`. Roles are now **Editor** (draft, upload, publish, schedule, restore) and **Administrator**; `publisher` remains an alias of editor. Security: password hashes only (12 to 72 characters), no secrets in responses, invite tokens stored hashed and single use, administrator password confirmation for sensitive actions, rate-limited failures, self-lockout guards, sessions of blocked/deleted/re-passworded users end on their next request, every account event audited. New `UserStore` class; actions `users`, `saveProfile`, `changePassword`, `saveUser`, `inviteUser`, `blockUser`, `deleteUser`.
- **Documentation**: README.md and docs/ARCHITECTURE.md are now kept in step with the changes (roles and Users, navigation menus, New Page wizard and templates, deleted-page recovery, subfolder installs, interface) and are tracked in the local history.
- **Data outside the web root**: `dataDirectory` is honoured everywhere (verified live: accounts, rate limiting, activity and the cron worker all use it; the dashboard writable check now checks it too). Administrators see a Dashboard recommendation while the data folder is inside the web root, and the README install steps and `config.local.php.example` describe the setting.
- **Login name wording**: the Users screen and login page call the username a "login name" (shown read-only in My account and in the account list); the email field is labelled as contact details, not a sign-in identifier.
- **Media library for images, documents, video and audio**: Media now uploads files (multiple at once) into four categories chosen by extension (Imagery, Documents, Video, Audio), with category tabs and counts, per-file description, Copy link and Delete. The editor has a **Media library** button that inserts an image, a document link, a video or an audio player at the cursor. Safety: an extension allow-list (no `.exe`, scripts, HTML/SVG, archives, legacy or macro Office files), filenames such as `report.exe.pdf` refused, content sniffing, refusal of executable signatures and of any file containing `<?php`, `<?=`, `<script` or `<%`, PDFs with JavaScript/launch/embedded files refused, and Office/OpenDocument packages inspected for macros, embedded objects and scripts (built-in ZIP reader, no `zip` extension needed). New `MediaTypes` class; `image_upload.php` now handles all media and returns `name` and `category`; `mediaAction` adds `category`, `ext` and `original`; new stored names are `slug-8hex.ext` (legacy names still work). `uploads/.htaccess` (outside `cms/`) denies more executable extensions and serves documents as downloads. TinyMCE toolbar now wraps so all buttons are visible. The code scan only looks for the long markers (`<?php`, `<script`) in binary files and adds the short ones (`<?=`, `<%@`, `<%=`) for text files only, because compressed video and audio contain short byte sequences by chance; audio and video whose type libmagic cannot name are accepted when their container signature matches; oversize files report the limit.
- **New page merged into Pages**: the New Page toolbar button and its separate dialogue are gone. Pages is now one dialogue with the page list on top and, for administrators, a **New page** section beneath it holding the same three-step wizard (template, page name, navigation group). Creating a page closes the dialogue and opens the private draft for editing. The dialogue scrolls inside the window when it is tall.
- **Welcome page**: updated the built-in welcome sections for the New Page wizard, templates, navigation menus, site scan and device icons.
- **Pages as templates**: each row in the page list has a "Create Template" action (administrators). The page is then offered in New Page beside the files in `cms/templates/`; the new page is a private copy with its title replaced. Renaming or deleting a template page keeps the list in step.
- **Template list**: the New Page template and navigation-group lists use the page-table styling; each template has a Delete action (a template file is deleted; a page used as a template is only unmarked).
- **Device preview**: the Desktop/Tablet/Mobile buttons are icons (with tooltips and accessible names) and the active one is highlighted.
### Migration notes
- New Page is now a section of the Pages dialogue (there is no New Page toolbar button).
- Roles: `publisher` is merged into `editor` (editors can now publish and schedule). Existing `publisher` assignments keep working as an alias; review any account that was a plain `editor`, because it can now publish.
- Accounts: config accounts are unchanged. CMS-created accounts are stored in `cms/data/users.json`. Sign-in is by login name only.
- Media: new uploads are named `slug-8hex.ext` and accept documents, video and audio; existing 32-hex image names keep working. Copy the updated `uploads/.htaccess` (outside `cms/`).
- New optional setting `dataDirectory`: set it to a folder outside the web root on live sites. The default `cms/data/` keeps working.
- Navigation menus can no longer be created from the CMS; they are discovered by the site scan (existing menus are kept). New pages are private drafts until published.
- New API actions: `users`, `saveProfile`, `changePassword`, `saveUser`, `inviteUser`, `blockUser`, `deleteUser`, `newPage` (changed), `discardNewPage`, `setPageTemplate`, `deleteTemplate`, `deleteMenu`; the dashboard response gains `deleted` and `notices`; `media` items gain `category`, `ext` and `original`.
### Verification
- The updated integration suite (`tests/run.php`, included in this contribution) passes, covering concurrency, site scan and tagging, menus, new pages
and templates, deleted-page recovery, users and roles, invitations, media type checks, and audit-log cases. Upstream's earlier `tests/run.php` expects
behaviour that has intentionally changed (editors cannot publish, menus can be created, New Page copies immediately), so the updated suite replaces it.
On Windows the Linux-only permission assertions are skipped.
## Merged in pull request #39
### Added
- **Site scan** (Tools > Site scan, administrators): analyses every managed page and the media library, then makes pages editable
in one step.
- Reports page and folder counts, pages that can be made editable, images without alt text, broken local image/CSS/script
references, unused uploads, and missing title, description and Open Graph tags.
- Tags content containers (`<main>`, else top-level `<article>`/text-bearing `<section>`; header, nav, footer, aside and
forms are skipped, `<body>` is never tagged), images outside editable regions (`data-neo-image`) and photo placeholders
(`role="img"` boxes), and inserts only missing SEO tags. Existing tags are never overwritten.
- Edits page source byte-for-byte (a tokenizer splices markers in; no DOM re-serialisation), keeps a revision of every changed
page, and after the first run lists only pages that still have something to change.
- Runs in small batches from the browser with a progress bar and "N of M pages" count, for both analysing and updating.
- New administrator-only actions: `listSitePages`, `analyseSitePages`, `applySiteTagging`.
- **Click-to-edit images and photo placeholders**: clicking an image (or a photo-placeholder box) opens an image dialog to pick from
the Media Library or change the address and alt text. Clicking other text still opens the content editor.
- **Subfolder installs**: the site can live in a subfolder (for example `http://localhost/neocms/`). The URL prefix is detected
automatically (`basePath`), the site root is the folder containing `cms/` (`siteRoot`), and the session cookie path follows.
Root installs behave exactly as before.
- **Audit log coverage**: cancelling a schedule, uploading an image, changing alt text and failed scheduled publications are now
recorded in the Dashboard activity feed and the daily audit log (new shared `Activity` class).
- **New interface**: bottom-centre snackbar with success/error states and a close button; table-style dialogs (page picker, revision
history, dashboard, shared content, navigation menus, site scan) with icons, draft/status badges and per-row actions.
### Changed
- **Editing workflow**: Save Draft, Schedule Publish and Publish Now moved from the main toolbar into the content editor, which now
offers `Save Draft | Schedule Publish | Publish Now | Cancel`. Saving in the editor (and applying image/SEO changes) stores a
private draft; the header shows "Draft:" when one exists. Publish Now keeps the accessibility confirmation and the editor stays
open if it is cancelled.
- **Structure**: `CMSController` split into `PagePaths` (URI/path safety), `ContentDom` (HTML transforms) and `Activity`; entry
points share `cms/bootstrap.php` (config + autoloader) instead of four copies.
- Restoring a deleted page now uses a `PageNotFoundException` instead of matching an error message.
### Fixed
- **Lost updates**: read-modify-write of JSON metadata is now serialised with `FileStore::update()` (file locking).
- **Non-atomic site-wide writes**: shared-content and menu propagation now replace pages atomically, and non-ASCII text survives.
- **Scheduled publishing** no longer contends for a lock on every request and cannot fail unrelated API calls.
- **Revision pruning** could drop the newest revision when several were created in the same second; revisions now carry a
precise `ts`.
- **Stale preview**: pages replaced on disk could show an old cached copy in the editor preview; each newly opened page is revalidated.
- **Draft reload**: a saved draft loaded into the preview lost its stylesheets and page path (relative URLs resolved inside `/cms/`),
and could make Publish target the wrong address.
- Page titles containing HTML entities are decoded in the page list; PHP `dirname()` backslashes no longer leak into folder names.
- Draft prompts are offered again after a draft is saved within the same session (previously Publish could discard the draft).
### Migration notes
- No manual steps. New optional config keys `basePath` and `siteRoot` are auto-detected (`bootstrap.php`); set `basePath` only
behind a path-rewriting proxy.
- `revisions.json` entries gain a `ts` field (older entries continue to work).
- The `analyseSite` action was replaced by `listSitePages` and `analyseSitePages`.
- The toolbar no longer has Save Draft, Schedule or Publish buttons; they are in the content editor.
+42 -3
View File
@@ -5,20 +5,29 @@
| Path | Responsibility |
| --- | --- |
| `cms/src/NeoCMS/Authentication.php` | Sessions, credentials, roles, capabilities, and CSRF tokens. |
| `cms/src/NeoCMS/CMSController.php` | API dispatch and all page, draft, revision, schedule, media, menu, and shared-content operations. |
| `cms/src/NeoCMS/FileStore.php` | Atomic JSON metadata storage and managed directories. |
| `cms/src/NeoCMS/CMSController.php` | API dispatch and the page, draft, revision, schedule, media, menu, and shared-content actions. |
| `cms/src/NeoCMS/FileStore.php` | Atomic JSON metadata storage, locked `update()` read-modify-write, and managed directories. |
| `cms/src/NeoCMS/UserStore.php` | Accounts: locked config accounts plus CMS accounts in `data/users.json`, invitations, and the password policy. |
| `cms/src/NeoCMS/MediaTypes.php` | Allowed media extensions and categories, and the upload safety checks (`inspect()`). |
| `cms/src/NeoCMS/Activity.php` | Records an event in both the dashboard feed and the audit log. |
| `cms/src/NeoCMS/PagePaths.php` | URI normalisation and containment of page paths inside the document root. |
| `cms/src/NeoCMS/SiteAnalyser.php` | Byte-exact analysis and tagging of content regions, images, and SEO tags for the Site scan. |
| `cms/src/NeoCMS/ContentDom.php` | Pure HTML transforms for shared regions, menus, and editable-class detection. |
| `cms/bootstrap.php` | Shared entry-point setup: configuration and autoloader. |
| `cms/src/NeoCMS/LoginRateLimiter.php` | Locked, filesystem-backed login throttling. |
| `cms/src/NeoCMS/Logger.php` | Daily audit logging with field normalisation, size rotation, and retention. |
| `cms/src/NeoCMS/SecurityHeaders.php` | CSP, anti-framing, cache, transport, and browser-policy headers. |
| `cms/js/cms.js` | Iframe editing, dialogues, workflow state, and API client. |
| `cms/index.php` | Authenticated administration shell. |
| `cms/image_upload.php` | Multipart image upload endpoint. |
| `cms/image_upload.php` | Multipart media upload endpoint (images, documents, video, audio); also used by TinyMCE for pasted images. |
| `cms/publish_scheduled.php` | CLI scheduled-publication worker. |
| `tests/run.php` | Isolated integration suite. |
TinyMCE 8.6.0 beneath `cms/tinymce/` is vendored third-party code and should not be reformatted or documented as first-party
source. Its package metadata, changelog, and licence information remain in that directory.
Every read-modify-write of a JSON document must go through `FileStore::update()`; plain `read()` then `write()` loses concurrent updates.
## Documentation Style
Comments use British English and explain intent, constraints, side effects, and security decisions. They should not narrate
@@ -74,3 +83,33 @@ ceilings. New HTML entry points should use `SecurityHeaders` before emitting out
The integration suite creates unique public and metadata directories under the operating system's temporary directory. It
seeds an authenticated administrator session, invokes controller actions directly, and cleans up in a `finally` block.
Tests must never rely on or modify the repository's public HTML, uploads, logs, or data files.
## Local Testing on XAMPP (Windows)
Place the project in XAMPP's `htdocs` folder (for example `htdocs/neocms/`) and browse to `http://localhost/neocms/cms/`. No virtual host is
needed: NeoCMS detects the `/neocms` URL prefix from the request and uses the folder containing `cms/` as the site root. On a
live site at the domain root the prefix is empty and behaviour is unchanged. If a proxy hides the real path, set `'basePath'`
in `cms/config.local.php`.
1. Copy `cms/config.local.php.example` to `cms/config.local.php`.
2. Generate a hash (use XAMPP's `php.exe` if `php` is not on your PATH): `php -r "echo password_hash('choose-a-password', PASSWORD_DEFAULT);"`.
3. Add the hash under `authentication` and a role under `roles`, then browse to `/cms/`. This config account is the main administrator; further accounts are added in Tools > Users.
Windows ignores `chmod`, so private-file permissions are not enforced there; the permission assertions in `tests/run.php`
run only on Linux. Keep `.htaccess` enabled (`AllowOverride All`) so `cms/data/` and `cms/logs/` stay private.
## Production Checklist (Linux + Apache)
- Serve over HTTPS only. Behind a TLS-terminating proxy set `'security' => ['cookieSecure' => true]` in `config.local.php`.
- Create `cms/config.local.php` with password hashes for the main administrator(s) (`editor` or `administrator` roles); never commit it. Other accounts are created in Tools > Users.
- Set `'dataDirectory'` in `config.local.php` to an absolute folder **outside the web root** (for example `/var/neocms-data`, writable by PHP). Accounts, drafts and revisions then can never be served. Administrators see a Dashboard recommendation until this is done.
- Copy `uploads/.htaccess` with the site: it denies executable and markup extensions and serves documents as downloads. On Nginx add the `/uploads/` rule from the README.
- Enable `AllowOverride All` for the site so the shipped `.htaccess` files apply, and confirm `mod_headers` is loaded.
- Own the writable paths (`cms/data/`, `cms/logs/`, `uploads/`, and managed HTML) by the PHP user, for example `www-data`.
- Set PHP `post_max_size` and `upload_max_filesize` to at least `uploads.maxFileBytes` (10 MiB by default; raise both together for video). The PHP `fileinfo` extension is required for upload checks; the `zip` extension is not.
- Add the cron entry from the README for `cms/publish_scheduled.php`.
- The admin loads jQuery from `code.jquery.com`; self-host it and update `cms/index.php` and the CSP if the server must work offline.
- Back up `cms/data/` together with the public HTML.
- Verify after deploying: `/cms/data/`, `/cms/logs/`, `/cms/config.php`, and any `.php` or `.exe` placed in `/uploads/` return 403; the session cookie is `Secure`;
five wrong passwords trigger throttling; `php tests/run.php` passes on the server.
- Upload `cms/`, `uploads/`, and the HTML to the site root. Nothing needs changing from the XAMPP setup. Links written as root-relative (`/about.html`) resolve under `/neocms/` only on the live site, so use relative links or test them after deployment.