Update-Manager
Aktuelle Version
3.5.0-rc.7-900beca2
Updates gültig bis: 2028-01-06 13:08:25
Update verfügbar
3.5.2
patchVeröffentlicht: 2026-09-04
Upgrade notes
- Zip installs: move your own files out of the Total CMS folder before you update. An update replaces
config/,public/,resources/,src/andvendor/whole — it installs the new release's copy over the old one rather than merging file by file — so a folder you added inside any of them is removed along with its parent. Earlier installation instructions told zip users to point their document root at the extractedpublic/directory, which leaves nowhere for a site's own stylesheets, scripts, fonts and media except inside the application. If that describes your install, move those files out to your site, alongside the Total CMS folder rather than within it, before updating. The installation guide now describes the layout this updater is safe in, and Updates lists exactly what is replaced and what is left alone. - The new "keep a copy of the previous version" safety net does not cover the update that installs it. An update is carried out by the version being replaced, so the run that brings you to this release still uses the old code — which deleted its backup as soon as it finished. Retention starts protecting you on the update after this one. Take your own backup before updating to this release, particularly if anything of yours currently lives inside the Total CMS folder.
- A Site Builder page routed at
/rssnow servesapplication/rss+xml. An extensionless route used to fall through totext/html; a route whose last segment is exactlyrssis now treated as a feed. Only that exact segment counts —/rss-helpis still a page, and no other type is matched this way. If you have an HTML page at/rss, rename its route - Enclosure types are no longer
application/octet-streamfor images and PDFs./feed/rss/{collection}guessed from a table covering only mp3, ogg, wav, mp4 and webm, so every other attachment was labelled as a generic download. Nothing needs changing; subscribers just start seeing the real type - An SVG that references an XML entity is now rejected rather than partially cleaned. The sanitiser was upgraded, and it refuses a document whose body uses a
&name;declared in aDOCTYPEinstead of stripping the expansion and keeping the rest — the XXE and billion-laughs shapes. A plainDOCTYPEis still fine, and so is declaring an entity without using one, so ordinary Illustrator and Inkscape exports are unaffected. If a file is refused, re-export it from your design tool /mcpnow answers the 2026-07-28 MCP specification as well as the older ones. Nothing needs changing and no client has to be reconfigured — each request is classified on arrival and routed to the lifecycle it belongs to. If you run PHP-FPM with a small worker pool, note that the new spec'ssubscriptions/listenholds one worker for the length of its window:mcp.subscriptionStreamSecondsdefaults to 1 second and the existingmcp.listeningStreamMaxConcurrentcap bounds how many can be open at once- Regenerate your Designer Tokens. Tokens issued before this release carry half the randomness they appear to — see below. Existing ones keep working and nothing breaks if you leave them, but a token generated from now on is genuinely 14 characters. To reissue one, turn the Template Designer toggle off and back on for that template, then update the token in your development tool
- Template Designer remote sync starts working again. If your production templates have been stuck at an old version — or sitting empty — since 3.5.0, the pushes were 404ing (see below) and the fix needs no configuration. Render the dev page once and the current content is pushed. Where a production
.twigwas left empty, the body is still on your development server: the sync will restore it, or the badge's "Copy Template" button hands it to you - Template ids resolve inside
builder/templates/on every surface. 3.5 movedtcms-data/templates/*intotcms-data/builder/templates/*, but the Twig loader's root becamebuilder/— one level above — socms.render.loadMore('blog', {template: 'blog/card'})started looking forbuilder/blog/card.twigand failing with "Unable to find template". Ids are now resolved relative tobuilder/templates/wherever one is accepted, so pages that worked before 3.5 work again with no edit. If you worked around this by writingtemplates/blog/card, that keeps naming the same file — the redundant prefix is ignored
Security
- Designer Tokens are no longer half as random as they look. The token pattern interpolated the same 7-character random value twice, so a 14-character token like
mt1euuqmt1euuqcarried only 7 characters of entropy — and this token is the sole credential on a public endpoint that can overwrite template content. New tokens are drawn as a single 14-character value. Existing tokens are unchanged and still valid; see the upgrade note above to reissue one - A user can no longer rewind their own login quota or audit trail.
maxLoginCountwas a protected field butloginCount— the counter it is measured against — was not, and bothLoginServiceandPasskeyLoginActiongate onloginCount >= maxLoginCount. Because the profile form submits every schema field, a user who had reached their limit could save their profile withloginCount: 0and grant themselves unlimited logins, repeatedly.loginCount,lastloginandcreatednow join the privileged set: a non-super-admin's write to them is reverted to the stored value and logged. Nothing legitimate is affected — the login counter is written byLastLoginUpdateServicethrough a domain service, andcreatedis assigned server-side on every create — and a super-admin can still set all three by hand
Added
- Feeds in Twig:
cms.feed.rss()andcms.feed.atom()build a feed from two arguments — the feed's own details, and a list of items the template has already shaped. Choosing and shaping stays where|filter,|sortByand|mapalready live, which is what the existing/feed/rss/{collection}endpoint cannot reach: it maps fields by name, so it cannot compose a title out of two fields or run a Markdown field through|markdown. Escaping, CDATA, RFC-2822 dates and theatom:linkself reference are handled for you. Items take an optionalmediafor enclosures — a bare URL, or{url, type, length}for a podcast, where image and file fields already carrymimeandsize. See Feeds - An update refuses an incomplete archive instead of installing it. Extraction reports failure when the disk fills, a permission cannot be satisfied, or an entry is damaged, and that result was not being read — so a partly-extracted release could be swapped over a working install one directory at a time, leaving a mix of versions or a truncated file. A single zero-length file in
config/is enough to end every request with "Value of type int is not callable" before any of the checks that would explain it can run. The extraction result is now checked, and the staged files must contain the directories every release ships before anything is replaced. A refused update leaves the site running on the version it already had - A zip install now keeps the version it replaced. A successful update used to delete its own backup the moment it finished, so
tcms update:rollbackonly ever worked after an update that failed part-way — exactly the wrong way round for anyone who discovered a problem afterwards. One copy of the previous version is now kept attcms-data/.system/backups/(about 60 MB), shown in the Update Manager with a button to remove it, and restorable withtcms update:rollback. Untick Keep a copy of the previous version, or pass--no-backuptotcms update:apply, to skip it. The copy goes in the data directory rather than beside the application on purpose: a directory next to the application sits inside the document root on most installs, which would leave the previous release's PHP reachable over the web — including whatever the update just patched. Where it cannot be placed there safely (a data directory on another filesystem) it is discarded rather than left behind, and the update log says so - Named validation patterns in schemas: a property's Extra Schema Definitions can say
{"pattern": "patterns.version"}instead of carrying a literal regex. It is the same name a form field uses, so both share one definition, and there are no backslashes to escape. Nested patterns use their dotted path (patterns.postCode.usa). Expansion happens once, on save, so the stored schema is still real JSON Schema for the validator, MCP and exports — and it adds the^/$anchors, which a literal pattern needs and often does not get patterns.versionandpatterns.versionExtended: a plain three-part release number (3.5.0), and full semver with an optionalvprefix, prerelease and build metadata (v3.5.1-rc.1,3.5.0+build.7)- Support for the 2026-07-28 MCP specification, served from the same
/mcpURL as the older revisions. That revision drops theinitializehandshake and the session that came with it: a client states who it is on every request instead, which means an agent can reconnect without re-establishing anything. Tools, resources and prompts are identical either way — the same registry, the same persona filtering, the same authorization — so a site gains the new clients without changing its surface. See MCP server subscriptions/listen, the new specification's replacement forresources/subscribe: one stream on which a client names the resource URIs it cares about and receives updates until the window closes. The stream is filtered by the caller's persona, so a client only ever receives updates for resources it could read directly — naming one it cannot reach is accepted but never delivers.mcp.subscriptionStreamSecondssets how long a stream stays open
Changed
- The JSON field forgives unescaped backslashes. The Extra Schema Definitions box holds JSON Schema fragments, and the common one is a regex — nearly all backslashes. JSON only permits a backslash before
" \ / b f n r t u, so{"pattern": "^\d+$"}was a hard parse error even though the intent is plain. A backslash that starts no legal escape is now doubled, alongside the trailing commas the field already forgave. The repair is idempotent, and runs only when the value fails to parse — valid JSON is never rewritten - An access-group refusal is now written to the access log. The 403 a user sees says only "Access denied", and until now nothing was recorded server-side either —
oauth-activitycovered API tokens, so a refusal for someone signed into the admin left no trace at all. Working out why a member could not save something meant reconstructing their session by hand. Every refusal now logs the resource, operation, user, route and path to the access channel, which names the check that refused rather than just the fact that something did - The MCP protocol revision is now negotiated rather than asserted. A client that asks for a revision the server supports is answered on that revision instead of being told which one the server prefers, so editors and agents pinned to an older revision stop having to decide whether to continue against a mismatch. Where the server genuinely cannot speak what was asked for it still counter-offers, which is what the specification calls for
- SVG sanitising is stricter about XML entities.
enshrined/svg-sanitize1.0 refuses a document that references an entity rather than cleaning around it, which also fixes 0.x leaving an unresolved&name;in its own sanitised output. The release additionally carries two upstream fixes tohrefhandling — one where cleaning was skipped whenhrefandxlink:hrefappeared together — and a fix for quadratic sanitising time on element-dense files mcp.subscriptionsEnabled: falsenow refusessubscriptions/listenoutright rather than accepting a stream that would deliver nothing for the length of its window. Handshake-eraresources/subscribecalls are still accepted as before, so clients that error on a rejected subscription keep working
Fixed
- Deleting the file from an image or file field no longer fails. The delete returned a 500 —
Cannot access offset of type string on string— and the file vanished from disk while the field went on claiming it was there. One remover serves every field type without a dedicated one, and it was written for the gallery shape: a gallery holds a list of files to filter, while an image or file field holds a single file's details directly, so the filter was reading a field name as if it were a file. A single-value field is now emptied, which is what deleting its only file means - A schema file that is not valid JSON no longer breaks the tools that would tell you so. One unparseable file in
.schemas/threw out of the internal "does this schema exist?" check, and because nine places ask that question — the admin schema page andschema:lintamong them — a single bad file could take out parts of the admin that had nothing to do with it.tcms schema:lintwas the worst of them: the command for finding broken schemas died on the first one it met, showing a stack trace rather than naming the file. A corrupt schema is now reported as<file> is not valid JSON: <reason>, everything else keeps working, and lint tells you which file to fix - A form no longer hangs when a field cannot read its own value. A JSON field holding invalid JSON threw out of value collection, and because the payload was gathered as an argument to the save request, the throw escaped before the error handler was attached. The form stayed in the processing state that blocks every later submit — a dead form, no request sent, and no message shown. The payload is now collected before the form is locked, and the error names the offending field
- The Template Designer pushes to production again. Every remote sync had been answered with a 404 since 3.5.0: the Designer endpoint moved under the site's
/apiprefix when the routes were reorganised, but the code composing the push was never told, so it kept addressing the old location. The failure was easy to miss — the local save still succeeded, the production file simply never changed, and a cached copy could keep serving the previous version until the cache was cleared. The push is now addressed to/api/designer/templates/{path}, which is where the endpoint has actually lived all along, and the documentation has been corrected to match. Subfolder installs are fixed by the same change, and a site at the domain root no longer emits a doubled slash after its hostname - A Template Designer id written with a leading slash is accepted.
{% templatedesigner for '/templates/card' %}produced an empty segment in the push URL and, locally, a folder named literally/templates. The id is now normalised where it is read, so both forms behave identically — the server already tolerated the stray slash - A template id now names the same file everywhere.
{% templatedesigner for '...' %}, thetemplateoption oncms.render.loadMore()and friends, and the load-more endpoint that serves the later pages each resolved an id their own way. The Designer's local save wrotebuilder/myblog.twigand reported a green "Local: ✓", while the remote half looked for the real template atbuilder/templates/myblog.twigand answered "Template not found" — a stray file on one side and a sync that could never succeed on the other. All four now resolve through one function: an id is relative tobuilder/templates/, a leadingtemplates/is optional, and so is the.twigextension. The load-more endpoint was corrected alongside the Twig helper, so a server-rendered first page and the pages fetched after it cannot disagree about which template they are rendering - A user can update their own profile again. Saving at
/admin/profilereturned "Access denied: Your access groups do not have permission to perform this action on this collection" for anyone whose groups did not separately grant write access to the auth collection — which is the point of having restricted groups at all. The carve-out that is supposed to let a user edit their own record compared the request's collection against the rawAUTH_COLLECTIONsession value, and that value is empty for everyone who signs in at the ordinary/admin/loginURL, since it carries no{collection}segment.'auth' === ''never matched, so the write fell through to the group check and was refused. Everywhere else in the codebase an empty auth collection already means "the configured default", and the comparison now resolves it the same way. Sessions store the resolved value from now on, but no one has to sign in again for the fix to take effect. Only super-admins were unaffected, which is why this survived since 3.1.0
This installation is managed by Composer. Run composer update totalcms/cms to update.