You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A critical security issue is fixed (3.5.0 through 3.6.4). Update as soon as you can. Then check your auth collection for admin accounts you did not create.
A number of smaller security fixes, found in a review of every route against every kind of caller. The behavior changes they bring are listed below
The data folder is protected on every install. The .htaccess that blocks web access to tcms-data is now added on the next request wherever it is missing, and the setup wizard always checks for it. An .htaccess that you wrote yourself is left alone
Composer installs keep tcms-data outside the web root. A new Composer install uses <project>/tcms-data. Zip installs and sites that are already set up do not change
Remote fetches use public addresses only. Save from URL, the RSS and WordPress importers and MCP fetch only from public addresses, with certificate checks on. A refused or invalid URL is a 400. Behavior change: feeds with a broken certificate, and feeds on your own private network (tcms rss:import included), no longer import. A feed over 20 MB is refused
Importers need the import utility permission on their API routes, as the admin import pages already did
Members of other auth collections no longer get the default group. The default group now applies only to the operator collection. Page gates and file access groups do not change. If your members read collections through the API, give them a Registration Group
Clearing the job queue needs the Job Queue or Import permission
The session cookie is Secure on HTTPS, and cookie_httponly is honored
Directory listings are off, and public pages send security headers. Every shipped .htaccess starts with Options -MultiViews -Indexes. Site Builder pages, forms and assets send X-Frame-Options, X-Content-Type-Options: nosniff and Referrer-Policy, as the admin already did. HSTS is there, commented out
Login limits per IP address. Failed logins are counted on the server for each IP address. After auth.maxAttempts failures (default 10), that address is locked out for auth.deniedTimeout minutes (default 7). Each lockout within a day doubles the next one, up to a day. Accounts are never locked. When many addresses guess at one account, each attempt on it is slowed by a second instead. Each lockout is logged with the address in the login log
Download passwords have the same limit. After auth.downloadMaxAttempts wrong passwords (default 25), that address is locked out of download passwords, with the same lockout times as login
Forgot password, resend verification and registration have rate limits. Forgot password and resend verification allow auth.mailRatePerIp sends per address per 15 minutes (default 5). Past the limit the form shows its usual message and sends nothing. Public registration allows auth.registerRatePerIp accounts per address per hour (default 20), then answers 429. Set either limit to 0 to turn it off. See Security
ImageWorks request limits. A parameter with a bad value (such as a negative width, q=101 or fm=php) gets a 400 before any image work. Numbers are made canonical, so w=0300 and w=300 share one cache file. Only two renders per CPU core run at the same time; other requests wait up to 30 seconds for a free slot, then get a 503 with Retry-After. Cached images never wait. Change the limits with imageworks.renderSlots and imageworks.renderWait. Images that a page renders on the server, such as cms.media.imageDataUri(), do not have these limits. See ImageWorks
Added
Security checks in tcms check, the Server Checker and the dashboard. These checks test your live install. They test whether the data folder or sensitive files can be read from the web, directory listings, debug or dev mode on a public host, HTTPS, security headers and HSTS, XML-RPC and an available update. They also check registration groups that give editor or admin powers, admin accounts and passkeys, broad or unused API keys, anonymous MCP collections that expose email or password fields, and |raw over request data in builder templates. Each finding has a fix and a docs link. tcms check exits non-zero on a critical finding (use --no-security to skip the checks), and admins see a dashboard alert. Add a check's ID to security.ignore to stop counting it. See Security
Stock Photos extension. A bundled extension, free in every edition. It adds a button to each image and gallery field and a Stock Photos item to the object page's actions menu. Both open a panel that searches Pixabay. Add a photo with its + button or drag it onto any field. Each photo comes with alt text and a full credit. The panel can also show your own galleries as tabs before Pixabay, as a media library. Pixabay shows only when you enter an API key, so a site can use galleries only. The API key stays on the server. Styled text and markdown fields get a Stock Photos tab in their Insert Image dialog: pick a photo and it is uploaded to the field, with its alt text and the credit as the image title. See Stock Photos
Extensions can add tabs to the Insert Image dialog. Styled text and markdown fields share the dialog. An extension pushes {id, label, render} onto window.TotalCMS.imageDialogTabs and picks a photo with ctx.select(). Core downloads the photo, checks the field's upload rules, uploads it and inserts it. See Extension Points
Quick Navigation lists extension settings pages. Admins can jump to an extension's settings, for example "Stock Photos Settings", from Shift+Cmd+O
Image credits. Images and gallery images can have an optional credit (author, author URL, source, source URL, license). Edit it in a new Credit section of the image dialog. The License field suggests common licenses and also accepts any text. Links are kept only when they are http(s)
Drop photos onto image fields from other parts of the page. Image and gallery fields accept a photo drag (application/x-totalcms-photo) or field.insertPhotos(). The alt text, credit, tags, link and focal point upload with the photo. The photo goes through the normal upload, so upload rules, previews and chunking all apply. Any extension can be a photo source this way
cms.collection.query() and cms.view.query(). These return one page of a collection or view. Filter with filterCollection rules and sort with sortCollection rules or a sort string. The result has what pagination needs (objects, total, page, pages, hasMore), and it stays cached until the collection is saved. objects() loads the whole index on every request, so use query() for listings. The blog starter, the docs and the agent skill now use it. A search option matches visitor text the way cms.collection.search() does and pages and caches the results, so search pages get the same speed; only the page is cached, so visitor text cannot fill the cache. See Collections
MCP attach_file_from_url and remove_file. Agents can put a file from a public URL on an object's image, file, gallery or depot field. An image or file is replaced, a gallery adds to the end, and a depot adds at path. meta sets the alt text and credit in the same call. The limit is 60 attaches an hour per caller. remove_file deletes one file. Both need the same permissions as patch_object. See MCP Server
tcms starter:export <dir> saves a site as a Composer starter. It copies the templates, schemas, collections and pages, and the content unless you pass --structure-only. Users are emptied, so the setup wizard runs for whoever installs the starter. Keys, sessions, secrets and password settings are never copied. The export fails if any secret is found in the output. It writes composer.json, a post-install script, a .gitignore, a README and a CREDITS.md made from the image credits. See Starters
tcms imageworks:warm builds a collection's image cache ahead of time. Run it from cron or a deploy script, so the first visitor to a page gets cached images. Choose the sizes with --widths and --formats, --picture (sizes that match cms.render.picture()), --params, --preset or --url. --dry-run only counts. Name card, deck and video images as parent/child, for example mycard/photo or myvideo/poster. See ImageWorks
Tiled text watermarks.marktextpos: tile repeats the text over the whole image in offset rows, so no corner is left clean to crop out. The text runs at 30° and 30% opacity unless you set marktextangle or marktextalpha. marktextrepeat (1 to 12, default 4) sets how many marks run across the image. See ImageWorks
Outline and shadow for text watermarks.marktextstroke draws an outline and marktextshadow a drop shadow, so the text stays readable on light and dark images. Both are a percentage of the font size (0 to 25), so they look the same at any image size. Set their colors with marktextstrokecolor and marktextshadowcolor (default black). They need a TTF or OTF font
tcms repair:index --check. Reports index problems without rebuilding: object files that do not parse, index entries with no file, files missing from the index, a missing index, and a wrong object count. It exits 1 on a problem and works with --all and --json
$ctx->http for automations. Use it for services whose URL you wrote yourself, such as Sendy, Stripe or Slack. $ctx->publicHttp is for URLs that come from content. See Automation Handlers
Activity Log records email sends. Each send writes a row with the sender, mailer template, subject and recipient, but not the body. Sends that fail or are refused are warnings with the reason. A bulk send writes one row with the count
Dashboard: Quick Actions setting, Needs Attention, Recent Activity, Newly Created. Choose and order Quick Actions in Settings → Dashboard. You can include a "New {item}" button for each collection. Only critical alerts stay as banners. Other alerts move to a Needs Attention card, which is hidden when it is empty. New alerts tell you when no SMTP From address is set and when emails failed. Recent Activity shows the newest Activity Log rows, and Newly Created lists objects that visitors and members created in the last 30 days. Both are for admins only. Empty Collections is now a compact table
Quick Navigation lists every New action. Type "New User" to open that collection's add form
Optional date fields have a clear button. Safari and Chrome have no way to empty a date input after it is set, so a date such as a user's expiration could not be removed
Extension settings forms support a formgrid. Headers, dividers and fieldsets in an extension's settings schema now appear in its settings form
A full-endpoint OpenAPI spec.resources/api/totalcms-api-full.json lists every registered route with its security and middleware, for security scanners. The public spec in totalcms-api.json stays the reference for people, with two errors fixed
Round Image Sizes setting. Settings → ImageWorks can round the width and height in ImageWorks URLs up to a multiple of a number of pixels, such as 50 (imageworks.sizeStep). This stops a bot from making a new render for every pixel size. The widths that cms.render.picture() uses and your preset sizes stay exact. The default is 0, which serves exact sizes. tcms imageworks:warm rounds the same way, so it warms the files that visitors get
Changed
/api routes answer a missing session with 401, not the login page. Scripts got a 302 to the HTML login page. They now get a JSON 401, or a 403 for an account outside the admin collection. An htmx request from an admin page whose session expired goes to the login and then returns to that page. Admin pages still redirect
Index rebuilds no longer block saves. A rebuild reads the files without the lock, then rereads under the lock only the objects that changed while it ran. With Queue Rebuild on Save, cron rebuilds no longer make every save wait
Cached queries no longer decode the index. A cached REST, MCP or load-more query decoded the whole index before it checked the cache, which costs about 120 MB at 50k objects. A save to one collection now clears only that collection's cached queries, not every collection's
The blog starter lists posts newest first, ten per page, with newer/older links
Admin login and forgot-password pages have a meta description. Site SEO audits flagged them as missing one
Job Queue Manager: "Clear Job Queue". It clears every job, not only imports, and its description now says so
Watermarks work in every edition. Image and text watermarks were a Standard feature. When a license check failed, a site fell back to Lite and served its images without watermarks, with a cache header that kept them for a year. Watermarks set in Settings or in a URL now always apply
Text watermarks size to the image by default. With no marktextw and no marktextsize, the text now spans 40% of the image width, so it has the same weight on a thumbnail as on the original. Before, it was drawn at 500px and shrunk to 90% of any image narrower than that. Behavior change: set marktextw for a different width. A mark with only marktextsize keeps its fixed size
SessionKeys no longer has LOGIN_ATTEMPTS, DOWNLOAD_ATTEMPTS or the get*Keys() lists. Login and download attempts are counted on the server now, and nothing used the lists. Extensions that referenced them need to drop those references
Fixed
Overlapping saves no longer damage files or lose updates. Collection files were written in place, so another request could read a half-written .meta.json ("Collection does not exist") or .index.json ("not valid JSON"). Two saves at once could also overwrite each other's index or count changes. Files are now written to a temp file and then renamed into place. The index and collection metadata are locked while they change
Two creates at once can no longer get the same ${oid}. The second create failed with "already exists", or sometimes overwrote the first. The number is now claimed under a lock. A failed create can leave a gap in the numbers, but a number is never used twice. A new collection also no longer numbers its objects 1, 3, 4
Objects without a localized value save again. Every create, update and import that left a localizedtext or localizedstyledtext field empty failed with "The data (array) must match the type: object"
Stacks 6 admin logins no longer loop back to the login page. Admin requests and Stacks pages used different session names, so Page Lock read a different cookie from the one the login wrote. Stacks 6 installs now use the name "Stacks" everywhere
A sitemap date that can't be parsed no longer breaks the sitemap. That entry's lastmod is left out, and an object without a date no longer gets the date of the object before it
The setup wizard runs when the auth collection has no users. It counted the collection's .meta.json and .index.json files as users. So a site copied with an empty auth collection skipped the wizard, and nobody could create the first admin
Clearing the cache clears it once. A full clear deleted the version stamp, so the next request took it for an update and cleared every cache again. This happened on every settings save, dev mode toggle and Cache Manager clear
The Twig cache no longer fills the disk on Stacks pages. A Stacks page renders as a string template, and a page with a value that changes on each visit (such as a form's hidden date or IP) made a new cache file on every visit. One site's cache grew to 18 GB. String templates now use their own folder, and copies unused for a day are deleted
Streamed index rebuilds work where tmpfile() is disabled. On those hosts, every rebuild of a collection over 500 objects failed. With Queue Rebuild on Save off, new objects never reached the index. JumpStart and Sync exports have the same fallback
repair:index shows the real count for collections over 500 objects. It printed "0 object(s)"
API keys sent as Authorization: Bearer work. Since 3.5.0-beta.7, every Bearer header was treated as an OAuth token. So a key sent the way the docs show first always got a 401
Malformed requests get a 400, not a blank 500. This covers a session cookie that PHP refuses (every page was a 500 until the cookie was cleared), malformed download, stream and object paths, and malformed headers. Also, a 400 no longer resets OPcache
A bad MCP prompt argument name no longer stops the MCP server. A name such as 1st stopped the whole server from building. That argument is now logged and skipped
Deleting a passkey that doesn't exist answers 404 and no longer saves the user record again
builder:init shows the right templates folder in its next steps on git-managed projects
The server check hides everything under a sensitive key, such as the URL or list ID in an automations.secrets block, not only keys whose names look sensitive
js-beautify's glob and minimatch pinned to patched versions to fix two moderate advisories
Downloading many files no longer locks visitors out. The download counter counted every download, not only wrong passwords, and never reset because it cleared the wrong session key. So a visitor who downloaded 25 files in one session got "too many attempts" on every download after that
Images with both an image and a text watermark are cached. The second pass, which adds the text, used a cache name that changed on every request. So each view rendered that pass again and left a new .cache/temp_*.png/ folder on disk. The finished image is now cached like any other image. A render deletes the old folders for that image, and clearing the ImageWorks cache removes all of them.
A watermark limit counts dpr.w=600&dpr=3 renders an 1800px image, but the limit compared only the 600. So with limit: 800, a high-density request got a large image with no watermark
Replacing a watermark logo updates the images. Renders were cached by the logo's file name only, so a new logo.png under the same name was not used until the cache was cleared. Renders with a logo are now cached by the logo file's modified time, so every image re-renders with the new logo on its next request. Old renders stay on disk until the ImageWorks cache is cleared
Documentation
Large collections. Server Sizing has a new section on what slows down at 10k and 50k objects and what helps. The cache tag page explains why tags slow down a collection that is edited often. See Server Sizing
Upgrading from a Total CMS 1 license. Enter a T1 key in the License Manager to apply it to the domain and see the upgrade pricing
Doc examples read the query string with getData. Four examples used request.query or app.request, which Total CMS does not have, so they always used their defaults
SEO fixes across the docs: meta descriptions, heading levels and broken absolute links
Agent skill: agents add a Total CMS credit line to a layout's footer, and listings use cms.collection.query()