README, the changelog and the built-in welcome page were missing the page-lock feature and the bootstrap/LoginRateLimiter/Logger hardening from the earlier security audit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
15 KiB
15 KiB
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 itdata-neo-menuin 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 setaria-currentper 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
deletedfield in thedashboardresponse. - 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.phpstay controlled by config (only display name and email can be set for them); CMS accounts live indata/users.json. Roles are now Editor (draft, upload, publish, schedule, restore) and Administrator;publisherremains 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. NewUserStoreclass; actionsusers,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:
dataDirectoryis 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 andconfig.local.php.exampledescribe 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 asreport.exe.pdfrefused, content sniffing, refusal of executable signatures and of any file containing<?php,<?=,<scriptor<%, PDFs with JavaScript/launch/embedded files refused, and Office/OpenDocument packages inspected for macros, embedded objects and scripts (built-in ZIP reader, nozipextension needed). NewMediaTypesclass;image_upload.phpnow handles all media and returnsnameandcategory;mediaActionaddscategory,extandoriginal; new stored names areslug-8hex.ext(legacy names still work).uploads/.htaccess(outsidecms/) 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.
- Lock pages: administrators can lock a page from the Pages list to stop editors publishing, saving a draft, or scheduling a change to it, without restricting administrators themselves. A "Locked" badge shows on the page for everyone; the Lock/Unlock action sits beside Create Template, Duplicate, Rename and Delete. State is stored in
cms/data/pagelocks.json, keyed by page URI, and follows a page through rename (cleared on delete, never copied by Duplicate). New actionlockPage;getPagesitems gainlocked.
Fixed
- Verbose PHP errors:
display_errorsanddisplay_startup_errorsare now disabled inbootstrap.php, so an uncaught engine error can never leak a stack trace or filesystem path to a client; every error still reaches PHP's own error log. - Unbounded login-throttling state:
LoginRateLimiternow caps tracked address/identity buckets at 20,000, evicting the least recently active first, so a distributed flood of distinct addresses or login names cannot growlogin-attempts.jsonwithout bound. - Audit-log pruning cost:
Loggernow prunes expired audit logs probabilistically (1-in-100 constructions) instead of scanning the whole logs directory on every single request.
Migration notes
- New Page is now a section of the Pages dialogue (there is no New Page toolbar button).
- Roles:
publisheris merged intoeditor(editors can now publish and schedule). Existingpublisherassignments keep working as an alias; review any account that was a plaineditor, 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.extand accept documents, video and audio; existing 32-hex image names keep working. Copy the updateduploads/.htaccess(outsidecms/). - New optional setting
dataDirectory: set it to a folder outside the web root on live sites. The defaultcms/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,lockPage; the dashboard response gainsdeletedandnotices;mediaitems gaincategory,extandoriginal;getPagesitems gainlocked.
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, page locking (lock/unlock, rename moves the lock, delete clears it, duplicate never copies it), and audit-log cases. Upstream's earliertests/run.phpexpects 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. - A self-review security audit found no critical or high-severity issues; the three fixes above address its medium/low findings.
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 containingcms/(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
Activityclass). - 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:
CMSControllersplit intoPagePaths(URI/path safety),ContentDom(HTML transforms) andActivity; entry points sharecms/bootstrap.php(config + autoloader) instead of four copies. - Restoring a deleted page now uses a
PageNotFoundExceptioninstead 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
basePathandsiteRootare auto-detected (bootstrap.php); setbasePathonly behind a path-rewriting proxy. revisions.jsonentries gain atsfield (older entries continue to work).- The
analyseSiteaction was replaced bylistSitePagesandanalyseSitePages. - The toolbar no longer has Save Draft, Schedule or Publish buttons; they are in the content editor.