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
+168 -41
View File
@@ -23,19 +23,29 @@ Further technical documentation:
* Seamless Integration: Simply add the CMS to any existing static website. Just assign a special class to the elements
you want to make editable, and you’re all set.
* Consistent Layouts: NeoCMS supports page templates to keep your layouts neat and uniform across all pages.
* Drafts and Publishing: Editors can save drafts without changing the public page. Publishers can publish immediately or
schedule a publication.
* Revision History: Every changed or deleted page receives a timestamped filesystem snapshot that publishers can restore.
* Page Management: Search, create, duplicate, rename, and delete editable HTML pages, including pages in subdirectories.
* Media Library: Browse uploaded images, reuse them, maintain alternative text, see usage counts, and remove files.
* Drafts and Publishing: Clicking Save in the editor keeps the change as a private draft without touching the public page.
Editors can then publish immediately or schedule a publication.
* Revision History: Every changed or deleted page receives a timestamped filesystem snapshot that editors can restore, and
deleted pages can be recovered from the Dashboard.
* Page Management: Search, duplicate, rename, and delete editable HTML pages, including pages in subdirectories. The New page section of the Pages dialogue is a
three-step wizard (template, page name, navigation group); the new page stays a private draft until it is published. Any page can
be offered as a template with Create Template.
* Media Library: Upload images, documents, video, and audio; they are sorted into Imagery, Documents, Video, and Audio by file
extension. Reuse them from the editor's Media library button, maintain alternative text or link text, see usage counts, and
remove files. Executables and files containing code are refused.
* SEO Settings: Edit the page title, description, canonical URL, Open Graph image, and robots indexing setting.
* Shared Content: Mark content with `data-neo-shared="name"` and update matching regions across the site.
* Navigation Menus: Build ordered and nested navigation markup and insert it into an editable region.
* Responsive Preview: Preview the current page at desktop, tablet, and mobile widths.
* Navigation Menus: The site scan finds each page's primary and secondary navigation and pre-fills the menus. Edit, rename, or
delete a menu and the change is applied to every page that uses it. Menus are created by the web developer's markup, not by the
CMS.
* Responsive Preview: Preview the current page at desktop, tablet, and mobile widths using the device icons in the top bar.
* Accessibility Checks: Check common authoring problems such as missing alt text, empty links, heading jumps, missing labels,
and absent page metadata before publishing.
* Roles and Activity: Use editor, publisher, and administrator roles. The dashboard displays drafts, schedules, system
problems, and recent activity.
* Site Scan: Analyse every managed page and the media library, then make pages editable in one step. The scan
finds content areas, images, navigation, broken local references, and missing SEO tags, and adds the markers NeoCMS needs.
* Users and Roles: Everyone can update their display name, email address, and password in Tools > Users. Administrators can add,
edit, invite, block, and delete accounts. Two roles, Editor and Administrator, are described on that screen.
* Activity: The dashboard displays drafts, schedules, deleted pages, system problems, and recent activity.
## Installation Guide
@@ -46,8 +56,15 @@ To install NeoCMS:
* Copy `cms/config.local.php.example` to `cms/config.local.php`, add password hashes, and assign roles.
* Update the elements you want to make editable by adding the configured editable class. It is `editable` by default.
* Confirm that PHP may write to the managed HTML files, `cms/data/`, `cms/logs/`, and `uploads/`.
* **Live sites:** create a folder outside the web root (for example `/var/neocms-data`), make it writable by PHP, and set
`'dataDirectory' => '/var/neocms-data'` in `cms/config.local.php`. It must be an absolute path. Without this, accounts, drafts,
and revisions live in `cms/data/`, which is protected only by the supplied `.htaccess`. Administrators see a Dashboard
recommendation until the data folder is outside the web root. Audit logs remain in `cms/logs/`, also protected by `.htaccess`.
* Visit `/cms/`, log in, and begin editing.
The site may also live in a subfolder (for example `http://localhost/mysite/`): the URL prefix is detected automatically. See
[Subfolder installs](#subfolder-installs).
That’s it! You’re up and running.
## Configuration
@@ -57,17 +74,21 @@ Tracked defaults live in `cms/config.php`. Deployment credentials and overrides
| Setting | Type | Purpose |
| --- | --- | --- |
| `authentication` | array | Maps usernames to `password_hash()` values; plaintext credentials are rejected. |
| `roles` | array | Maps usernames to `editor`, `publisher`, or `administrator`. |
| `authentication` | array | Maps the main administrator usernames to `password_hash()` values; plaintext credentials are rejected. These accounts are managed only here. |
| `roles` | array | Maps those usernames to `editor` or `administrator` (`publisher` is accepted as an alias of `editor`). |
| `security` | array | Configures session expiry, login throttling, request limits, storage quotas, and audit-log retention. |
| `uploads` | array | Configures image size, dimension, file-count, and aggregate-storage limits. |
| `uploads` | array | Configures the per-file size limit (all media types), image dimension, file-count, and aggregate-storage limits. |
| `audit` | boolean | Enables daily activity logs under `cms/logs/`. |
| `skipWelcomePage` | boolean | Opens the public root instead of the built-in guide after login. |
| `showFullUrl` | boolean | Displays the current page path in the toolbar. |
| `editableClass` | string | Selects the single CSS class used to identify editable regions. |
`dataDirectory` is an internal override used by the integration suite and specialised deployments. Normal installations
should allow NeoCMS to use `cms/data/`.
`dataDirectory` is the absolute path of the folder holding accounts (`users.json`), drafts, revisions, schedules, and menus. It
defaults to `cms/data/`; on a live site, point it at a folder outside the web root (see the Installation Guide) so those files
can never be served. The folder is created on first use if PHP may create it, and the CLI scheduler uses the same setting.
`basePath` (URL prefix of the site root, such as `/mysite`) and `siteRoot` (the folder containing `cms/`) are detected
automatically and only need setting behind a path-rewriting proxy.
### Editable Class
@@ -95,27 +116,52 @@ created pages are immediately discoverable by the page picker.
### Users and Roles
Passwords must be generated with `password_hash()` and stored under `authentication` in `cms/config.local.php`. Assign the
same username a role under `roles`:
There are two kinds of account:
* **Config accounts** are the main administrators. Generate a hash with `password_hash()` and store it under `authentication` in
`cms/config.local.php`, with a role under `roles`. They can be changed only in that file (in Tools > Users they are listed as
"Config"; only their display name and email can be set from the CMS).
* **CMS accounts** are created in **Tools > Users** and stored in `cms/data/users.json`.
```php
'authentication' => [
'author' => '$2y$10$...',
'publisher' => '$2y$10$...',
'admin' => '$2y$10$...',
],
'roles' => [
'author' => 'editor',
'publisher' => 'publisher',
'admin' => 'administrator',
],
```
* `editor`: edit content, save drafts, and upload media.
* `publisher`: editor permissions plus immediate and scheduled publishing and revision restoration.
* `administrator`: publisher permissions plus page, media, shared-content, and menu management.
There are two roles:
Users without an explicit valid role receive `editor`, the least powerful role. Account removal, password-hash changes,
and role changes affect active sessions on their next request. Sessions expire after 30 minutes of inactivity and 12 hours
in total by default.
| Role | Can do |
| --- | --- |
| `editor` | Edit content, save drafts, upload images, publish or schedule pages, and restore revisions or deleted pages. Cannot change the site structure or manage users. |
| `administrator` | Everything an editor can, plus creating new pages, duplicate, rename and delete pages, templates, navigation menus, shared content, site scan, deleting media, and managing users. |
The former `publisher` role is now part of `editor`; existing `publisher` assignments are treated as `editor`. Users without an
explicit valid role receive `editor`, the least powerful role.
#### The Users screen
* **My account** (every user): see your login name (set by an administrator), change the display name, the email address (needs the
current password), and the password (current, new, confirmation; CMS accounts only). You sign in with your login name and
password; the email address is contact information only.
* **Roles**: a description of each role, with your own marked.
* **User accounts** (administrators): add an account with a password, edit an account (name, email, role, optional password
reset), invite someone, block or unblock, or delete. Add, edit, invite, block, and delete ask for the administrator's own
password. You cannot block, delete, or demote yourself, and config accounts cannot be changed here.
* **Invitations** produce a one-time link that is shown once and expires after 7 days. The invitee opens it and chooses their own
password. No email is sent by the CMS.
#### Account protection
Passwords are 12 to 72 characters and are stored only as hashes. Invite tokens are stored only as hashes and work once. Secrets
never appear in API responses or the activity log. Repeated wrong passwords are rate limited, every account event is recorded in
the activity feed and audit log, and blocking, deleting, changing a role, or changing a password affects that user's sessions on
their next request. `data/.htaccess` denies web access to `users.json`.
Sessions expire after 30 minutes of inactivity and 12 hours in total by default.
When HTTPS terminates at a trusted reverse proxy, set `'cookieSecure' => true` beneath `security` in the local override.
This keeps the session cookie marked Secure even when PHP receives the proxy's internal HTTP request.
@@ -156,7 +202,12 @@ of its shared regions.
### Navigation Menus
Menu items use one line each in this format:
Menus come from your site's own navigation. The **site scan** finds each page's `<nav>`: a header, banner, or main navigation is
treated as `primary`, a footer or legal navigation as `secondary`, and the block is marked in place with
`data-neo-menu="name"`. The first page that contains a menu supplies its items, and a saved menu is never overwritten.
**Tools > Navigation Menus** (main toolbar) lists the menus. Choose **Edit** to change a menu's display name or items, one per
line:
```text
Home | /
@@ -164,13 +215,82 @@ About | /about.html
Team | /team.html | About
```
The optional third value is the label of the parent item. After saving, the generated `<nav>` can be inserted into the
currently selected editable region.
The optional third value is the label of the parent item. Saving rewrites only the list inside each marked `<nav>` on every
page (other bytes, the `<nav>` attributes, and `aria-current="page"` for the current page are handled), and skips pages that
would not change. **Delete** removes a menu from the CMS; pages keep their current navigation. **Scan site for new navigation**
finds navigation blocks a developer has added since the last scan.
The CMS does not create menus: new menus are the web developer's markup, discovered by the scan. The menu key cannot be renamed,
but the display name can.
### Media Library
**Media** uploads and lists files in four categories, decided by the file extension:
| Category | Accepted extensions |
| --- | --- |
| Imagery | jpg, jpeg, png, gif, webp |
| Documents | pdf, docx, xlsx, pptx, odt, ods, odp, txt, csv |
| Video | mp4, m4v, webm, mov, ogv |
| Audio | mp3, wav, ogg, oga, m4a, aac, flac |
Anything not in this list is refused: executables (`.exe`, `.dll`, `.bat`, `.msi`, ...), scripts and server code (`.php`, `.js`, `.sh`, ...),
HTML, SVG, XML, archives, the older `.doc`/`.xls`/`.ppt` formats, and macro-enabled Office files. Every upload is also checked
by content, not just name: the file type must match its extension; files that start like an executable or contain `<?php`,
`<?=`, `<script`, or `<%` anywhere are refused (so a code payload hidden inside an image is caught); PDFs containing
JavaScript, launch actions, or embedded files are refused; and Office/OpenDocument files are refused if they contain macros,
embedded objects, scripts, or executables. Names such as `report.exe.pdf` are refused too. These checks are best-effort hardening,
not a malware scanner, so keep server-side virus scanning where policy requires it.
Uploaded files are saved under `uploads/` with a generated name (`price-list-3f2a9c1d.pdf`); the original name is kept as the
default link text. Every file is limited by `uploads.maxFileBytes` (10 MiB by default), and PHP's `upload_max_filesize` and
`post_max_size` must allow at least that. In the editor, use the **Media library** button to insert an image, a document link, a
video player, or an audio player at the cursor. Documents are served as downloads. `uploads/.htaccess` denies executable and markup
extensions; for Nginx use the rule shown under Web Server Security.
### Site Scan
Administrators can open **Tools > Site scan** to analyse the whole site: page and folder counts, pages that can be made
editable, images without alternative text, broken local image/CSS/script references, uploaded files that no page uses, and
missing title, description, and Open Graph tags. Tick the pages and the kinds of change to make, then apply:
* **Content areas**: adds the editable class to `<main>` (or `role="main"`), otherwise to top-level `<article>` or
text-bearing `<section>` elements. Headers, navigation, footers, asides, and forms are skipped, and `<body>` is never
tagged. Pages that already contain an editable region are left alone; pages with no safe container are reported for manual
tagging.
* **Images**: adds `data-neo-image` to images outside editable regions. In the editor, clicking one opens a dialogue to pick
a Media Library image or change its address and alternative text.
* **Navigation**: marks each `<nav>` (primary/secondary) with `data-neo-menu` and pre-fills Navigation Menus from it.
* **SEO tags**: inserts only the missing title, description, `og:title`, `og:description`, and `og:image` tags, with values
taken from the page. Existing tags are never changed, and canonical and robots values are reported but not invented.
The scan edits page source directly rather than re-parsing it, so nothing else in a file changes, and a revision of every
changed page is kept in Revision history.
### Drafts and Revisions
Drafts do not alter public HTML. When opening a page with a saved draft, NeoCMS offers to load it. Publishing creates a
revision of the previous public file first. Deleting a page also retains its last revision.
revision of the previous public file first. Deleting a page also retains its last revision, and **Dashboard > Deleted pages**
lists pages whose newest revision is "Before delete" and whose file is gone, with a **Recover** button that recreates the page
from that snapshot. Revisions are pruned by the configured retention limits.
### New Pages and Templates
The **New page** section (administrators), under the page list in **Pages**, is a three-step wizard: choose a template, give the page a name, then choose a navigation group
(or none). The filename is generated from the name (`About Us` becomes `about-us.html`, numbered if taken). The page is saved
only as a private draft: no file exists, so it is not visible on the site, until it is first published (Publish Now or a
schedule). Publishing creates the file and adds a link to the chosen menu on every page. Unpublished pages appear in Pages as
"New - unpublished" and can be discarded.
Templates are the files in `cms/templates/` plus any page you choose **Create Template** for in the Pages list. A template page
is used live (later edits to it show in new pages), and the **Delete** action in the template list deletes a template file or
stops using a page as a template (the page is kept).
### Subfolder Installs
The site can live in a subfolder such as `http://localhost/neocms/`. The URL prefix (`basePath`) and the folder that contains
`cms/` (`siteRoot`) are detected automatically, page links stored by menus are prefixed accordingly, and the session cookie path
follows the CMS. Root installs are unchanged.
NeoCMS stores metadata, drafts, scheduled content, and revisions under `cms/data/`. Back up this directory together with
the website. The supplied `.htaccess` denies direct HTTP access on Apache; other web servers should deny `/cms/data/` and
@@ -190,14 +310,20 @@ The server timezone controls PHP date handling; the browser converts the selecte
## Administration Workflow
1. Open a page from **Pages**.
2. Select a region carrying the configured editable class and edit it in TinyMCE.
3. Use **Save Draft** to retain private work, or **Publish** to replace the public HTML after creating a revision.
4. Use **Schedule** when publication should occur later.
5. Check revisions, accessibility findings, SEO metadata, shared content, and menus through the relevant toolbar dialogues.
1. Open a page from **Pages** (or create one in the **New page** section under the list).
2. Select a region carrying the configured editable class and edit it in TinyMCE. Clicking an image opens the image dialogue.
3. In the editor, click **Save Draft** to keep your change private, **Publish Now** to replace the public HTML after
creating a revision, or **Schedule Publish** when publication should occur later. **Cancel** discards unapplied edits.
4. Check revisions, accessibility findings, SEO metadata, shared content, and menus through the relevant toolbar dialogues.
The Save button inside the content-editor modal only applies content to the in-browser preview. It does not write a public
file. This distinction is intentional and prevents an innocent wording change from making an unscheduled public debut.
The top bar shows the current page path on the left; your name and Log out are at the top right, with the toolbar buttons
(Dashboard, Pages, Media, SEO, Navigation Menus, Tools) and the desktop/tablet/mobile preview icons beneath. Tools holds
Revision history, Accessibility check, Shared content, Site scan, and Users. Dialogue buttons share one style: small
secondary buttons for row actions, larger buttons for actions (Save, Next, Apply), and blue for the primary action.
Save Draft in the content editor (and Apply in the image and SEO dialogues) stores the change as a private draft. It never writes
the public file, which only changes on Publish. This is intentional and prevents an innocent wording change from making an
unscheduled public debut. The page header shows **Draft:** once a draft is saved, and reopening the page offers to load it.
## Page Markup Recommendations
@@ -208,7 +334,7 @@ common authoring issues but is not a complete accessibility audit.
## Requirements
* PHP 8 running on Linux
* PHP DOM and fileinfo extensions
* PHP DOM and fileinfo extensions (the fileinfo extension is used to check uploads; the zip extension is not needed)
* PHP sessions enabled
* Write access to `cms/data/`, `cms/logs/`, `uploads/`, and managed HTML pages
* A modern browser with JavaScript enabled
@@ -225,7 +351,7 @@ Apache users should allow the supplied `.htaccess` rules. For Nginx, add equival
location ^~ /cms/data/ { deny all; }
location ^~ /cms/logs/ { deny all; }
location ~ ^/cms/(config(?:\.local)?\.php|config\.local\.php\.example|publish_scheduled\.php)$ { deny all; }
location ~ ^/uploads/.*\.(php[0-9]?|phtml|phar|cgi|pl|py|sh)$ { deny all; }
location ~ ^/uploads/.*\.(php[0-9]?|phtml|phar|cgi|pl|py|sh|exe|dll|bat|cmd|com|msi|asp|aspx|jsp|jspx|html?|xhtml|shtml|svg|xml|js|jar|swf)$ { deny all; }
```
Also disable directory listings, prevent PHP source download through server misconfiguration, serve the CMS only over
@@ -254,8 +380,9 @@ php tests/run.php
```
It exercises authentication hardening, session revocation, login throttling, iframe sandboxing, path containment, content
limits, configurable editable regions, drafts, publication, revisions, page operations, shared content, menus, and
scheduled publishing in temporary directories. See [Development and testing](docs/DEVELOPMENT.md) for the complete check
limits, configurable editable regions, drafts, publication, revisions, page operations, new-page privacy and templates,
shared content, menu detection and propagation, user accounts (roles, invitations, blocking, password confirmation, config
accounts), and scheduled publishing in temporary directories. See [Development and testing](docs/DEVELOPMENT.md) for the complete check
list used by this repository.
## Licence