Skip to content

3.6.2

Latest

Choose a tag to compare

@joeworkman joeworkman released this 06 Oct 05:30
3.6.2
6749dc6

Security

  • RSS feeds are off unless a collection turns one on. Upgrade note. /feed/rss/{collection} served every collection that had a URL, and the feed URL's parameters chose which fields appeared, so any indexed field of those collections could be read by anyone. A collection now publishes a feed only when Publish RSS Feed is on in its settings. On upgrade, blog and feed collections are turned on automatically; every other collection's feed returns 404 until you turn it on. Field mapping (title, content, date, author, media) and link, image and language are now collection settings and are ignored in a feed URL, so a feed that mapped them in its URL needs the same values set on the collection's RSS Feed card. See RSS Feeds
  • Drafts no longer leak into RSS feeds. Only the blog schemas left drafts out, and adding any include or exclude to the feed URL switched that off, for blogs too. Drafts are now left out for every schema, whatever the URL says, unless the collection's own Hidden Field setting is changed
  • Signing in and password reset match the email address exactly. The lookup matched part of an address, so smith@gmail.com resolved to john.smith@gmail.com. A password reset mailed the address that was typed and looked the account up the same way when the link was used, so the owner of the shorter address could set the password on the longer one
  • A failed sign-in no longer says whether the account exists. The message named the account and told a wrong password apart from an unknown address. Both now show "Invalid login credentials", and an inactive, expired or login-limited account is only reported once the password is correct
  • The Bulk Mailer endpoints need a signed-in user with the Mailer permission. /api/action/mailer/bulk, /preview and /objects only checked the edition, so on a Pro site anyone could queue a bulk send to a whole collection or list any collection's object ids and titles without signing in. They now require a session or API key whose access group grants mailer, the same check as the Mailer admin page. POST /api/action/mailer, which front-end form actions call, stays public
  • A new access group no longer starts with Mailer, Data Views, Builder and every utility ticked. The admin form pre-selected them, and "All utilities" as well, while a group created any other way starts without them, so a group made in the admin could reach more than the built-in Editor group. The form now starts from the same defaults, so these stay off until an admin grants them. Existing groups are unchanged
  • Password hashes are never sent to clients. Upgrade note for API and MCP writers. With public Read on an auth collection, anyone could GET a user record and receive its bcrypt hash. Read permissions decide who sees a record; no setting exposes the hash. API responses and HTML fragments now leave every password property out, index rebuilds never store one even when a schema lists it in index (saving the schema drops it, and schema:lint warns), and a REST PUT that omits the password key keeps the stored hash, so a GET-then-PUT round trip no longer locks the user out. MCP update_object now merges like patch_object instead of replacing the record: agents never see passwords or binary values, so a full replace could only wipe them
  • Public Read is removed from sensitive collections. Upgrade note. 3.0.41 and 3.0.42 defaulted new collections to public Read and nothing cleared it since. Several affected collections are hidden from the admin, so the box could not be unticked; on an auth collection it let anyone fetch user records. A one-time migration removes read from the public operations of auth collections (and schemas inheriting from auth), playground, mailer, automations, Site Builder pages and data views, matched by schema so renamed collections are covered. Other public operations and content collections are left alone. A front end that read one of these collections anonymously needs an API key or an access group
  • Deck item labels and list option labels are sanitized when shown. Both printed stored values into the admin as HTML, trusting that the value had been sanitized on save. A code field is not, and neither is any field with htmlclean off, so a deck label pattern or a relational option pointing at one could run stored markup. Deck labels now go through the sanitizer in the browser and on the server, so SVG icons and simple formatting still render; list fields default to allowHTML: false, so a list that relies on HTML in its option labels needs allowHTML: true in its settings

Added

  • tcms check: the Server Checker from the command line. Bundle integrity, license, server information, directory permissions, required and optional PHP extensions, in the admin utility's order, with --json and --config (the merged configuration, secrets redacted). It exits 1 when the install cannot run (corrupted bundle, unwritable directory, missing required extension), so a deploy script can gate on it. The checks run in the CLI's PHP, so extension and cache rows can differ from what a page request sees; the output says so
  • The markdown field: a Markdown source editor. You write Markdown and it is stored as written. A toolbar inserts the syntax, Preview shows it rendered, and Fullscreen puts the source and a live preview side by side. Images and files upload through the same dialogs as Styled Text and are inserted as Markdown. The editor is the admin's CodeMirror, or a plain textarea on a page that does not load it. Render with |markdown. See Markdown
  • The styledmarkdown field: the Styled Text editor, saving Markdown. The Tiptap visual editor limited to what Markdown can express, with a source mode and preview, storing a Markdown string instead of HTML. A field nobody edits is never rewritten, and content the visual editor cannot keep (raw HTML, comments, footnotes, abbreviations, linked images) opens in source mode; switching to visual asks first. markdown and styledmarkdown store the same string, so a schema can switch between them, and htmlclean: true opts in to the sanitizer as on the code field. See Styled Markdown
  • The login form can leave out "Keep me signed in". cms.form.loginForm({showRememberMe: false}), or showRememberMe in the whitelabel login options, renders the form without the persistent login checkbox
  • RSS Feed settings on each collection. A new card on the collection form holds the feed's name, description, language, link, image, filters, item limit and field mapping, plus a Hidden Field: objects with that property on are left out of the feed. It defaults to draft. The feed URL is a clean /feed/rss/{collection}; include, exclude, limit, name and description can still be passed to build a filtered variant, which is what the Feeds utility now generates. A Markdown content field is rendered to HTML in the feed
  • Send History for bulk emails. A template's edit page lists its recent bulk sends with sent, failed, skipped and pending counts, marks test sends to an override address, and refreshes after each queue. Until now nothing in the admin showed what a bulk send had actually done

Changed

  • cms.adminAssetsHead() loads browser error reporting. The Sentry snippet was included only by the two built-in admin layouts, so a custom admin page (the Stacks Admin Core, or any page calling the helper) ran the admin scripts with nothing listening, and the upload bug fixed below was never reported. The helper now emits the snippet when the sentry setting is on, limited with allowUrls to errors thrown by Total CMS's own assets or its dashboard pages, so a site's own scripts are not reported

Fixed

  • tcms info reports the site's real domain, edition and license. The domain comes from the request's Host header, which the CLI never has, so every command ran as the domain unknown: the license API had no record for it and tcms info showed a licensed Pro site as Domain unknown, Edition Trial, License Invalid. Web requests now record the site's origin in cache/.siteurl (the same way .docroot already is) and the CLI reads it back; domain in config/tcms.php still overrides it. Until the site has been loaded once in a browser, or right after a cache clear, the CLI still reports unknown
  • The Server Checker shows a directory that is not writable. The permissions table dropped every failing row along with the disabled-cache row, so an unwritable tcms-data, cache, logs or tmp simply disappeared from the list instead of showing as failed. The Domain row now shows the domain Total CMS resolved (the one it licenses against), including behind a reverse proxy, rather than the raw SERVER_NAME
  • tcms info lists every installed cache backend. It read the cache settings with a key shape that does not exist, so it said filesystem on every install, with APCu, Redis and OPcache all running. It now reports the same per-backend status as the admin's Cache Manager (APCu (not active in CLI), Redis, Filesystem, OPcache), and the JSON gains cache.backends alongside cache.backend, which names the backend a web request stores data in
  • A blank-named depot folder no longer wipes the whole depot. The Add Folder dialog is prefilled with parent/; submitting it without a name created a folder named "", which the next save wrote into the record. Deleting that folder passed the name-confirm prompt (an empty answer matches an empty name) and built a request that the trailing-slash rewrite turned into the whole-property delete, which emptied the depot and removed its directory. Blank names are now dropped in the browser and refused by the server for create, delete and rename, and an existing record with a blank-named folder is healed on load by moving its contents into the parent, where they already lived on disk
  • A fresh upload keeps its data on a page that loads forms.js with admin.js. A custom admin page loading both scripts lost every new upload: forms.js replaced the field classes admin.js had registered with its lazy loaders, the preview's tags sub-field had no field object yet, and the next save stored empty sub-fields over the file that had just been uploaded, leaving it on disk but broken in the object. forms.js now keeps any class already registered, and file and image fields wait for the preview's sub-fields before writing the upload's data
  • Deleting a file while it is still uploading no longer throws. The upload's completion rebuilt a preview that was already gone and failed with "Cannot read properties of null (reading 'preview')"; file and image fields now return when there is no preview
  • Site Builder pages answer HEAD requests like GET. Every builder page returned 404 to HEAD while GET returned 200, so uptime monitors, link checkers and curl -I reported working pages as missing. HEAD now gets the same rendering, redirects, fallback 404 page and page gates as GET; the server drops the body
  • The new object form keeps the cursor in the first field, in Safari too. An image field's link dialog holds a hidden iframe of the ImageWorks builder page, whose own form counted as a new object form and focused its first input; Safari moves focus into a hidden frame on a scripted focus(), so the field lost focus about half a second after load. The auto-focus now runs in the top window only, and picks the top-most, then left-most, field on screen rather than the first in the markup, so a formgrid that places the Draft toggle above the ID field no longer gives the toggle the cursor
  • IndexNow submissions are accepted. Every submission was sent without its JSON content type, so the IndexNow endpoint answered HTTP 415 and no changed URL ever reached Bing or the other engines. The job failed in the queue with "IndexNow returned HTTP 415". Submissions now go out as JSON; re-save a page to submit it
  • MCP content no longer returns & for an ampersand. Styled text read as Markdown through the MCP tools came back with every & HTML-escaped, so "Tom & Jerry" read as Tom & Jerry and agents copied that into titles and summaries. A plain ampersand is now returned as written
  • Form dividers, section headers, fieldsets and accordions hide with their fields. A --- divider, a section header, a fieldset or an accordion panel in a formgrid stayed on screen when every field in it was hidden by a visibility rule. Each now hides with its fields, and the blank space left by hidden rows at the end of a form or card is closed up
  • A Sync push no longer removes the receiving site's files. A file or depot field was sent empty and written over whatever the receiving site had, so a push dropped a file uploaded there from its record. File and depot fields are now left out of the payload and kept on the receiving site, the same as images and galleries, at the top level and inside cards and decks. Both sites need this version
  • A Sync push no longer breaks images inside cards and decks. A page's SEO image lives in its seo card, and the push sent its file name and alt text to a site that did not have the file. Re-uploading the image there lasted until the next push, which replaced the card. Image and gallery fields inside cards and deck items are now left out of the payload and kept on the receiving site, as top-level ones already were. Both sites need this version
  • A test send with Override To no longer stops the real send. Proofing a bulk email to your own address recorded every object as sent, so clearing the override and queuing the real send skipped everyone, while the admin reported the emails as queued. Test sends are no longer deduped and never count as a delivery, and an install that already has test rows can simply queue the real send again
  • A bulk send says how many objects it left out. A template goes to each object once; objects that already received it are now left out when the send is queued, and the result says how many ("Queued 12 emails for sending (40 left out: already received this email)"). When every object has it already, nothing is queued and the error says why, instead of reporting emails that would all be skipped
  • A scheduled bulk send goes out at the time you picked. The schedule was stored in the browser's 2026-10-01T09:00 form and compared as text against the queue's UTC clock, so a send scheduled for later today waited for the next UTC day and the site's timezone was ignored. It is now converted from the site's timezone to UTC when queued, and an unreadable date is refused
  • The hourly and daily email limits count the right hour. The window was worked out in the site's timezone but compared against send times recorded in UTC, so the limits were off by the site's UTC offset: too strict west of UTC, too loose east of it
  • Waiting out an email limit no longer uses up a job's retries. Each deferral counted as an attempt, so a bulk email that waited for the limit a few times had no retries left if the send then failed once
  • The Mailer whitelist matches domains exactly and ignores case. Jane@Example.COM was refused by an @example.com entry, and an entry written without the @, such as example.com, also let @badexample.com through. The @ is now added when missing
  • A bulk email to an object deleted after queuing is logged as failed. It left no record, so its batch would have shown as in progress indefinitely
  • SMTP encryption applies on an install that never saved its SMTP settings. The shipped default was TLS while the check only matched tls and ssl, so no encryption mode was set until the settings were saved from the admin

Documentation

  • The MCP docs give the right default for mcp.publicAccess. It has been on since 3.5.0; the docs still said off. Nothing is readable anonymously until a collection's MCP Access is set to Public, and setting mcp.publicAccess to false refuses anonymous callers outright
  • The Mailer has full documentation. The placeholder page is replaced with SMTP and Mailer settings, template fields and variables, Inky layouts, sending from forms, auth flows, automations, PHP and the API, and Bulk Send, including the once-per-object rule, proofing with Override To and Send History
  • The OpenAPI spec describes /action/mailer correctly (mailerId and data, not to/subject/body) and adds the bulk endpoints
  • Pushover is documented as the bundled extension it is. Form Settings and the Pushover page pointed at a Settings → Push Notifications page that no longer exists; setup now goes through Admin → Extensions
  • The license page is rewritten. Lite is no longer sold, so its section is gone and the schemas every edition gets move under "Included in every edition", with an editions summary linking to pricing. New sections cover what a license covers (www, ports, testing subdomains, rehoming, client transfer), free development domains, the trial, updates and how validation works. The installation guide's License step now says that localhost, 127.0.0.1 and any .localhost or .test domain run with every Pro feature and no license, that .local does not, and that DDEV does not serve on .test by default
  • Versioning content in git. The deployment guide gains an optional section for sites that commit tcms-data: what must never be committed (.system/ holds plain-text API keys, the sync key, site.key, OAuth signing keys, sessions, queues and migration state; auth/ holds password hashes; .index.json churns on every save), an ignore list that keeps content and drops runtime state, repair:index after a pull, and what to expect from .meta.json, uploads and Markdown storage
  • json_decode is documented as the PHP function it is, not a filter. The filters page showed |json_decode, which fails with "Unknown filter", and docs_lookup served the same entry. It now points to parseJson(), whose entry says what it returns: an array, or null for empty, invalid or non-array JSON
  • Internal doc links resolve on docs.totalcms.co and in the admin. 58 links were bare relative paths that the docs site resolved against the current page and the admin viewer sent to /admin/...; Search Console was reporting the doubled paths as 404s. They are written as docs/ paths, which both places resolve