Upgrade notes
- Regenerate any UPC-E or Codabar barcodes. Both were being encoded incorrectly, and the fix below means existing ones are wrong wherever they have been printed or saved. UPC-E now takes the 6-digit payload; the 7- and 8-digit forms are still accepted and normalised, so no template needs changing. Codabar data must no longer contain A–D — the encoder supplies the start/stop characters itself
tcms push --collectionshas changed meaning. It used to move objects from five allowlisted collections; it now moves collection settings, which is what the name should always have meant. Scripts callingpush --collections=builder-pagesexpecting objects will silently push settings instead — use--pagesfor those objectstcms push --collection-metais removed. Its job is now--collections. This one fails loudly as an unknown optioncms.barcode.upce()output: the rendereddata-valueattribute now carries the 6-digit payload actually encoded rather than the input string, when a longer form was passed- Everyone is signed out once when you upgrade. Session files move from the server's shared session directory into
tcms-data/.system/sessions(see below), and sessions in the old location are not carried over. Anyone who used "keep me signed in" is restored automatically rather than bounced to the login form - Behind a CDN that is not Cloudflare, set
trustProxyHeaders. Total CMS now believesCF-Connecting-IPandX-Forwarded-Foronly when the request came from a private address or a Cloudflare edge. Cloudflare and a reverse proxy on the same server both keep working untouched; any other CDN in front of the site needs$settings['trustProxyHeaders'] = 'always'— and its origin firewalled first, or that setting is an open door. Settings → Server Info reports what your install is doing - CSV exports may gain columns. A collection whose objects still hold properties its schema no longer declares now exports those properties instead of dropping them. Existing columns keep their positions — the new ones are appended at the end
Security
- Client IP is no longer taken on trust.
CF-Connecting-IPandX-Forwarded-Forare set by whatever sits in front of the server — but a visitor can send them too, and seven places read them unconditionally. On a site reachable directly from the internet that let a caller hand itself a new identity on every request, walking straight past the per-IP rate limit onPOST /api/action/mailer, the OAuth token endpoint, the MCP endpoint and the automation webhook throttle. One resolver now makes that decision for all of them, andtrustProxyHeadersdecides when the headers are believed. Cloudflare's published ranges ship with each release and are refreshed at build time, so Cloudflare sites need no configuration - Session files are no longer stored in the server's shared session directory. PHP's default location is one directory for every site on the machine, so a neighbouring site's session cleanup could read or delete Total CMS session files. They now live in
tcms-data/.system/sessions, which the shipped Apache and nginx rules already deny, with0700permissions
Added
- Seed content to production:
tcms push --objects=blogsends a collection's objects to your production server, skipping anything already there. Any collection can be seeded, not just the five that sync could already move — so a new feature's schema, collection and starter content can all travel together.--objects=blog:welcome,aboutnarrows to specific objects;--overwrite(with--force) lets the local copy win instead - Seed Objects in the Sync Manager: pick the collections whose objects should be seeded on push. All-or-nothing per collection, and the Sync Manager can only add — overwriting stays on the CLI where it has to be asked for explicitly
- Feature-named sync flags:
--pages,--dataviews,--mailer,--mcp-promptsand--automations, each accepting an optional id list (--pages=home,about). They replace the old--collections, which named a gated set of collection ids and required knowing that Site Builder pages live in a collection calledbuilder-pages - MCP: read-only Site Builder template tools —
list_templatesandget_template, so an agent editingbuilder-pagescan see whichpage.data.*keys a template actually consumes instead of guessing. Admin-scoped, and deliberately read-only: writing Twig reaches the wholecms.*surface, which is a larger authority grant than writing content
Changed
- A mixed push now sends two requests. Seeded objects go to the skip-existing import endpoint while everything else keeps its upsert semantics, so
push --schemas=faq --objects=faqdeploys the schema and seeds its rows without either behaving like the other. Both routes already existed in 3.5.0, so a seed push works against a production server that has not upgraded yet tecnickcom/tc-lib-barcodemoves to^2.14, which brings real input validation to the barcode encoders. Values that used to render an incorrect barcode now raise an error instead. In a Twig template that error renders as an HTML comment rather than breaking the page — so a bad barcode value degrades to a missing barcode, with the reason visible in View Source- Binaries still never travel. Image, file, gallery and depot fields are omitted from every sync payload and the receiving side keeps what it already had, so a seeded post cannot blank a production image. The
image,gallery,fileanddepotcollections cannot be seeded at all — the binary is the object there, and seeding one would land a record pointing at a file the target does not have
Fixed
-
UPC-E barcodes encoded the wrong product.
upce()accepted 7–8 digits, but UPC-E's payload is 6, and the encoder never decompressed the longer forms — it zero-padded the input and appended a check digit.04252614encoded UPC-A0000042526148instead of the correct0042100005264, scanning cleanly as a product that does not exist. All input forms now reduce to the same payload and the same barcode -
Codabar barcodes did not scan. A, B, C and D are reserved as Codabar's start/stop characters and the encoder adds them itself, so passing
A1234Bencoded literally asAA1234BA. Such values are now rejected with an explanation rather than silently producing an unreadable barcode -
"Keep me signed in" survives an update. Persistent-login tokens were stored inside the application directory, which a zip update replaces wholesale — so every update signed out everyone who had ticked the box. They now live in
tcms-data/.system/persistent_tokensalongside logs, and existing tokens are carried across on the first run after upgrading -
Sessions no longer expire early on shared hosting. PHP's shared session directory is cleaned using whichever site triggers the cleanup, commonly on a 24-minute lifetime, so a neighbouring site's cleanup deleted Total CMS sessions regardless of the 24 hours Total CMS configures. With sessions in their own directory that can no longer happen. If the directory cannot be written, Total CMS falls back to PHP's default rather than leaving nobody able to log in
-
A single unreadable object no longer aborts a whole export. One object whose stored data did not match its schema — usually after the schema was edited — threw out of
exportAllObjects(), so the export produced nothing instead of the remaining objects. It now skips and logs, matching the JSON and CSV exports, and the export job records which object ids it skipped -
CSV export no longer silently drops data. Columns are built from the schema, so properties still stored on an object but removed from the schema had no column and vanished — while a JSON export of the same collection kept them. A CSV taken as a backup after a schema edit was quietly incomplete
-
Booleans inside cards survive a CSV round trip. A card's
falseexported as an empty cell andtrueas1, while a top-level boolean column in the same file saidtrueorfalse. Card booleans now use the same spelling and are restored as booleans on import, using the card's own sub-schema so a text field containing the word "true" stays text -
A partial
authconfiguration no longer breaks every request. Twenty places readauth['enable']directly. On installs that turn PHP warnings into exceptions, a settings array without that key made the authentication middleware throw on every request — a 500 rather than a login page. Missing now means enabled, as the shipped default always intended -
A throwing extension route registrar can no longer take down the site. An extension whose route callback threw took the exception straight out of the boot sequence, producing an empty HTTP 500 on every route — including
/admin, so the operator could not reach the page to disable it. Route registrars now run through the existing extension guard: a thrower loses only its own routes, is logged with attribution, and surfaces as an error in the admin -
Large collections could render an empty listing. Collections over 500 objects build their index by streaming it straight to disk, and the build returned nothing because the content is deliberately never held in memory. Anything that triggered that build and used its result — a fresh install, a cleared index, a repair, the rebuild endpoint — got zero objects back, so the page rendered empty and the rebuild reported an empty index. The next request was already correct, because the file itself was written, but nothing reported the empty one
-
"This month" excluded the first of the month. The window opened at the 1st at the current clock time rather than midnight, so a record dated the 1st — stored as midnight, therefore earlier — fell outside it. A post dated the 1st was missing from every "this month" filter for the whole month, not just on the 1st. "This week" was unaffected