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:
co-authored by
Claude Sonnet 5
parent
89d64418de
commit
8946915bfe
+115
-15
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
Reference in New Issue
Block a user