Exponential 6.0.15
Not released yet. The 6.0.15 line follows v6.0.14 (2026-06-07); no tag v6.0.15 existed on 2026-10-12. This page
lists what the line contains on that date.
Read this page if you run 6.0.14 or older and plan to move to the current code, or if you want to know what the line brings. It is long because the line is: four months of work. Use the sections by period.
Upgrading
The console does it in one call (Upgrading with the console); on an installation that is not a git checkout with git_manager, update the code first as the Upgrading guide, part C, says:
./console exp:upgrade:status # what is done and what is pending; changes nothing
./console exp:upgrade --to=v6.0.15 # dry run: the code, the libraries and every pending step
./console exp:upgrade --apply --to=v6.0.15 # backup, code, libraries, database, scripts, deploy, a check of the site
./console exp:upgrade:rollback # the backups, should you need to go backThe list below is what those steps do, kept as a reference; each item names its step (exp:upgrade:steps lists
them all). What you already did by hand counts: the step says not-needed. Read the checklist of each period you
pass, in this order:
For the changes of 10 to 12 October 2026: the operator notes of the autoload generator (run
php bin/php/ezpgenerateautoloads.php -eonce) and of user accounts that exist only as a draft;expaiandexpqueuearrive inactive, see Exponential Intelligence in the distribution.Authorizing an OAuth client needs the policy oauth/authorize: run
php bin/php/addoauthauthorizepolicy.php --dry-run, then without--dry-run, so the roles that let users sign in keep authorizing REST clients. Step6.0.15-oauth-authorize-policy.php bin/php/addoauthauthorizepolicy.php(a dry run), then with--apply, so the roles that let users sign in keep authorizing REST clients.Well-known passwords are refused, and a forgot-password link expires: new passwords are checked against a list of well-known ones (
WeakPasswordCheck), and a reset link works for one hour (ForgotPasswordKeyLifetime). Existing passwords keep working.Demo mode: nothing changes unless the environment variable
EXP_DEMO_MODE=1is set, which is only for public demos. Check that no production process has it. Under Velocity it is taken only from the installation's site envvars (/etc/vc/sites-available/<site>.envvars), never from the shared/etc/vc/envvars: move it before restarting a demo.Translated documentation: the documentation moved to
doc/eng-us/, the German translation isdoc/ger-de/(a language directory is the locale code in lower case). Links, scripts and settings overrides of your own that name a documentation file needeng-us/afterdoc/, and ones written for the interim names (en/us/andde/belowdoc/) need the new names; help addresses do not change, and the old ones (/exphelp/viewer/en/us/<id>,?lang=en-us) are redirected or still accepted.Help maintained as content: DITA export, stored e-books, overdue reviews: on a site that runs help as content, setting a help object to Released now builds and stores the manuals' e-books as file objects (
EbooksOnRelease); run./console exp:help:content ebooks --applyonce after the deploy.PKCE and state for the OAuth authorization server:
ezprest_authcodehas two new columns,code_challengeandcode_challenge_method(in the database update below); until they exist no authorization code can be issued (step6.0.15-oauth-pkce-columns). Clients made in the administration are not affected otherwise. CORS for the REST API is new and off by default.The help's manuals made from the Markdown help: on a site that runs help as content, run
./console exp:help:content install --apply -s adminand./console exp:help:content build-manuals --apply -s admin --releaseafter the deploy, and again on each release.The public policies of the Anonymous role: an installation made between 12 and 19 October 2026 refuses every registration; run
php bin/php/addanonymouspublicpolicies.php --dry-run, then without--dry-run(--skip=user/registerkeeps registration closed). Step6.0.15-anonymous-public-policies(with the command's defaults: to keep registration closed, skip the step and run the command).php bin/php/addanonymouspublicpolicies.php(a dry run), then with--apply(--skip=user/registerkeeps registration closed).The everyday policies of the Editor role: editors of a new installation could not hide, restore from the trash, see what uses an object or multi-upload; run
php bin/php/addeditorpolicies.php(a dry run), then with--apply, and read itsWARNlines; since 30 October it also adds the interface choices and the media site's classes (settings/editorpolicies.ini) and removes duplicate policies. Step6.0.15-editor-policies.Member management: editors manage the accounts below Members, never those of Administrator users or Editors; run
php bin/php/addmembermanagement.php(a dry run), then with--apply(step6.0.15-member-management).user/settingnow also asks forcontent/editof the account ([MemberGuardSettings] SettingRequiresEdit).The installers write the cron job: new installations get the crontab lines that run their cronjob parts (the queue, the imports, the notifications, the search index); on an installation that exists run
php bin/php/installcrontab.php --add(a dry run), then with--apply, as the owner of the installation (or as root). The dashboard's block Cronjobs warns when the parts do not run.plain_site's Editor role without content/*: on an installation of
plain_site,php bin/php/addeditorpolicies.php(a dry run) now also listscontent/*to be replaced by the content functions the other packages give editors; run it with--apply(--keep-content-wildcardkeeps it).Approval of new user accounts: editors may create editor accounts, which wait for an administrator's approval; run
php bin/php/adduserapproval.php --dry-run, then without--dry-run, and clear the INI cache (--clear-tag=ini). Step6.0.15-user-approval; the finish stage clears the caches. When content/publish (before) runs a workflow already, which wait for an administrator's approval; runphp bin/php/adduserapproval.php(a dry run), then with--apply, and clear the INI cache (--clear-tag=ini). When content/publish (before) runs a workflow already, the event goes first into it.Help maintained as content: review workflow, "Was this helpful?", token clients, EPUB checks, history, public pages: on a site that runs help as content, the author of a help object can no longer release it (
ReleaseByAuthor), and two tables are new (exphelp_help_id,exphelp_feedback, in the database update); run./console exp:help:content install --apply -s adminandcheck -s admin --fixonce after the deploy. the event goes first into it. Approve and Deny of such an account ask "Verify it's you" (group users) first.The second step on the other sign-in paths: nothing changes by default. With
sevenx_authentication_2fa1.0.17, REST, SOAP, WebDAV and the expservices login still sign anEnforce2FAaccount in with its password alone. A site that wants one of them to refuse that lists it insevenxauthentication2fa.ini [OtherSignInPaths] Guard[](expservices,webdav,rest,soap); before that, give the clients of such accounts app passwords (user2fa/setup), API keys or OAuth tokens, and the expservices clients the fieldotp.OAuth tokens: disabled users, client binding, refresh token lifetime and token sources: a REST client that sends
?oauth_token=in the address gets401until it sends theAuthorizationheader (orrest.ini [OAuthSettings] AllowTokenInQuery=enabledis set); refresh tokens expire after 30 days (RefreshTokenTTL,0= never).The user audit log: a new table,
expuser_audit, keeps the security events about user accounts and groups (in the database update, orphp update/common/scripts/6.0/createuseraudittable.phpon any engine; step6.0.15-user-audit-table); until it exists nothing is stored there. Giveuser/auditto the roles that may read it, and run the cronjob groupinfrequent(or the partuseraudit) for its retention.Your devices: a new table,
expuser_session_info, keeps the host, the place and the second step of each signed-in session (in the database update, orphp update/common/scripts/6.0/createusersessiontable.phpon any engine); until it exists the sessions are listed without these details. Addresses of sessions are now stored as the audit stores them ([UserSessionSettings] RecordIP=audit: the network by default); setRecordIP=fullto keep full addresses.Commands only change things with --apply: the setup, policy, maintenance, mail, notification, crontab, installer and Velocity configuration commands only show what they would change unless
--applyis given. Add--applyto every script, runbook and crontab line that runs one of them to make the change (demo rebuild scripts:exp:install,collaborationsampledata,preload).ezpgenerateautoloads,exp:cacheandezcache.phpact at once as before.The SOAP entry script is now index_soap.php (Deutsch): on an installation with
soap.ini [GeneralSettings] EnableSOAP=true, letindex_soap.phpthrough the web server (the.htaccess, the HTTPS virtual host, Plesk's additional directives, the nginx server block) and point the SOAP clients (the imports ofsyndication, middleware) at it, then test withcurl(a GET answers "only understand POST methods", a POST of an envelope a SOAP fault, never404).soap.phpkeeps answering as a shim that writes a notice tovar/log/warning.logonce a day; it will be removed in a later major version.The WebDAV entry script is now index_webdav.php (Deutsch): where WebDAV is on, change the virtual host (HTTP and HTTPS) and the rewrite rules from
webdav.phptoindex_webdav.php: ApacheRewriteRule, Plesk's additional directives for HTTP and for HTTPS, nginxlocationandfastcgi_param SCRIPT_FILENAME, IIS rules, and any allowlist of root scripts.webdav.phpforwards toindex_webdav.phpand writes one warning a day towarning.loguntil it is removed in a later major version. Test withcurl -i -X PROPFIND -H 'Depth: 1' https://www.example.com/index_webdav.php/(207 Multi-Status). Velocity does not serve WebDAV under either name.Why Exponential supports PHP 7.4 (Deutsch): the extension packages that need PHP 8 moved from the root
composer.jsoninto the metapackagese7enxweb/exponential-php-floor. A project that copies the kernel'srequirekeeps working; one that lists those packages itself should keep them only where its PHP is 8.0 or later. Runcomposer updatewith the PHP that serves the site. Code that names the view classesExponential\View\Kernel\Class\...keeps working throughclass_alias; new code usesExponential\View\Kernel\ContentClass\....Fixes from the Opencontent and Mugo Web forks (Deutsch): a URL alias deeper than 30 path elements is not found (
site.ini [URLTranslator] MaxPathDepth), everyeZLog::write()entry is one line, uploads withphp4,php5,php7,php8,phps,shtml,cgi,pl,asp,aspx,jsporhtaccessare refused (file.ini [FileSettings] FileExtensionBlackList[]), andezjscnode::subtreereturns at most 500 items (ezjscore.ini [ezjscServer_ezjscnode] HardLimit). Nothing to run; raise a setting where a site needs more.The record of exp:upgrade is exp_migration: only an installation that ran a pre-release of 6.0.15 has the table
expupgrade_step. Step6.0.15-upgrade-record-tablecopies its rows intoexp_migration, checks that every row is there and drops it (dry run first:./console exp:upgrade:database). Every other installation has nothing to do.The switch to expcomponents (Deutsch): the kernel requires its libraries as
se7enxweb/expcomponents-*, Exponential's fork of the Zeta Components, instead ofzetacomponents/*. The libraries stage swaps them (composer installfrom the release's lock file; in a Composer projectcomposer update se7enxweb/exponential --with-all-dependencies). Sameezc*classes, no setting or database change. A projectcomposer.jsonthat pins an exactzetacomponents/*version must loosen or rename it; the mail fork refuses addresses with whitespace or< > , ; ".The site packages no longer carry ezpaypal or bcwebshop. ezpaypal is installed by Composer (the default install requires
se7enxweb/ezpaypal); the site packages (ezwebin_site,ezflow_site,ezdemo_site,sevenx_siteand their_cleanvariants) no longer require theezpaypal_extensionpackage, so the package server's older copy is no longer written over Composer's. Their installers still activate ezpaypal (ActiveExtensions[]=ezpaypalin thesite.inithey write). bcwebshop is no longer installed by any package (thesevenx_extensionpackage dropped it) nor activated by the installers: the kernel's shop covers what it added, and its shop module and kernel override conflicted with it. Existing installations keep working; nothing is removed. To move a site from bcwebshop to the kernel's shop: removeActiveExtensions[]=bcwebshopfromsettings/override/site.ini.append.php(and any siteaccessActiveAccessExtensions[]=bcwebshop), runphp bin/php/ezpgenerateautoloads.php -eandphp bin/php/ezcache.php --clear-all, then check the basket, checkout and order list (Webshop tab); orders, VAT rules and currencies are kernel tables and stay. A site that installed ezpaypal from the package server and now gets it from Composer needs nothing more thancomposer update(theextension/ezpaypalfolder becomes Composer's).
The database update for the line is update/database/<engine>/6.0/dbupdate-6.0.0-6.0.15.sql, followed once by
php update/common/scripts/6.0/createaudittables.php; ./console exp:upgrade:database does the same on every engine
in 21 steps (6.0.15-upgrade-record-table to 6.0.15-site-version), and ./console exp:upgrade:scripts the update
scripts of update/common/scripts/6.0/ (6.0.15-missing-user-settings, 6.0.15-trash-records-to-columns; the
audit, API key, bookmark, passkey, session, transfer and user audit tables are database steps). Not steps: the
crontab (item 16), the help content commands (items 10, 12 and 19) and the demo mode variable (item 8).
How this page is organised
- July and August 2026
- 16 to 30 September 2026
- 1 and 2 October 2026
- 3 and 4 October 2026: notifications
- 4 October 2026: e-mail preferences and consent
- Required packages
- From 5 October on, one section per change, in date order; the newest ones: the content finder, a JSON format for packages, Exponential Velocity streams, Exponential Intelligence in the distribution, Setup > Packages end to end, the audit console's extension sources, registries declared in rad.ini, the system card: exp:expfetch, exp:info and exp:about, Exponential Velocity is part of Exponential, Exponential Layouts and the required packages of 10 to 12 October
June and the first half of September are told on the chronicle pages June 2026, second half and September 2026, first half. Every single change, with its commit, is in the ledger of the root repository.
July and August 2026
The changes between 1 July and 31 August 2026. The stories are in July 2026 and August 2026; the upgrade checklist is Behaviour changes of July and August 2026.
Added
ezpm.php, the package manager, runs throughbin/php/console. It installs content packages with a lock, delayed indexing, batched commits, a destination node and a progress line. Feature.bin/php/resetuserpassword.phpresets a password, authorised by an administrator or by the root user. Feature.- The Kickstarter (
bin/php/kickstarter.php,exp:kickstarter) installs a site fromkickstart.ini. Feature, upgrade notes. - The Template Editor orders overrides by priority, chooses the source, matches by object id, edits and removes match conditions, and has a menu entry. Feature.
ShowTemplatePathComments(site.ini [TemplateSettings]) wraps every template in START and STOP comments that name the override source. Feature.AdditionalExtensionDirectories[]andeZExtension::extensionName(): extensions in more than one folder. Feature.- Icon themes in extensions (
icon.ini [ExtensionSettings] IconExtensions[]) and a defaultjobicon. Feature. - Hooks for a Redis or Valkey content view cache and compiled INI cache. Feature, the extension.
- Setup > Extensions sorts, shows info cards and downloads an extension in four formats. Feature.
- The installation name output filter, and the
installation_nameandis_production_systemoperators. Feature. fetch( 'content', 'class_list' ),ezscript.ini [eZScriptSettings] RootDelay, a Layouts tab in the admin node view (Layouts), and the PHPUnit workflow of GitHub Actions. Specification.
Updated
- The setup wizard needs privileges only on your own database; packages install their dependencies first; the seed
data defaults to
eng-USand describes Exponential; the kickstart siteaccesses aresiteandadmin. Feature. - Security: tokens and activation hashes come from
random_bytes(); header and shell injections are fixed; a login fails closed when the user has noezuser_settingrow; CDN scripts load over https; jQuery 3.7.1 with Migrate 3.4.1 at that time. Specification. - HTML5 markup in kernel and design output; PHP 7.4 to 8.5 clean-ups; INI files are saved so that untouched keys stay
as they were; permission SQL works with empty limitation lists; imported Ibexa DocBook output; better diagnostics
for image aliases and missing objects; SQLite schema and debug output fixes; MongoDB logs through
eZDebug. Upgrade notes. - The media theme directory is part of the repository, no longer a submodule.
- The default sort of the extension list is the loading order since 29 September, not A to Z. Feature.
Renamed
kickstart.iniis shipped askickstart.ini--example.- The progress reporter
eZPMStatusisexpScriptStatus.
Related: Continuous integration and the test suite,
Template override ordering (how the editor's Priority is used).
16 to 30 September 2026
The story is in September 2026, second half; the upgrade checklist is Behaviour changes of 16 to 30 September 2026.
Added
- Velocity (
./console exp:velocity): a persistent-worker application server with the Qbix, FrankenPHP and PHP built-in engines, HTTPS, HTTP/2, and thedeploy,config,sslandextverbs. Feature, Deploying guide. - The server control commands
exp:webserver,exp:frankenphpandexp:solr. Feature. ./console exp:install --apply: an installation in one command on SQLite, MySQL, PostgreSQL, MongoDB or Oracle, with the logsvar/log/setup.logandvar/log/kickstart.log. Feature, specification, Getting started.- Maintenance mode (
bin/php/maintenance.php,exp:maintenance, Setup > Maintenance). Feature. - The role-aware HTTP cache and the SQL query cache, both off by default. HTTP cache, SQL query cache.
- The store dashboard (
shop/dashboard) with fourteen order statuses, and permanent order receipts (shop/orderreceipt/<token>,shop.ini [OrderViewSettings] OrderLinkView). Store dashboard, order receipts. - A package contents browser, package compare and selective import (
package/compare), package licenses frompackage.ini [LicenseSettings], and Semantic Versioning for packages. Compare and import, licenses and versions. - Cache clears that move directories aside (
site.ini [FileSettings] RenameBeforeDelete) and thecache_cleanupcronjob part. Feature. - The "form expired" 403 page (
design:error/kernel/6.tpl). Feature. - The loading order card of Setup > Extensions and
[SiteAccessSettings] ExtensionSettingsSiteAccess; drag and drop order of template overrides; policy IDs, sorting and order buttons in roles; moving a class attribute to the top or bottom. Loading order, override ordering, role policy order, class attribute move. exp:cache;./console --version,--aboutand--copyright; view templates for DocBook tags. Cache console, chronicle.
Updated
- Security defaults: headers on every page, the session cookie, uniform answers of login and forgot-password,
stricter file rules,
[HttpCacheSettings] TagHeader. Specification, Security and audit guide. - About 60 datatype and helper fixes: arrays posted as text, script URLs, directory parts in file names, header and INI injection. Specification.
- Driver and installer fixes for SQLite, PostgreSQL, MySQL, Oracle and MongoDB; SQLite is the wizard's default. Specification, MongoDB.
- The administration: the content structure tree starts at the top node; the header tabs read Media, Users and Store; the About and Copyright pages list every extension with description, license and website. Content tree, About and package pages.
- Translations: a finished
eng-US, complete German, and validlangvalues for twenty locales. Feature. - Translations: the French administration is complete, a new and complete Ukrainian one is offered by the interface language switch, and the German one uses one term per concept (Siteaccess, Extension, Sektion, Eigentümer, Ausstehend, USt) with the wrong translations of "Action" and "Discard" corrected. Languages.
- Translations: the German administration passes the translation quality test, which now checks it too: lost placeholders, tags, entities, spaces and line breaks restored, and the English texts left in it translated. Languages.
- Start preloading runs in the background. Feature.
Removed
- The setting
AnonymousOrderLinkView: every order links to its receipt whenOrderLinkView=orderreceipt. - The SQLite demo data files that no installer loads.
Renamed
[HttpCacheSettings] ProxyHeadersisTagHeader;ProxyHeaders=enabledin an override still meansxkey.- The site's service worker is
/index.js;/sw.jsloads it. - The version class is
ExponentialSDK, and the example crontab isexponential.cron. - The engine package is
se7enxweb/exponential-velocity. - The top-level alias of Setup is
x-setup.
All five: upgrade notes.
1 and 2 October 2026
The story is in October 2026; the upgrade checklist is Behaviour changes of 1 to 2 October 2026.
Added
- Bookmark folders: bookmarks can be organised in a tree of virtual folders of any depth, in the bookmark page,
the Bookmarks box, the browse dialog, the online editor (TinyMCE 3 and 8) and the
expbookmarkremote services. Needs the database update (a tableexpbookmark_folderand two columns onezcontentbrowsebookmark); existing bookmarks stay at the top level and the flat bookmark list is unchanged. Feature, upgrade. - Added: Bookmarks of system pages: "Bookmark this page" on every module view of the administration keeps the page's
address with its parameters (without the siteaccess) in the user's bookmark folders, with a name of its own; the
Bookmarks box and My bookmarks list both kinds, and a page the user may no longer open is shown without a link.
New table
expbookmark_view(update/common/scripts/6.0/createbookmarkviewtable.php). Feature, upgrade, help. - Added: Bookmarks of system pages know what they are and why they cannot be opened. A search is bookmarked with its
search text (a saved search, with a magnifier and the type Search); every other system page has the icon of its
module. A page that cannot be opened in the siteaccess it is shown in is greyed with the reason (switched off in
this siteaccess, no role allows it, the view no longer exists ...). Views that only act on a posted form, sign in or
out or edit cannot be bookmarked. All of it is set in the new
settings/bookmark.ini, where an extension also adds its own kind of bookmark (expBookmarkType); the button is in the admin3 design too. The help shows every step in admin4, the classic design and the editor interface, light and dark, in English and German. Feature, help, bookmark.ini. - Added: A bookmark of a system page keeps its address taken apart as well:
expbookmark_view.module_name,view_nameandview_parameters(JSON of the ordered, unordered and query parameters), indexed by module and view, so bookmarks can be checked and moved by module and view.createbookmarkviewtable.php --rename=<old>=<new>moves the bookmarks of a renamed module or view. A table made by an earlier 6.0.15 build gets the columns from./console exp:upgrade --apply(steps6.0.15-bookmark-view-columnsand6.0.15-bookmark-view-fill) orcreatebookmarkviewtable.php; bookmarks keep working until then. Feature, upgrade. - Audit trail: a hash-chained, signed, searchable record of sign-ins, content, settings, commerce and deploys.
It comes with the
exp:auditcommand, the Audit tab and console, sinks (syslog, journald, signed webhook, mail), alert rules, archives in five formats and the import of the 4.x logs. On by default. Feature, specification, upgrade notes. - Content jobs: large remove, copy, move, hide, section, state and location operations run as resumable
background jobs, with
content/job/<id>,content/jobs,./console exp:expcontentjob, thecontentjobscronjob part and[ContentJobSettings]. Feature, upgrade notes. exp:ini, a settings editor that changes INI files line by line in every scope. Feature, upgrade notes.- The Exp Debug bar: tabs, a live summary, settings with presets and undo, cache controls, minimise. Feature, upgrade notes.
- Table options of the sub-items list: 129 registered columns, attribute columns, presets, CSV export, columns defined by INI and PHP or template. Feature, upgrade notes.
- Request rules (
settings/requestrules.ini,bin/php/ezrequestrules.php) and the policycontent/view_system_url. Feature, upgrade notes. - expservices: about 1,100 remote services over ezjscore, personal API tokens,
expfeed::runImport, theexpeditorservices and two portal front ends. Feature, specification, upgrade notes, Remote services guide. - The online editor on TinyMCE 8.9.2, an opt-in second engine, with ezoe dialogs for embed, links, custom
tags, literal tags, tables and general tags, an engine registry,
/ezoe/engineand browser tests. Feature, upgrade notes. - The admin4 design, light and dark. Feature.
- The editor siteaccess and design, installed by the setup wizard, with
EditorAccess,EditorAccessPortandEditorAccessHostnameinkickstart.ini; a new look for the setup wizard. Feature. HiddenTabs[]inmenu.ini [TopAdminMenu]andadmininterface.ini [WindowControlsSettings]. Feature.- A page for missing Composer libraries that repairs the installation from the browser, with
exprepair.php. Feature, upgrade notes. Exponential\Runnablebase classes (Runnable,Command,CronjobPart,ModuleView), events around every command, cronjob part and view,[RunnableSettings], and shared services (trash, sessions, drafts). Specification, upgrade notes.- Every column of the order list sorts it. Feature.
- The trash records who trashed each item and from where. Feature.
- A queue for SQLite writers: a transaction waits at its start for the writers ahead of it, up to
[DatabaseSettings] SQLiteTransactionWaitseconds (default 25). Upgrade notes. - An opcode cache profile for comparing engines, off unless the file
var/tmp/opcache_profile.onexists. Feature. - English and German texts for the debug bar, the audit console, content jobs,
setup/radand about 500 other strings that showed up untranslated.
Updated
- jQuery 4.0.0 and jQuery UI 1.14.2 in ezjscore 1.4.0; the admin runs on jQuery 4 and Exponential UI. Since
2026-11-02
ezjsc::jqueryloads jQuery 4 without jQuery Migrate: Migrate 4.0.2 is still shipped, but not loaded unlessezjscore.ini [eZJSCore] LocalScripts[jqueryMigrate]names it. Feature. - The SQL query cache knows Oracle's statements (ezoracle 2.3.2). Specification.
- The static cache refreshes the pages of a publish once each, in parallel (
staticcache.ini [CacheSettings] FetchConcurrency, default 8). In the measured case 4975 ms became about 500 ms. Feature. - Velocity keeps the kernel's regenerated cache files in the opcode cache (
file_update_protection0 for the server), so a rendered page costs about what it does under PHP-FPM. Before, the opcode cache served 2.7% of the includes under Velocity against 97% under PHP-FPM; 644 ms against 166 ms of CPU. Feature. - The admin dashboard, menus and top tabs show a link only to a user who can open it. Feature, upgrade notes.
- The online editor's upload checks the file type on the server; poll votes, form submissions and the VAT country
choice need
content/read. Upgrade notes. exp:velocity deployalways clears thedesign_basecache and, with--packer, the ezjscore packer cache. Deploying guide.- Admin details: the header search popup works in both copies; the Store sidebar lists orders and products first; long names in the side columns wrap; the page-not-found error names the missing page; the debug report remembers whether it is open.
- The classic grey administration design has the Exponential logo, a left menu that collapses and resizes, and the Layouts pages in its look. Every logo image of the kernel designs shows the Exponential logo.
- Every copyright notice names 1998 - 2026 7x & Exponential Foundation first. Specification.
- Kernel commands, cronjob parts and module views are classes; their entry points are unchanged. Every command shows
a description in
bin/php/console list. - Empty trash purges in batches, like the command; Remove timed out sessions also removes their baskets; the
clusterpurgepart uses 30 days; a copy of an object with an image owns its own image files. - Fixed: SQLite "database is locked" under concurrent writes (transactions now queue for the write lock);
subString()without a length; the asynchronous publisher shares no connection with its fork and no longer stops with a type error on PHP 8; the template path comments of a request no longer read another siteaccess's override list. - Fixed: PHP 8.5 notices on Oracle (preferences, class attribute categories,
ezpExtension::getInfo, the template compiler,eZHTTPTool::redirect(), the INI setting datatype,ristring,{if});E_STRICTin the tree menu and the REST front controllers; recurring warnings in the logs. - Fixed: removing a draft removes every image file of that draft; removing a media attribute keeps a file that
another row names; removing a subtree that holds every location of an object removes the object; a node's
path_identification_stringis rewritten from the stored value; removing a VAT rule no longer stops with a fatal error; the PDF export of a content node no longer answers 500. - Fixed: the CSS packer keeps the space before a colon in selectors; the Velocity warm-up never takes the setup wizard's lease.
- Fixed: a module redirect carries the request query (
?gclid=...) only to relative targets and to targets on the own host; redirects to another host, including another host-matched siteaccess such as the admin host, and protocol-relative (//host) or other-scheme targets are left untouched, so a sealed payment URL is no longer invalidated by tracking parameters. The query joins with&when the target has one and stays before a#fragment. See the redirect note. - Fixed:
ezjscore/callanswers a PHPErrorin a server function with a generic text and writes the details to the log, instead of breaking the JSON response; exceptions still pass their message, and the{ ok, error }envelope of the remote services is unchanged. - Fixed: an image attribute that never stored a file no longer asks the image manager for every alias on every request.
- Fixed: the lazy image alias write-back no longer reverts a published image to stale draft paths.
- New releases of the required extensions; see Required packages.
Removed
- YUI, from the admin, admin3, admin4 and standard designs and from the bundled ezjscore 1.5.0: the YUI libraries,
ezjsc::yui2,ezjsc::yui3,ezjsc::yui3ioand their settings, and the YUI 2 calendar script of the standard design. Upgrade notes.
Renamed
- explayouts 1.4.10: the component blocks are
exp_component_<type>, and block links name their node by its remote id. Two upgrade scripts rename the stored blocks and links. The media theme 1.5.29 and expsite_data_media 1.0.6 follow. - The debug report, and the bar, are named "Exp Debug".
3 and 4 October 2026: notifications
The upgrade checklist is Upgrade notes: notifications. The system is described in the user's guide, the administrator's guide and the specification.
Added
- Notification status page (Setup, Notification) with pending events, digest items, subscriptions, the last runs and a problem list; Run now and a dry-run preview as background jobs; cleanup of handled events, old events and subscriptions to deleted content.
- Commands
exp:notification:status,run,eventsandsubscriptions(aliasesexp:notify:*), with--dry-run,--mail-file-dir(mail to files for one process),--at,--event. Command reference. expNotificationService: one place that runs, plans, records and audits notification runs, under a lock. The cronjob partnotificationcan run alone (php runcronjobs.php notification).- Confirmation pages for adding and removing a notification, and guides for administrators and developers, the settings reference and the command reference.
- Failed mail is kept: a notification the mail transport refuses stays in the queue and is tried again at every run for
[RuleSettings] RetryHours(default 72); the run record,exp:notification:run(exit code 1) and the status page report it, and a message older than that is given up and counted.
Updated
- My notification settings is an overview (admin4, admin4l): the followed items with path, class and last change, a filter, paging, a confirmed removal, notices, a clearer digest and collaboration choice; English and German texts.
- Opening
/notification/addtonotification/<node>asks first instead of subscribing on a plain GET. - The notification filter survives a failing handler, can run for given events, and removes handled events nothing waits for.
- Fixes: the digest settings are validated and a user without a settings row can save them, a single address no longer
breaks the mail transport, the subtree form refuses unreadable, missing and duplicate nodes, collaboration rules are looked
up by user id,
eZNotificationCollection::addItem()stores its send date andfetchByDate()finds due items. eZFileTransportwrites to[MailSettings] FileTransportDirectory(defaultvar/log/mail).
4 October 2026: e-mail preferences and consent
The upgrade checklist is Upgrade notes: e-mail preferences. The system is described in the user's guide, the administrator's guide, the developer's guide, the specification and the law checklist (CAN-SPAM, GDPR and ePrivacy, CCPA/CPRA, CASL, RFC 8058).
Added
- E-mail preferences: one page (
/mailpreferences/settings, also in the administration under My e-mail preferences, and without login by a personal link) where every person turns each optional kind of e-mail on or off, chooses how often, turns all optional e-mail off with one button, sees the history of their choices and downloads their data as JSON or CSV. Every optional kind is off until the person turns it on. Essential mail (account security, orders, legal notices, admin alerts) is listed and always sent. - The mail gate in
eZMailTransport::send(), for every transport: mail declares a category witheZMail::setCategory(); optional mail goes only to people who turned it on, one mail each, with a footer (why, one-click unsubscribe, preferences, the organisation and its postal address) and the RFC 8058 headersList-UnsubscribeandList-Unsubscribe-Post. Mail without a category is sent as before and counted. - Unsubscribe in one click (
/mailpreferences/unsubscribe/<token>), also from the mail program's own button; a page opened by a link changes nothing until its button is pressed. Double opt-in for newsletters, offers and a new e-mail address. Send me a link for people without an account. - Consent log of every change with the exact text shown, the source, the IP address and the acting user; suppression
list stored as salted hashes; links encrypted with AES-256-GCM under a site secret; export and erasure that keeps the
proof of a withdrawal; retention by the cronjob part
mailpreferences(groupinfrequent). - Administrator's pages under Setup > E-mail preferences: status, categories (create, rename, describe), suppression
list, consent log with filters and CSV export, a user's preferences. Policies
mailpreferences/administrateandmailpreferences/export. - Commands
exp:mail:status,exp:mail:preferences,exp:mail:suppression,exp:mail:consent,exp:mail:gate. Command reference. - Unticked boxes for the optional kinds on the registration form; English and German texts.
- Tables
expmail_category,expmail_preference,expmail_consent_log,expmail_suppression,expmail_pendingin the kernel schema and the database update. - Tests:
MailPreferencesCoreTest(20 cases of the core and the gate) andMailPreferencesComplianceTest(one case per law requirement of the checklist), live style with the file transport and.invalidaddresses.
Updated
- Optional mail to several recipients is sent as one mail per recipient (
[GateSettings] SplitRecipients). - The profile, the notification settings, the current user box and the menus link to the e-mail preferences.
- The text footer of optional mail keeps its line breaks: each link ends its line.
- Collaboration mail follows the frequency chosen on the preference page: daily or weekly it goes into the digest, which now has a collaboration part, and the digest lists its items again (its loops and includes rendered nothing).
- A new e-mail address of an account (user/edit, the administration's edit, the account services) takes effect only once
it is confirmed from the new mailbox, and the old address gets a notice (
[EmailChangeSettings] Confirm). - The downloads of a person's e-mail data, the consent log and suppression list exports and the RFC 8058 one-click
answer end the request through one helper (
MailPreferencesPage::sendResponse()). Under Velocity they never answered (504) although the change was saved: the loop that emptied the output buffers did not stop at the worker's own buffer. The setup views that stream (preloader, static cache, cronjob log) and the Ajax answer of the cronjob console had the same loop; all of them now useeZExecution::discardOutputBuffers(). - The history shows a waiting e-mail address change as E-mail address: New address asked for, waiting for confirmation, no longer as "All optional e-mail".
- The footer and the confirmation and "send me a link" mails name the public site (the siteaccess of the request when
it is public, else the default siteaccess), never an administration siteaccess, also from a command or a cronjob;
OrganisationNamestill wins when set. - Closed gaps of the law checklist:
DefaultOn=trueof an optional category is treated as off and shown on the status page unless[CategorySettings] AllowDefaultOn=enabled; removing an account erases its e-mail preferences and anonymises its consent log (the withdrawals stay as proof, the suppression list stays); optional mail with an empty From gets[MailSettings] EmailSenderorAdminEmail(logged, and not sent without either); the footer and the preference page link the privacy notice ([FooterSettings] PrivacyURL, else the node of menu.ini[SiteInfo] PrivacyPolicyID); unsubscribe and manage links never live shorter than 60 days. - Tests:
MailPreferencesHardeningTest(MPH-01 to MPH-09), including the one-click POST and the downloads over HTTP on the site's web server and on Velocity.
5 October 2026: PHP 8.0 is the oldest supported version
PHP 8.0, the stock PHP of Red Hat Enterprise Linux 9, runs the kernel and the shipped extensions again (Velocity needed 8.1 until its release 0.0.4.54 of 20 October 2026, which runs on 8.0 too; see 20 October 2026). Details, the full list of findings and the RHEL 9 notes: PHP 8.0 support.
Added
lib/phpcompat.php:array_is_list()for PHP 8.0, read byautoload.phponly on PHP older than 8.1.- The Quality workflow's job
php80:php -lwith PHP 8.0 on every PHP file, a production install with 8.0 and the boot smoke testtests/bin/bootsmoke.php(every kernel and extension class, the compatibility functions, a template withwash);make lint80andmake lint80-all.
Updated
htmlspecialchars()and the other HTML escaping functions get the PHP 8.1 default flags explicitly in 28 kernel, library, ezjscore, ezoe and expservices files, thewashoperator and the simple tags operator included: single quotes are escaped on PHP 8.0 as on 8.1 and later. Clear the template cache once after upgrading.- The CSV export of sub-items writes its
"\r\n"lines on PHP 8.0 too (fputcsv()has no$eolthere). - The OAuth authorize view passes
implode()its arguments in the order PHP 8 requires; the LDAP login no longer callscrypt()without a salt. - The checks of pull request #113 test PHP 8.0 instead of 8.1:
phpcs.xml.disttestVersion8.0-, the pre-commit hook (EXP_PHP80),composer.dev.jsonplatform PHP 8.0.30. - README, the PHP 8 page, the console and CI pages state PHP 8.0 as the minimum.
5 October 2026: related objects sorted by class name
Updated
- Fixed:
eZContentObject::relatedObjects()(and the fetchesrelated_objectsandreverse_related_objects) sorted byclass_nameno longer fails on MySQL and PostgreSQL with "Unknown column 'ezcontentclass.id' in 'on clause'":ezcontentclassis now the last table of the FROM list, the one the class name join can see. SQLite was not affected. The other queries that append the sorting joins ofcreateSortingSQLStrings()(the node list and count ofeZContentObjectTreeNode, the content structure tree, the keyword fetch, the trash list) were checked: they build their FROM as a chain ofINNER JOINs that includesezcontentclass, so the class name join sees it; onlyrelatedObjects()used a comma list.
5 October 2026: database sessions
Updated
- Fixed:
ezpSessionHandler::setSaveHandler()no longer callssession_module_name( 'user' ), which throws aValueErroron PHP 8 and stopped every request of an installation with[Session] Handler=ezpSessionHandlerDB. See bug 4 on the session handler page. - Updated:
setSaveHandler()gives PHP the handler as an object (ezpSessionHandlerAdapter, implementingSessionHandlerInterface) instead of six callbacks, which PHP 8.4 reports as deprecated on every request; the handlers themselves are unchanged. See bug 5 on the same page.
5 October 2026: view cache files of cache_ttl=0
Updated
- Fixed: a node view whose template sets
cache_ttlto 0 no longer stores the whole page in its view cache file. The file keeps only the no-cache advice (no_cache,cache_ttl0, a few bytes), which the next requests still read to generate the page without the cache lock and without storing it. The number of files stays the same: one per cache key, as before.
5 October 2026: expiry timestamps per siteaccess VarDir
Updated
- Fixed: with siteaccesses that have a
[FileSettings] VarDirof their own (several sites on one installation), clearing a cache of one site no longer expires the caches of every site. The expiry handler is created before the siteaccess is known, with theexpiry.phpof the default VarDir, and kept it;eZSiteAccess::change()now callseZExpiryHandler::resetForCurrentCacheDirectory(), which stores the old instance and drops it when its file is not the one of the current cache directory. Installations with one VarDir for all siteaccesses see no change. Under Exponential Velocity the shared instance does not outlive a request either:$GLOBALS['eZExpiryHandlerInstance']is not invelocity.ini [ApplicationSettings] KeepGlobals[]or its built-in defaults, so a persistent worker readsexpiry.phpanew for every request and sees the timestamps another worker or PHP-FPM wrote.
5 October 2026: benchmark command
Added
./console exp:benchmark(exp:bench,bin/php/benchmark.php): measures an installation. HTTP mode times the front page, the first menu pages and the admin login page (or--url,--urls-file) with PHP's curl, warm or--cold(a unique query string past every response cache), and compares two or more servers back to back with--compare=A,Bin alternating rounds over kept-alive connections. Kernel mode times boot, the full view render cold and warm, INI load, content fetch, node list, database round trip, cache write and read and an image alias lookup in-process. Output as a table with min, median, p95, p99, max, requests per second, errors, status codes, bytes and cache answers, or--json/--csv;--savewrites a run and--baselinecompares with one, exiting with status 1 when a metric got worse by more than--threshold(15 %) and the--min-deltanoise floor (1 ms).ab,wrkandohaare used with--tool=when installed. Read-only and polite by default (100 requests, concurrency 4); more than 1000 requests or a concurrency above 16 against a host that is not loopback needs--force, and a host the site does not serve is only measured when its URLs are named. Guide: Benchmark.expBenchmark,expBenchmarkHttpandexpBenchmarkKernel(kernel/classes/), with tests intests/tests/kernel/classes/benchmark/that need no network: the HTTP client is tested against aphp -Sserver on 127.0.0.1 that the test starts and stops.
5 October 2026: digest send date without an item
Updated
- Fixed:
eZNotificationSchedule::setDateForItem()acceptsnullas item and then only returns the next send date of the digest, instead of failing with "Call to a member function setAttribute() on null". A digest handler of an extension that needs the date without an item no longer stops the request. The kernel handlers always pass an item and are unchanged. - Fixed: a digest goes out at the chosen hour of the local clock across a change between summer and winter time; the
send date was the current time plus a fixed number of seconds and was an hour off then. The date is now built from
calendar fields (
eZNotificationSchedule::nextSendDate()). - Fixed: a monthly digest on a day the next month does not have (the 31st, in a month with 30 days or in February) falls on that month's last day; it used to run into the month after (31 January + 31 days = 3 March).
- Added:
setDateForItem()takes the time to compute from as an optional third argument, for tests.
5 October 2026: edit access to objects that were never published
Updated
- Fixed:
eZContentObject::checkAccess( 'edit' )on an object that was never published:- The rule that someone who may create the object under its parent may edit it applies at every version, not only at version 1; a rejected first version is edited as version 2 and later. Without a node assignment the rule denies instead of failing with "Call to a member function attribute() on null".
- A Subtree limitation also counts the location the object will be published under (the parent of the main node assignment of the current version) for edit, so an approver with an edit policy for that subtree can edit it. Read and the other functions are unchanged, the owner rule too, and a policy for another subtree still gives no access.
- Updated: the same edit rules reach every place that wrote them out. A role for edit assigned for a subtree
(User_Subtree) counts the location an object that was never published will be published under, as the Subtree
limitation does;
eZContentObjectTreeNode::checkAccess()(the preview node ofcontent/versionview),eZContentObjectVersion::checkAccess()andcontent/removeeditversionapply the create rule at every version and deny without a node assignment, through the neweZContentObject::draftCreateAccess(); the rule reads the main node assignment (draftMainNodeAssignment()), not the first one. A parent node that no longer exists is skipped instead of warning. Who may now edit what, and how a site notices: Edit access to objects that were never published. Tests:eZContentObjectDraftLocationTest(no database),eZContentObjectDraftAccessLiveTest(live).
5 October 2026: new version of an object that was never published
Updated
- Fixed: a new version of an object that was never published keeps the location of the version it is copied from.
eZContentObject::copyVersion()skipped every node assignment without remote_id, andcreateNewVersion()rebuilds the others only from existing nodes, which such an object does not have; after an approval workflow rejected the first version, editing it again lost the location, and publishing asked for one again (or used the default location from the version view). A location still to be created (op_code CREATE without remote_id) of an object that was never published is now copied as it is. Published objects are unchanged.
5 October 2026: version preview path of an object that was never published
Updated
- Fixed: the path of
content/versionviewfor an object that was never published shows the folder the object will be published in. The preview node of such an object has no node ID, so its path string ends with the parent node andfetchPath()dropped the parent as the node's own.\Exponential\View\Kernel\Content\Versionview::previewParentNodes()reads that path including the last element; published objects are unchanged. - Updated:
eZContentObjectTreeNode::fetchPath()(thepathattribute) reads the path of a node without node ID including its last element (pathStringEndsWithParent()), so a template that reads$node.pathof the preview node gets the folder too;previewParentNodes()uses it. Test:eZContentObjectTreeNodePreviewPathTest(no database).
5 October 2026: changing your password
Added
user/passwordlists the password's requirements and reports each problem next to its field, with a summary at the top that links to the fields. A successful change gets its own success state. The rules are in one class,expPasswordPolicy. The optional rules are in the new blocksite.ini [PasswordSettings](lowercase, uppercase, digit, symbol, a number of character kinds, not the login, not the current password); all of them are off by default, so no site's rules get stricter. Guide: Changing your password.- After a change the session id is renewed. With the database session handler the user's other sessions are ended
(
EndOtherSessions, new methodezpSessionHandler::deleteOtherSessionsOfUser(), audit eventaccess.session.revoke), and the mail "Your password was changed" is sent in the essential categorysecurity(ChangeNotificationMail, templatedesign:user/password_changed_mail.tpl). The mail never contains the password. - Template variables for the modern page:
$password_rules,$field_errors,$error_summary,$password_changed,$min_length,$password_js_config_jsonand others. Old templates keep working unchanged. See the compatibility note. - Translations: the contexts
kernel/user/password,kernel/user/password/js,kernel/user/password/mailanddesign/standard/user/password_changed_mail, in English and German.
Updated
user/passwordno longer writes typed passwords back into the page ($oldPassword,$newPasswordand$confirmPasswordare always empty). When the current password is right, every problem with the new password is reported at once.access.user.password.change.failednames the rule that failed. Test:expPasswordPolicyTest.
5 October 2026: workflow process list
Updated
- Fixed:
workflow/processlistlists a waiting workflow process that has no main operation memento, for example one deferred to cron. The trigger is read from the child memento, and the main memento was never used; requiring it left such processes out, so the list could be empty although workflows were waiting. The grouping is\Exponential\View\Kernel\Workflow\Processlist::processesByTrigger(). - Updated:
processesByTrigger()takes the function that finds a process's trigger as an optional second argument (defaultProcesslist::triggerOfProcess(), which reads the child memento), looks each trigger up once per listing and skips a memento that does not name its operation instead of warning. Test:eZWorkflowProcessGroupingTest(no database).
5 October 2026: more tests, and what they found
Added
- Unit tests that need no database, in the suites CI runs: the template engine (about 150 sources rendered
processed and compiled, both must match), every locale of
share/locale, the input validators, character transformations and code pages,eZMimeType,eZPHPCreator, the PHP error handler ofeZDebug, the template operatorsmonth_overview,autolink,simpletags,alphabetandeztoc, the author, option, range option, multi-option and matrix values, ISBN checksums,toString()/fromString()of the simple datatypes, object name patterns, mobile device detection, administration list paging, and the REST routes, prefix filter, request parser and OAuth token lookup.tests/tests/kernel/commonis part of thekernel-classessuite. Test counts and line coverage: continuous integration.
Updated
- A debug message no longer removes the PHP error handler installed below eZDebug's (
setHandleType()restored a handler forHANDLE_TO_PHPandHANDLE_NONE, which install none). - A template edited while a process runs (a persistent Velocity worker, a CLI script) is no longer compiled again from the tree parsed before the edit; the stale tree had made the new compiled file carry the old template for every process.
nl2brgives the same output processed and compiled (<br />before each line break).eZLocale::internalCurrency()uses the currency's thousands separator; a locale code with a charset such aseng-GB.utf-8keeps its digits;swe-SEnumbers use the full stop for thousands instead of the comma that is also their decimal symbol, so "1,00" is no longer read as 100.eZCharTransform::transform()sets the charset before naming its cache file; the bork translator no longer warns on a text that starts with a placeholder.eZDateTimeValidatoraccepts digits only in a time and checks the seconds of a date and time;eZRegExpValidator::fixup()no longer changes the validator's own rule.eztocescapes the header text it prints;month_overviewno longer warns without a next month.- A range option with a decimal step no longer loses its last value (0 to 0.3 by 0.1 has four options).
- Mobile device detection sees a WAP type that is the first of the Accept header.
- Object name patterns: several groups in one pattern, the alternative after a group
(
<(<first_name> <last_name>)|login>) and a group without values resolve correctly; objects whose class uses such a pattern get the corrected name when they are next published. - REST: a request no longer keeps the API provider and version of the request before it in a persistent worker, and
ResponseGroups[]or_method[]in a request no longer end it with a TypeError. sum,sub,mulanddivgive the same result processed and compiled, with the fractions of their operands:sum(1.5,2.25)is 3.75 (it was 3 processed),div(1,0.5)is 2; a word or a division by zero in a variable gives 0 in a compiled template too, where it was a fatal error.- Compiled
lt,le,gtandgecompare counts as the processed ones do (array count, string length, the number); they compared the raw values, solt(array(1),array(2))andgt("abc","z")differed. eZDebug::printReport()returns the report when$returnReportis set, so command line scripts write their debug report to STDERR instead of printing it with their output.- Currency conversion applies shop.ini
[MathSettings] RoundingType(none, round, ceil, floor) and a type set withsetRoundingType(); every type had come out as none. eZURI::convertFilterString()writes the converted namefilter back and returns it; a URI's namefilter is decoded once, as before.gettimegives a Sunday its ISO-8601 week number (date( 'W' )); it was one higher, week 54 for 3 January 2021.
5 October 2026: other sessions end after a password change; the same rules on every password form
Added
- A sign-in keeps a password stamp in the session (
eZUserPasswordStamp: a keyed hash of the user id and the stored password hash, made with the site secret; never the hash itself).eZUser::currentUser()signs out a session whose password has changed since, so a password change ends the user's other sessions with every session handler. The session that made the change stays signed in. Sessions from before the upgrade, the anonymous user and accounts without a password of their own (LDAP, SSO) are not checked. The setting is[PasswordSettings] EndOtherSessions; the audit event isaccess.session.revoke(reasonpassword_changed); the user/password page has the new template variable$other_sessions_signed_out. Guide: Changing your password. - Translations: the context
kernel/user/password/account, in English and German.
Updated
Registration, content/edit of a user account, the administration's user edit and
bin/php/resetuserpassword.phpcheck passwords with the rules of user/password (expPasswordPolicy::accountErrors()). With the defaults this is the length check of before, with the same sentence. The edit form's unchanged password (_ezpassword) is no longer length-checked, which refused every edit of a user that left the password alone whenMinPasswordLengthwas above 11. A generated console password is generated again until it meets the rules.The
UpdateHashre-hash at sign-in returns the user with the hash it stored, and purges that user's cache, which kept the old hash until it expired. See the compatibility note. Tests:expPasswordSessionStampTest,expPasswordPolicyTest(PWD-12, PWD-13).A session is no longer signed out because a server's OPcache still held an outdated user cache file: a stamp that does not match is checked once against the stored row.
eZUser::purgeUserCacheByUserId()also invalidates the cache file in OPcache, so a change made through the site reaches every worker of that server at once; a change made on the console reaches a server when its OPcache revalidates (opcache.revalidate_freq).
5 October 2026: cjw_newsletter 4.2.0
Added
- The root
composer.jsonrequiresse7enxweb/cjw_newsletter~4.2.0. The release takes over the features of the old eznewsletter extension and builds them on the e-mail preferences of this line: bounces into the suppression list, rate limits and batches, subscribe by e-mail; recurring sends, article pools, the approval of editions in the collaboration inbox; conditions, interests, an edition in several languages, new skins and plain text views; open and click statistics per person only with consent, A/B subject tests; newsletters by SMS with a code double opt-in; the CSV import with a column mapping, the subscriber export and the migration from eznewsletter. Guide: cjw_newsletter; upgrade from 4.1: the extension'sdoc/upgrade-4.2.md. - Translations: the e-mail preference categories "Newsletter statistics" and "Newsletters by SMS" have their names
and descriptions in the context
kernel/mailpreferences/categories, in English and German.
Updated
expCollaborationInbox::stateOf()asks the collaboration handler of an item for its state: a handler of another type thanezapprovewith a methodinboxState( $item )that returns waiting, approved, denied, open or closed sets the state of its items, so the approvals of an extension (the newsletter'scjwnewsletterapproval) are filtered and shown as approvals are. Any other answer, or no such method, keeps the state from the item's status; only active handlers are asked. Test:CollaborationSampleDataTest(CS-06b).
5 October 2026: forwarded headers are trusted only from configured proxies
Upgrade notes, examples for Apache, nginx, Velocity and cloud load balancers: Forwarded headers are trusted only from configured proxies.
Added
site.ini [HTTPHeaderSettings] TrustedProxies[]: the proxies whose forwarded headers are believed, as addresses and ranges, IPv4 and IPv6. Default127.0.0.1and::1, a proxy on the same machine; an empty list trusts none.eZTrustedProxy(lib/ezutils/classes/eztrustedproxy.php): address and range matching, the right-to-left walk ofX-Forwarded-For, the trusted hop count.eZSys::trustedProxies()andeZSys::isFromTrustedProxy().- Tests:
eZTrustedProxyTestandeZSysTrustedProxyTest(suitelib): a visitor'sX-Forwarded-Proto: httpsis not HTTPS, a trusted proxy's is; spoofedX-Forwarded-Forchains; CIDR ranges in IPv4 and IPv6; invalid addresses; an empty and a missing setting.
Updated
eZSys::isSSLNow()readsX-Forwarded-Proto,X-Forwarded-PortandX-Forwarded-Serveronly whenREMOTE_ADDRis a trusted proxy. Before, any visitor could make a plain HTTP request count as HTTPS (absolute URLs, redirects,CookieSecure=auto, cache keys with the scheme).$_SERVER['HTTPS']and the port are checked first, as before.eZSys::hostname()usesX-Forwarded-Hostonly from a trusted proxy.eZSys::clientIP()withClientIpByCustomHTTPHeaderreads the header only from a trusted proxy, and from the right: the first address that is not a trusted proxy is the visitor, every entry checked withFILTER_VALIDATE_IP. Before, it took the left-most entry, which the visitor writes, soDebugByIP, the sign-in lockout'sTrustedIPList, the audit log and the consent log could be given any address.- The HTTP cache (
httpcache.ini) works out scheme and host as the kernel does: the contract carries the trusted list and the lookup believes the forwarded headers only from a trustedREMOTE_ADDR. Test: HC-15, HC-16. - The debug bar's address test reports the proxy header as used only for a request from a trusted proxy.
- Installation guide: §8.8 "Behind a reverse proxy" and §13.10 "Behind a proxy: trusted headers" describe the setting.
5 October 2026: saved settings files are no longer world-writable
Upgrade note, with the one-time fix for files saved before: Settings files saved through the admin are no longer world-writable.
Added
config.phpconstantEZP_INI_SAVE_FILE_PERMISSION(default0640): the mode of a settings file a save creates.eZINI::newSaveFileMode(),eZINI::saveFileMode(),eZINI::saveFileOwnership().- Tests:
eZINISaveFilePermissionsTest(suitelib), and the mode of a new file inexpIniEditorTestINI-06.
Updated
eZINI::save()keeps the mode, owner (as root) and group of the file it replaces, without write for other users, and gives a new file0640with the owner and group of its directory, or of the nearest one above it that root does not own. Before, every save made the file0666,settings/override/site.ini.append.phpwith the database password included. New settings directories are0755instead of0777; the temporary file is0600until it is in place.expIniEditorcreates a new settings file0640with the same owner rule (was0644) and keeps its temporary file0600until it is in place.- INI cache files (
var/cache/ini) are0644instead of0666;EZP_INI_FILE_PERMISSIONnow applies to them and to generated autoload files only. config.php-RECOMMENDEDand the installation guide §13.11 describe both constants.
5 October 2026: eng-US instead of eng-GB as the content language fallback
Upgrade note: Content language fallback: eng-US and a setting instead of a hard-coded eng-GB.
Added
site.ini [RegionalSettings] ContentObjectFallbackLocale(defaulteng-US): the locale class and attribute names are stored in when no content language is prioritized andContentObjectLocaleis not set.eZSerializedObjectNameList::fallbackLanguageLocale()andconfiguredLanguageLocale().- Tests:
eZSerializedObjectNameListFallbackLocaleTest(suitekernel-classes, no database).
Updated
eZSerializedObjectNameList::defaultLanguageLocale()falls back to that setting, theneng-US; it was a hard-codedeng-GB, which left class descriptions in a language the site did not have.- The default
site.ini [RegionalSettings] ContentObjectLocaleiseng-US(waseng-GB). - The SQL clean data (
kernel/sql/common/cleandata.sql,kernel/sql/sqlite/cleandata.sql) is ineng-US, likeshare/db_data.dba. kickstart.ini--exampleno longer addseng-GBas a second language;kickstart.ini-distshowseng-US.- The subitems column "Language codes" gives
eng-US, ger-DEas its example; the micro benchmark's in-memory class attributes useeng-US. - The multisite installer packages name the folder attributes in the configured locale, else the kernel's fallback,
never
eng-GB.
5 October 2026: personal API keys and the REST administration
Guide: Personal API keys. Upgrade note: Personal API keys: a table, a module and a policy.
Added
- Personal API keys: a signed-in user with the new policy
apikey/createmakes keys on My account > API access (apikey/list): a name, scopes from what the user's own policies allow, a lifetime. The key (expk_<id>_<secret>) is shown once; only an HMAC-SHA-256 hash with a salt per key is stored. A REST request withAuthorization: Bearer expk_...runs as the owner, limited to the key's scopes, the owner's policies and, for write routes, the owner's rights on the node. Last use and address are noted at most once a minute; a rate limit per key is counted in APCu. - Table
expapikeyin every schema (share/db_schema.dba, MySQL, PostgreSQL, SQLite) and in the 6.0.0 to 6.0.15 update files (created only when missing);update/common/scripts/6.0/createapikeytable.phpfor any engine. - Module
apikey(viewlist, policycreatewith aScopelimitation, fetch functionscan_createandcounts);rest.ini [ApiKeySettings]. - Setup > oAuth admin > API keys (
oauthadmin/keys): every key with owner, scopes, last use and status, a search, a status filter, one user's keys (/(user)/<id>), revoking with a confirmation (oauthadmin/keyaction). The user settings page links there. - Audit events
access.apikey.create,access.apikey.revoke,access.apikey.use(the first use) andaccess.apikey.use.failed(with the reason); the catalogue has 140 names. - Tests:
expApiKeyTestandApiKeySchemaSqlTest(suitekernel-classes, no database);ApiKeyLiveTest(groupnetwork-live: makes, uses, refuses and revokes keys over HTTP on Apache and Velocity).
Updated
- The REST applications pages (
oauthadmin/list,view,edit, the removal confirmation) are redesigned in the look of the cronjobs page: figures, a card per application with its users and valid tokens, the secret folded away. Every address, form field and button is unchanged. - A new REST application gets a client identifier and secret from
random_bytes()(wasmd5of the name anduniqid()), and removing an application removes its authorizations, tokens and codes. Client secrets are compared in constant time. - HTTP basic authentication of the REST interface (
ezpRestBasicAuthStyle) checks the password with the user's own hash type througheZUser::authenticateHash(). It comparedmd5("login\npassword")in SQL, so every user on a modern hash was refused. A wrong password counts as a failed login. - The REST layer reads
Authorization: Bearer <token>as well asOAuth <token>, and answersinsufficient_scopewith 403 instead of 401 (the status is sent after theWWW-Authenticatechallenge, which PHP turns into a 401). - The form token filter keeps a view's
Cache-Control: no-storeinstead of replacing it with its ownprivate, no-cache, must-revalidate.
6 October 2026: REST permission checks and routes
Guide: Permissions of the REST interface.
Added
expRestContentPermission(kernel/private/rest/classes/auth/content_permission.php): one check of the current user's rights for a REST read, create, edit or remove, done as the content module does it (canRead(),checkAccess( 'create', class, parent class, language ),canEdit(),canRemove()of every location and of the subtree). Theezprestapicontent controller (1.2.5) asks it before every call, for OAuth tokens, personal API keys, basic authentication and the anonymous user alike, and answers403 access_deniedwith the policy named; the API key write guard asks the same helper. Before, an OAuth token could create and remove content where its user had nocontent/createorcontent/remove.ezpRestVersionedRoutetakes a list of versions (array( 1, 2 )) andgetVersions(); a URL it generates keeps the version of the current request.ezpMvcRailsRoute::anyMethod()builds a map that runs one action for every method.
Updated
- The REST router tries the next route when one matches the path but not the method, and answers
405with anAllowheader listing every matching route's methods only when none takes it. - The authentication routes (
/auth/oauth/login,/auth/http-basic-auth) and the error route answer every method: aPOSTorDELETEwithout a valid token, or refused by a key's scope, answered405instead of401or403, which made/api/ezp/v2/content/node/createand.../delete/<id>look unreachable. - The REST answer cache keeps one entry per user and only for
GETandHEAD: one user's read was served to another for ten minutes pastcontent/read, and a repeated create or delete was answered from the cache without running. It keeps no refusals: a cached403could not be read back (its status object has no__set_state()), so the second identical refused read answered 500, on both web servers since they share the cache. A cache file that cannot be read back now counts as expired. - The
content/createcheck of the REST interface takes the parent class from the parent object (it asked the node for an attribute it does not have, logged an error and checked the ParentClass limitation against nothing). ezprestapi1.2.5: reads answer at/api/ezp/v1/...again (404 since the extension took theezpprovider over fromezprestapiprovider; existing clients such as a mobile app read throughv1), writes atv2; delete takesDELETEas well asPOST; create answers201and delete200with the ids (a delete that worked answered 500). Velocity needsexp:velocity deploy --kernelfor the kernel classes.
6 October 2026: access and view cache filters for extensions
Added
- Five filters of
ezpEventthrough which an extension takes part where it had to patch the kernel; for the four access filters onlytrueallows. Guide: Access and view cache filters.content/view/cachekeys: the keys of a view cache file name, now named (userroles,userlimitedlist,viewparameters, ...), with the user and the node; the file names do not change.content/download/access: the answer ofcontent/download, with the object, the attribute and the version. That the attribute belongs to the object stays checked first.content/edit/access: the neweZContentObject::editAccess( $version, $language ), whichcontent/edit,content/history,content/removeeditversion,content/versionviewand the upload and tag dialogs of the online editor use for their edit check.collaboration/item/access: whether the current user may open a collaboration item; the template getsis_participant, and only a participant files the item in a group of the inbox.content/notification/create: whether the publish operation creates the notification event.
- The events are described in the settings extension wizard. Test:
expPermissionHooksTest(no database).
Updated
- Every edit check of
content/edit(a new draft, the choice of a language or of a draft, the check of one version), ofcontent/multieditand of the REST interface goes throughcontent/edit/access, so a listener decides the same way on every path;eZContentObject::filterEditAccess()passes any edit answer of the kernel through the filter, and without listeners no time is spent on it (ezpEvent::hasListeners()). content/downloadrefuses the attribute of another object or version before any listener is asked.collaboration/actionacted on any item it was posted, for anyone (a comment on an item the user does not take part in; a missing item was a fatal error). It now acts only on an existing item of the posted type that the user may open, whichcollaboration/item/accessdecides.content/view/cachekeys: node, view mode, language, offset and layout always stay; non-scalar values are left out.- A debug notice says when a listener changed an access answer.
- The settings extension wizard writes listeners of these filters with their arguments and a commented example that reads its rule from the extension's settings.
- The guide explains what a listener can narrow and widen, where edit access is asked and where not, with worked examples.
6 October 2026: version preview in other view modes
Added
content/versionviewshows a version in another view mode with the parameter(view_mode), for examplecontent/versionview/<object>/<version>/<language>/(view_mode)/printfornode/view/print.tplin a print or PDF layout. The view modes allowed are listed in the new settingcontent.ini [VersionView] ViewModes[](fullby default); another one answers "not available". Without the parameter nothing changes. The view mode is theviewmodedesign key of the preview and the template variableview_mode;design/standardrenders the node in it, and the admin designs pass it on to the preview frame. Test:eZContentVersionviewViewModeTest(no database).design/standard/templates/node/view/print.tpl, a print view of a node (its name and the attributes with content), as an example forViewModes[]=print.
Updated
- With more than one view mode listed, the View control box of the preview (admin, admin3, admin4) offers them, and
Update view keeps the chosen one; it went back to
fullbefore. - Only a name of letters, digits,
_and-is a view mode, whatever the setting lists. - Guide: Version preview in other view modes.
6 October 2026: Cancel in user/edit
Updated
- Fixed: Cancel in
user/editgoes back where the user came from: the page the form names inRedirectIfDiscarded(as incontent/editanduser/register), else the page viewed last, else the sitemap of the users. It always went tocontent/view/sitemap/5, also for an editor changing their own account on the site. A host thatsite.ini [SiteSettings] AllowedRedirectHostsdoes not list is refused as before. Test:eZUserEditCancelTest(no database). - Updated: One set of rules for every redirect target from outside the code, in
eZRedirectManager::safeURI()andunsafeReason(): a path of this site or an allowed host; never a backslash (/\evil.example,\\evil.example), a control character (CR/LF), a scheme other than http(s) (javascript:,data:), a user in the URL, or the percent-encoded forms of these (%2F%2Fevil.example).eZModule::redirectTo()answers 403 to a target that breaks them ("Redirection requested to an unsafe address"); before, a backslash orjavascript:target ofuser/login,content/actionoruser/editwent out as theLocation.eZRedirectManager::redirectURI()checks its preferred URI and the page from the session the same way;eZModule::allowedRedirectHosts()is public. - Updated: Cancel and Discard go back to the page the form was opened from in
user/password,user/setting,section/edit,state/edit,state/group_edit,role/edit,class/editandclass/groupedit, as inuser/edit: the form'sRedirectIfDiscarded, else the page viewed last, else where they always went.eZRedirectManager::returnURI()never returns the running view (a loop), a view of the newsite.ini [SiteSettings] DisallowedReturnViews(content/action,user/logout,content/download, ...) or a page viewed last that the user can no longer view. The views setredirect_if_discardedand their standard, admin and admin4 templates send it;user/editin the standard design has a Cancel button. - Updated:
user/passwordchecks itsRedirectOnCancelandRedirectURI(both redirected to as posted before),user/registeritsRedirectIfDiscarded(used as the default before), and Discard incontent/editthe page kept in the session;content/edit_redirection.tplescapes the two hidden fields it writes. - Added: The page Safe redirects and "back to where you came from", and
eZRedirectManagerSafeURITest(no database): the rules case by case, and the Cancel target of every view. Velocity needsexp:velocity deploy --kernelfor the kernel classes.
6 October 2026: event listeners without a callback
Updated
- Fixed: an entry of
site.ini [Event] Listeners[]that is not of the form<event>@<callback>(no@, or an empty event or callback) is logged as an error and skipped. It raised the PHP warning "Undefined array key 1" on every request and attached a listener that could never be called, or one to an event named ''. A callback is split off at the first@only.[RunnableSettings] Listeners[]already skipped such entries. Test:ezpKernelResultAndEventTest(no database). - Blanks around the event or the callback of an entry are dropped (
content/view @ myClass::methodattached a listener to the eventcontent/viewthat was never sent). A callback that is not written as the name of a function or of aClass::method(a typo such as:::or a blank inside) is logged and skipped without being looked up. The same entry listed twice, by the site and by an extension say, is attached once; it ran twice for every event. - A mistake in the entries, and a listener whose class, method or function does not exist, is logged once in the life of the PHP process instead of once per request or per event; under Velocity a worker logged it on every request.
6 October 2026: content policy limitations of extensions
Added
- An extension can add a policy limitation of its own to a content function (
content/read,content/edit, ...). The filtermodule/functionlist(new, ineZModule::initialize()) adds it to the function list, so the role editor offers it; a class implementingezpContentLimitationHandler, named in the new settingsite.ini [RoleSettings] LimitationHandlers[<limitation>], evaluates it in thecheckAccess()methods of objects, nodes and versions, in the SQL of list and tree fetches (permissionSQL()) and for subtree notifications. Guide: Content policy limitations of extensions; listed in Setup > RAD. checkAccess()ofeZContentObject,eZContentObjectTreeNodeandeZContentObjectVersionandeZContentObject::draftCreateAccess()take the user to check for as the last argument ($userID, default the current user);eZUser::accessUser()returns that user. An ID that is no user gets no access.
Updated
- A limitation the kernel does not know and no handler evaluates denies everywhere. Fetches ignored it and listed
objects whose pages
checkAccess()refused; subtree notifications ignored it and sent the mail. eZContentObjectVersion::checkAccess()stops at the first limitation of a policy that denies; aLanguagethat did not match, or a limitation it did not know, could be allowed again by the next limitation.- Subtree notifications check the
Grouplimitation ("Self group") ofcontent/read. - A class that overrides one of the
checkAccess()methods must accept the new argument; see behaviour changes. Tests:ezpContentLimitationTest,eZContentPermissionSQLTest(no database),eZContentAccessForUserLiveTest(live).
6 October 2026: content limitation handlers, hardened and extended
Guide: Content policy limitations of extensions.
Added
exp:access:check(bin/php/accesscheck.php): whether a user (ID, login oranonymous) may read, edit, remove ... a node or object, and which policy and limitation refused it, with the handler of an extension limitation or the note that none evaluates it; ALLOWED or DENIED, or JSON.expContentAccessReport::check()gives the answer to PHP.eZContentObject::editAccess()andfilterEditAccess()take the user as a further argument: that user'scontent/editanduser/selfedit, and the filtercontent/edit/accessgets that user's ID.- The settings extension wizard writes a working limitation handler (
<class>MaxDepth) next to a listener ofmodule/functionlist, with the commented[RoleSettings] LimitationHandlers[]line; Setup > RAD points at it. - A limitation added through
module/functionlistmay carry alabel, which the role screens show. permissionSQL()may answerarray( 'column' => ..., 'values' => ..., 'type' => 'int'|'string' ); the kernel writes theINstatement and casts or escapes the values.
Updated
- Fixed: a handler that is missing, does not implement the interface, cannot be made, or throws in
checkAccess()orpermissionSQL()denies and is logged once per request. An exception ended the request (a list page with one such object answered 500), and a missing handler was looked up and logged for every node of a list. - Fixed: a string condition from
permissionSQL()must be self-contained (quotes and parentheses balanced; no;or comment outside quoted strings):1 = 1 ) OR ( 1 = 1closed the parentheses it was put in and gave every user of the role the whole tree in fetches. Any answer that is notfalse, such a string or a column condition denies. - The handler of a limitation is made once per request and again for the next one, also in a persistent Velocity
worker, and when
LimitationHandlers[]changes. Handlers get the values as a list of strings. - The listeners of
module/functionlistrun once per module and request, not on every module lookup; only when one is attached. What they return is kept where it has the form ofmodule.php; a listener that throws or returns no list leaves the module its own functions instead of ending every request. The RAD survey lists the event again. eZUser::accessUser()accepts a positive whole number only, gives the current user's own object for its ID and no user for a disabled account, and keeps other users for the request (forgotten by the next request,eZUser::cleanupCache()and a purge of that user's cache).- The role screens (role view and editor, policy lists of the node and user views, policy forms; admin, admin4 and
standard designs) show a limitation's label, washed, with "(no handler, denies)" next to a content limitation no
handler evaluates, and the stored values of one whose extension is gone. The editors store only values they
offered; a single value posted instead of a list no longer ends the request in
in_array(). - The values of the Class, Section, Node and object state limitations are cast to integers in the fetch SQL.
expservicesdecides edit access to content ascontent/editand the REST interface do: object updates througheditAccess()in the language written, price and community item changes throughfilterEditAccess().- Fixed: the stand-in object of
expRestContentPermissionTesttakes the user argument ofcheckAccess(); PHP refused to load the test since the handlers came in. The ezjscore module definition reads its function list withoutezjscore.ini, and twoexpservicesmethods mark their optional arguments nullable (PHP 8.4). - Tests:
ezpContentLimitationTest(CL-11 to CL-18),eZContentPermissionSQLTest,eZContentAccessForAnotherUserTest,eZPolicyLimitationLabelsAndValuesTest,expRadWizardListsTest(no database). Velocity needsexp:velocity deploy --kernelfor the kernel classes.
6 October 2026: host matching by the start of a host, and a redirect to the language for addresses without one
Added
- The host map of siteaccess matching (
MatchOrder=host,HostMatchType=map) can match a host by its start, end or a part, ashost_urialready could: the new settingsite.ini [SiteAccessSettings] HostMatchMethod(strictby default, as before) or a third field of an entry,HostMatchMapItems[]=www.example.com;site;start. Withstart, a test host such aswww.example.com.test.localreaches the siteaccess ofwww.example.comwithout entries of its own. Both probes use the neweZSiteAccess::hostMatches(). As forhost_uri, the static cache only knows strict matching. - An address that no probe of
MatchOrdermatches, whereDefaultAccesswould apply (typically the one without siteaccess or language segment), can get its siteaccess from the new settingsite.ini [SiteAccessSettings] DefaultHostUriMatchMapItems[](host;uri;siteaccess[;method[;language]]), with anyMatchOrder: the first entry for the host whose language the browser accepts (Accept-Language, the most wanted first,dealso forde-CH), else the first entry without a language; an empty host withpartmatches any host. A GET or HEAD request is sent on (302), once, to the same address with the uri part, kept as it was asked for, also for a site with one language:/to/ger, the URL alias/news/an-articleto/ger/news/an-article, with its query. So every cached page has one address, and an address a probe matches (/ger/...,/admin/..., after switching the language too) is never sent on. The redirect needs a probe that takes the address with the uri part for that siteaccess; without one, for other requests or withDefaultHostUriRedirect=disabledthe page is shown at the address itself with links carrying the uri part. When the host has more than one language variant, the redirect, and a page shown this way, are sent withVary: Accept-LanguageandCache-Control: private, and the role-aware HTTP cache does not store them. NeweZSiteAccess::matchDefaultHostUri(),acceptedLanguages(),reachesSiteAccess()andezpKernelWeb::languageRedirectURI(). Guide: Siteaccess by host prefix, and the redirect to the language. Test:eZSiteAccessMatchTest(no database).
Updated
- The redirect to the language sends a path on the host that was asked for (
Location: /ger/news?x=1), not a URL built from theHostheader, so a forged host is never written into it. The path, which the kernel keeps decoded, is encoded again: an encoded line break cannot split the header,%2F%2For a backslash cannot turn it into an address on another host, and empty and dot segments are dropped. It starts with the index file and the segment of the entry instead of the address built for links, whichRemoveSiteAccessIfDefaultAccess=enabledshortened by the segment of the default siteaccess: withDefaultAccessthe language siteaccess, the browser was sent to the address it had asked for, endlessly. NewezpKernelWeb::languageRedirectLocation(), which never answers with the address asked for itself. - The answer at an address without segment also varies by
Accept-Languagewhere an entry names a language and the entries lead to another siteaccess with the same segment, or where no entry applies without a language (a German browser went to/ger, any other gotDefaultAccessat the same address withoutVary, and a shared cache could hand the one answer to the other). AVaryfrom[HTTPHeaderSettings]is kept and extended. - The HTTP cache's early exit (
ezpHttpCacheContract::resolveSiteAccess()) knowsHostMatchMethod, the method of a host map entry and the hosts ofDefaultHostUriMatchMapItems; before, it served the page ofDefaultAccesskept for/instead of letting the kernel choose by language. - Host matching (host map,
host_uri, the language default) compares host names without regard to case and to a trailing dot, ignores the port unless the entry names one, reads a Unicode entry as its ASCII form, andendmatches the end of the host also where the listed host comes twice (example.com.example.com). An entry ofDefaultHostUriMatchMapItemswhose siteaccess is not inAvailableSiteAccessListis ignored and logged.acceptedLanguages()reads at most 32 languages, skips what is not a language tag and counts aqabove 1 as 1.RedirectOnNormalizeno longer sends a redirect whilereachesSiteAccess()only asks where an address leads. Tests:eZSiteAccessMatchTest,ezpHttpCacheContractTest(no database). - Only the web page request uses the language default:
eZSiteAccess::match()takesDefaultHostUriMatchMapItemsinto account only when its new fifth parameter$languageDefaultistrue, whichezpKernelWebalone passes. The REST interface (ezpKernelRest), the tree menu (ezpKernelTreeMenu), extensions and scripts match as before #131: the probes, thenDefaultAccess, never by the browser's language, without redirect orVary. Test:eZSiteAccessMatchTest(no database).
6 October 2026: the users and groups of a role a page at a time
Updated
role/viewshows the users and user groups a role is assigned to a page at a time, sorted by name, on the parameter(assignment_offset). The new settingsite.ini [RoleSettings] AssignmentsPerPage(default50,0lists all) sets the page size. A role given to thousands of users loaded every one of them with its object. The heading counts all of them ($assignment_total;$assignment_countis what the name filter keeps); the admin designs page with the navigator,design/standardwith previous and next links. NeweZRole::assignmentCount()andeZRole::assignmentPage( $offset, $limit );fetchUserByRole()still returns all. A design that overridesrole/view.tplmust add a pager, see behaviour changes. Test:eZRoleAssignmentPageLiveTest(live).- A page of
role/viewcosts the same few queries whatever its size: the objects, main nodes and limitation nodes of a page come in one query each, and the name shown is the one fetched with the object (a role with 114 assignments: 106 queries before, 10 now). The sorting is the same on every database: assignments whose object is gone first, then by name without regard to the case of a-z, then by assignment (checked on SQLite, MySQL, PostgreSQL, Oracle and MongoDB, which sorts in php). - An assignment whose user or group no longer exists is listed first as such and counted above the list, so it can be removed; it was an empty link. A limitation whose node or section is gone says "not found".
- Paging or sorting the policies keeps the place in the assignments and the other way round, and the form (Remove selected, Assign) comes back to the same page; past the last page shows the last page.
- The audit of removing assignments reads the rows of the role without loading their objects (it loaded every object of the role once per removed assignment) and records the user id of a gone object instead of 0.
Added
- A name filter on the assignments of
role/view:(assignment_filter)/<text>, without regard to case,%and_matched as themselves. NeweZRole::assignmentOrphanCount(),eZRole::assignmentRows()and the optional$filterofassignmentCount()andassignmentPage(). role/listshows how many users and groups each role is assigned to (column Assigned, one query per page:eZRole::assignmentCounts()). The role name in thedesign/standardrole list is escaped.- Translations (eng-US, ger-DE, untranslated) of the new texts. Guide:
Role assignment paging. Tests:
eZRoleAssignmentSortTest(no database) and new cases ofeZRoleAssignmentPageLiveTest(gone object, filter, role list counts, query count).
6 October 2026: links, URL aliases, wildcards and search statistics
Updated
url/listis an overview (links in published content, valid, invalid, never checked, the last link check), a search over the addresses (?q=), the lists all, valid, invalid and the newunchecked, an order ((sort)/checked,(sort)/modified, address A to Z by default) and one card per link with its state, how the link check treats it (e-mail, on this site, link to content, not tested, other address), its times, the published objects that use it (two queries per page) and View, Edit and Open. Mark selected valid and Mark selected invalid use theSetValidandSetInvalidactions the module always had but no template offered, and say how many links changed. The page size comes fromadmininterface.ini [PaginationSettings](url/list).- Fixed:
url/listprinted the address unescaped and made it a link whatever its scheme; Open is now offered for web, mail and site addresses only, withrel="noopener noreferrer". content/urltranslatorleads with the creation form, each field explained and its error on the field; what was typed stays in the form when an alias cannot be created (it was emptied). The list searches the last segment and destination (?q=), filters redirecting or direct aliases ((kind)/...), shows each alias's whole path and what its destination resolves to, and marks an alias whose module no longer exists. Remove all asks in place how many aliases go. The destination of an alias is escaped.content/urlwildcardsrefuses a destination with a placeholder the pattern has no*for (new info codeerror-wildcard-placeholder), searches pattern and destination, filters by kind, and Try an address (?test=) says which wildcard an address matches, what it becomes and what that resolves to, as the wildcard cache would. Remove all asks in place.search/statssays which siteaccesses record searches (site.ini [SearchSettings] LogSearchStats, disabled by default) and warns when none does, counts phrases, searches and those that found nothing, searches the phrases, filters those that found nothing ((show)/none), orders by count, phrase or fewest results ((sort)/...), draws each count as a bar and runs a phrase again with Search now. Reset statistics asks in place first (it had no confirmation). The page size the template used is now the one the view pages with.- The four pages share the look of the section and cronjobs pages, in
design/adminanddesign/admin4(the url and search sections ofstylesheets/modules/workflow.css), in the light and dark modes, and work without javascript. Every field name, button and template variable they had is kept. eZURL::fetchList()andfetchListCount()takelast_checked(never,checked),search(withonly_published) andsort(address,checked,modified,id) on every database; on MongoDB the published listing now also returnscreatedandmodified.- The link check (
linkcheck.php, partinfrequent) decides with the newexpLinkCheck.https://is requested likehttp://(it was never tested): HEAD, then GET when HEAD fails, redirects followed one by one up toMaxRedirects, TLS verified, a timeout and a User-Agent of its own. 429 and 503, and 401 and 403 (a site refusing automated requests), decide nothing and keep the state. Links to content (ezlocation://,eznode://,ezobject://) are valid while their node or object exists, is published and is not hidden; they were looked up as paths and always marked invalid. A path of the site is found behind its siteaccess name, query and fragment, and a module view counts.file:addresses are no longer tested.mailto:and other schemes are treated as before. - The link check never requests an address on a private, loopback, link-local or reserved network, also behind a
redirect, unless
AllowedPrivateHosts[]names its host, address or range, and pins each request to the address it checked (SSRF). It waitsHostDelaybetween requests to one host, skips links checked less thanRecheckIntervalseconds ago, checks the ones checked longest ago first and ends with its counts, requests and time. New keys incronjob.ini [linkCheckSettings]:Timeout,ConnectTimeout,MaxRedirects,HostDelay,RecheckInterval,MaxURLsPerRun,UserAgent,AllowedPrivateHosts[]. On the demonstration server the first run checked 350 links in 9.5 minutes with 725 requests (valid 284 to 254, invalid 66 to 96, never checked 21 to 0). - The badges of
url/listfollow the new rules ("https: not tested" is gone; "Other address" for other schemes). - The search and kind filter of
content/urltranslatorandcontent/urlwildcardsare conditions of the database query (they read every row and filtered in PHP): neweZURLAliasQuerypropertiessearch(last segment or action) andredirects, neweZURLWildcard::fetchFilteredList()andfetchFilteredListCount(), on every engine and MongoDB.
Added
- Guide: Links, URL aliases, wildcards and search statistics. Test:
expUrlAdminPagesTest,expLinkCheckTest(no database, no network).
6 October 2026: RSS feeds, triggers, workflow groups and class groups
Updated
rss/listis an overview (exports, active exports, imports, active imports, imported objects), a search and a filter, then one card per export with its public feed address, format in words, sources with their class, number of items, when the feed was last written (the newest cached copy) and what needs attention, and one card per import with its destination, class, owner, the objects it has created and the newest of them. A status bar names the cronjob part that runsrssimport.php, its schedule and next run. Sorting, paging and the page size are as before; new template variablesrssexport_info,rssimport_info,rss_summary,rss_import_cronjob,rss_feed_cache_time,rss_feedback. Guide: RSS feeds.- Removing RSS exports or imports takes two steps: Remove selected shows
design:rss/confirmremove.tplwith what removing means, and onlyConfirmRemoveButtonposted with the same ids removes. A removed export now takes its sources and OPML lines with it (removeThis(); they were left behind). rss/edit_exportandrss/edit_importare forms in the same look, each field with what it is used for: the export in the feed, what it lists (one card per source with its location, class and attribute mapping) and the Apple Podcasts or OPML part; the import in four steps (feed, where the items go, what each item becomes, active). The export form shows the public feed address (new template variablerss_feed_url); validation messages are escaped. An import is stored only with a name and an http or https source address (\Exponential\View\Kernel\Rss\EditImport::validate(),validation_errors), and says how many objects it has created (rss_import_imported). The content browser descriptions of the RSS pickers and the lock pages (edit_export_denied.tpl,edit_import_denied.tpl, new indesign/adminanddesign/admin4) use the same look. Every field and button name is unchanged.trigger/listnames every operation in words ("Before publishing content"), grouped by module, with the workflow it runs, its events, whether it is disabled and its waiting processes; triggers without a workflow are marked "Not set". A trigger whose workflow is no longer offered by the select keeps it as an extra choice, so Apply changes no longer drops it unnoticed; one whose workflow was removed is marked. Stored triggers for operations no longer inAvailableOperationListare listed and can be removed (the existingRemoveButton). Apply changes says how many triggers changed after the redirect. Workflow names are escaped. Guide: Workflows, section 11.workflow/grouplistshows each group with its workflows, their triggers, waiting processes, other groups and last change, and what removing the group does. Removal asks first (design:workflow/confirmremovegroup.tpl,ConfirmRemoveButton). A workflow that is also in another group now stays there instead of being removed, a disabled workflow of the group is removed too instead of being left without a group, and a removed workflow's events and temporary version go with it, as inworkflow/workflowlist. Guide: Workflows, section 12.class/grouplistshows each group with its classes and their published objects, the last change of the group or its classes, and how many classes and objects removing it would remove; an overview counts classes in more than one group and in none. Removal still goes throughclass/removegroup. Guide: Class groups.- The other class pages use the same look:
class/classlist(the classes of a group with their objects, the other groups they are in, View, Edit and Copy, a search, New class in a chosen language),class/view(settings, attributes as cards, the class groups, translations and override templates behind chips),class/edit(the class settings and one card per attribute with every field explained; the attribute order script, Apply and Cancel back to the page the form came from work as before),class/groupedit, the language choice, the edit lock page and the confirmations ofclass/removeclass,class/removegroupand translation removal, which now say how many objects go. Class names, override template paths, translation names and validation messages that were printed unescaped are escaped. Copy on the class list is a form button that posts toclass/copy/<id>. Every field, button and template variable is unchanged. class/copy/<id>copies only for a POST that carriesCopyClassButtonorConfirmCopyButton(the form token protects it). Opened by a link it shows what the copy will be (design:class/copy_confirm.tpl, new indesign/admin,design/admin4anddesign/standard) with a Copy button; before, following a link made a copy. The copy links ofdesign/standardare form buttons now. Test:expAdminListsRedesignTest.- The four pages use the look of the redesigned section pages in
design/adminanddesign/admin4, light and dark, with module-localexp_list_script.tpland the module stylesheets understylesheets/modules/. Every existing template variable, field and button name is unchanged.Processlist::scriptCronjob()finds the cronjob part of any script (workflowCronjob()uses it). Test:expAdminListsRedesignTest(no database).
6 October 2026: sessions and collected information
Updated
- Setup > Sessions (
setup/session) is redesigned for the admin and admin4 designs. With a session table (ezpSessionHandlerDB): figures (sessions, users active withinActivityTimeout, sessions of signed in users, anonymous sessions, timed out sessions still stored), a search over names, logins and e-mail addresses beside the user filter, one card per user or per session with last activity and idle time in words, sorting by last activity, name, login, e-mail and count ((sortby),(order)), and page sizes (admininterface.ini [PaginationSettings] ItemsPerPageList_setup_session[], 50, 25, 100). Remove selected and Remove all ask first on a confirmation page (ConfirmSessionRemoval); the session the administrator is using is never removed by a selection, and removing one's own user keeps it. Session keys no longer reach the page: one user's sessions are posted as references (SessionRefArray[]),SessionKeyArray[]from older overrides is still accepted for that user's keys only. Every button and field name is kept. The list reads names, logins and e-mail addresses in its one query instead of loading every user, and a page past the end shows the first page instead of redirecting. - Without a session table (
ezpSessionHandlerPHP, the default) the page said only that the handler is not supported. It now names the handler and PHP's storage, lists who signed in within the last hour, day or session lifetime fromezuservisitwith a search, sorting and paging, and explains how to switch to the database handler. - Setup > Collected information (
infocollector/overview) is redesigned: figures (objects with collections, collections, the last 30 and 7 days, the latest collection, objects removed or in the trash whose collections are not listed), a search by name, sorting by name, collections, last or first collection and type, one card per object with its recent collections and links to the collections, the object and the CSV export, and the published objects that can collect but have nothing yet. A page is one query instead of three per row. The removal confirmation (also used byinfocollector/collectionlist) lists what goes, says what stays and that the data can be exported first, and the overview says how many collections were removed or that nothing was selected.
Added
infocollector/export/<ObjectID>: everything an object collected as CSV (collection ID, sent, changed, user ID and one column per information collector attribute), formula cells guarded, at most 50 000 rows, audited asdata.export.csvwithout values. Policyinfocollector/read.- Guides: Sessions and Collected information.
Tests:
SessionPageTest(no database).
6 October 2026: PDF exports
Updated
- A PDF export generated once wrote its file and then stopped with "[] operator not supported for strings" (PHP 8):
the second template of the generation was handed a string where the template engine appends to an array. The
export was never stored and its file stayed behind with nothing pointing at it. A PDF generated on the fly was
streamed from inside a template (
pdf(stream): the whole file in a string,ob_clean()on a buffer that may not exist, no file name,cleanExit()in the middle of a template); it is now written to a temporary file and sent in chunks withContent-Disposition, after the page's buffers are dropped and the session is closed, withcleanExit()outside any catch block, so it works under Velocity's persistent workers. - The list linked a stored file at
var/<site>/storage/pdf/<name>, which the web server does not serve (only images are served from the storage directory): the link was always a 404. Every download goes throughpdf/edit/<id>/generate, which now sends the stored file of an export generated once instead of an empty page. - The file name came from the form through
basename()only: an empty name, a hidden file or any character reached the storage path. It is now letters, digits,.,_and-, at most 100 characters, ending in.pdf(added when missing); removing an export deletes its file only for such a name. - The edit form ticked class 1 when class 12 was chosen (a substring test on
"12:16"), kept the old classes when every class was unticked, and labelled the file name "if generated on the fly" while it is the stored file's name. pdf/editwithout an id made a new draft on every reload; it now redirects topdf/edit/<id>.- A stored PDF is public at
var/<site>/storage/pdf/<name>.pdf, linked from its card: one rule for storage/pdf in.htaccess_root,.htaccess_root_staticand the nginx example (storage/pdf/*.pdfonly). - Setup > PDF export (
pdf/list) is redesigned for the admin and admin4 designs: what an export is, figures (exports, generated once, on the fly, stored files, their size, needing attention), the drafts of new exports that were never saved, a search over title, file name, ID, source node and classes, filters (generated once, on the fly, with a stored file, needing attention), sorting by title, modified, generated, file size and ID, page sizes (admininterface.ini [PaginationSettings] ItemsPerPage[pdf/list],ItemsPerPageList_pdf_list[], remembered per user), and one card per export: its source node and whether it still exists, what it contains, its stored file with size and date, Download, Regenerate (writes the file anew; greyed out with the reason when the source is gone) and Edit. Remove selected asks first and lists each export with the file that goes with it. - pdf/edit explains every field in five groups (title, content, front page, footer, export type), checks them (a title, an existing source node, at least one class for a tree, a safe file name no other stored export uses) and names the field of each error; it says when somebody else has the export open. A new file is written to a temporary name first and replaces the old one only when complete. Cancel goes back to the page the form was opened from (safe redirect rules), else to the list.
- Every field and button name (
Title,SourceNode,ExportType,ClassList[],DestinationType,DestinationFile,ExportPDFButton,DiscardButton,DeleteIDArray[],RemoveExportButton,NewPDFExport) and every template variable is kept; new areConfirmRemoveButtonandRegenerateButton.generatePDF()is kept and calls the new classes.
Added
expPDFExportFile,expPDFExportGenerator,expPDFExportInfoandexpPDFExportForm(kernel/classes/pdfexport): the file name and stored file, the generation, what the pages say about an export, and the form. Tests:expPDFExportTest(no database).- Guide: PDF exports.
6 October 2026: system information
Updated
- Setup > System information (
setup/info) is redesigned for the admin and admin4 designs. It leads with the server that answered (Apache with PHP-FPM and its pool, Exponential Velocity with its release and worker model, FrankenPHP, PHP's built-in server), then health checks with ok, warning, failure and note and what to do for each, overview cards (Exponential, server, PHP, OPcache and APCu, database, storage, caches, cronjobs, mail, locale and time, machine), and the detailed parts folded below them (extensions with their versions, PHP extensions and settings, site addresses, OPcache and APCu, Velocity, the engine archive, the HTTP cache, database queries, the response cache, the database connection). Every template variable of the earlier page is kept. - OPcache was reported as "not installed" under PHP-FPM when
opcache_get_statusis indisable_functions(Plesk sets it per domain); it is now "enabled, statistics not available". - Velocity's own server was said not to be started by exp:velocity when its worker could not read the TLS certificate; the configured HTTPS port is compared instead.
- The PHP settings no longer report
safe_modeandregister_globals, removed from PHP (the template keys stay, always off). setup/info/phpcallsphpinfo()without the request variables and the environment, which carried the HTTP sign-in and the session cookie, and as plain text under Velocity.- Full server paths (installation root, engine archive, cache directories, Velocity's log) are shown relative to the installation.
Added
expSystemReport: the facts of the process that runs it, read only and in about 10 ms, the health checks and the cards; directory and database sizes only on request (setup/info/sizes, three seconds at most).expSystemReportMask: the masking rules (secret names, credentials in addresses,password=...pairs, session ids and tokens, paths).- The report for a support request:
setup/info/report(text) andsetup/info/json, and Copy report as text. ./console exp:system:info [--json] [--sizes](bin/php/systeminfo.php), exit code 1 when a check fails.- Guide System information. Tests:
expSystemReportTest,expSystemReportMaskTest(no database). - The buttons of the HTTP cache (purge, remove dead entries, reset counters), the query cache (clear, reset
counters) and the SQL profile (on, off) are on Setup > Caches as well, in a block of their own. Both pages post
the same fields to one code path,
expCacheManager::sharedActionFromPost(), which checkssetup/managecache; every post carries the form token. expSecretRule: the one rule of what is a secret, shared by exp:ini (expIniEditor::isSecret()), the audit log, the settings pages (expSettingsSecretRule) and the system report (expSystemReportMask); names ending inpwdordsnnow count as secrets everywhere. A password inside an address keeps its user name on every page (mysql://user:***@host). Tests:expSecretRuleTest.
6 October 2026: the history of a draft for its editors
Updated
content/historyopens for who may edit the object as well as for who may read it (\Exponential\View\Kernel\Content\History::canOpen(), witheZContentObject::editAccess()). A draft that was never published is readable for its owner only, so an approver or a further editor that an extension lets in through the filtercontent/edit/accesscould not reach the versions to make the next one from a rejected one. The forms of the page post without a version, which the check does not need; the diff still askscan_diff. Test:eZContentHistoryAccessTest(no database).- The view's policy functions are
read or edit(they werereadandedit, so a user without any read policy never reached the view).History::canOpen()lets in who may edit the object, and who may read it with an edit policy for some content, as before; the anonymous user and other readers without an edit policy stay out. - Fixed: the page showed the content of versions the user may not read. It now compares, copies and links to the
version view only for versions whose content the user may see (
History::canSeeVersionContent()): the published and archived versions and one's own, a rejected version for who may edit the object, and someone else's draft or pending version only withcontent/versionreadfor it. This also closes the comparison of other users' drafts for readers, whichcontent/diffalone allowed. A comparison or copy of such a version sent in a request is not done, and the page says so (design:content/history_access_messages.tpl). - Fixed: removing versions takes only versions of this object in the statuses the page offers (draft, archived, rejected, untouched draft) and says which versions were not removed; a pending version in a running workflow can no longer be removed by a made-up request. Copying starts the new draft in one of the version's own translations and reports a language the user may not edit on the page instead of a 403. An unknown version in the edit action returns to the list instead of ending in an error, and a comparison skips an attribute one version lacks.
- An editor who may edit but not read the object gets a note on the page; the copy button of a version they may not see says why it is disabled. New strings in eng-US, ger-DE and the untranslated catalogue.
- Guide: Edit access to objects that were never published, section
"The versions of such an object". Tests:
eZContentHistoryAccessTest(10 cases, no database) andeZContentHistoryAccessLiveTest(8 cases with its own users, roles and drafts). - Fixed: the Back button of
content/historyopened directly went to the edit the session had last sent to the history ("Manage versions", even of another object, an edit that makes a new draft), or to the page viewed last. It now goes to where the page was opened from: the edit of a version of this object that opened it with "Manage versions" (taken once), else the page the browser came from, each only wheneZRedirectManager::safeURI()and the return rules take it (never the history itself, nevercontent/edit/<object>without a version, never an edit of another object). Without an origin it goes to the object's own location in the full view (/content/view/full/<main node>), or to the content dashboard for an object never published. The origin is carried in the form (RedirectURI, now escaped), so comparing, copying and removing keep it. All four designs.History::originURI(),History::originCandidate(); testeZContentHistoryAccessTestHA-11.
6 October 2026: My drafts and My pending items in the look of the redesigned pages
Updated
- Fixed:
content/draftremoved any version whose id a request named (DeleteIDArray[]with Remove selected), someone else's draft or a published version included, for every user who may open the page. It now removes only the current user's own drafts (and untouched drafts), each checked again at the moment it is removed (expContentDraftList::removable()); every other id is left as it is and the page says how many. content/draftis figures (drafts, of new objects, not modified for 30 days, translations, classes), a search (?q=), filters by translation, class and age (7, 30, 90, 365 days), an order (last modified, oldest, name, class), paging (admininterface.ini [PaginationSettings], keycontent/draft, preferenceadmin_draft_list_limit) and one card per draft: name, class, location or "new object", translation, version, created, modified and age, with Edit, View and Remove confirmed in place. Remove selected, the new Remove old drafts (RemoveOldButton,OldDraftDays) and Remove all are confirmed in place; one draft is removed withRemoveDraftButton[<id>].RemoveButton,DeleteIDArray[]andEmptyButtonare kept.content/pendinglistlists the user's own pending versions and, new, the versions waiting for the user's approval that the user may read (content/versionread), each with the state of its approval and its approvers, the workflows whose process holds it, who sent it and when, its location, and links to the version, the approval (when the user takes part in it) and the published page; figures, filters by list and class, an order and paging (keycontent/pendinglist, preferenceadmin_pending_list_limit).- Both pages are in
design/adminanddesign/admin4with the look of the versions page (the history and draft sections ofstylesheets/modules/content.css);design/standardkeeps its pages. The views are thin; the lists areexpContentDraftListandexpContentPendingList, which need no database.
Added
- Guide: My drafts and my pending items. Test:
expContentDraftListTest(no database).
6 October 2026: the versions page in the look of the redesigned pages
Updated
content/historyis an introduction to versions and to making a new draft from one, the notices of the last action, figures (all versions, each status, translations, your drafts), filters by status, translation and creator, an order (newest, oldest, last modified), paging (admininterface.ini [PaginationSettings], keycontent/history, preferenceadmin_history_list_limit) and one card per version: its number, status, translations, creator and dates, and the actions you have on it (View, Edit, Compare, Make a new draft in a translation you may edit, select for removal). An action you do not have says why on the card, or once above the list when no version on the page allows it.- Comparing is a two-picker panel offering only the versions whose content you may read, and a Compare button on
each card (new POST
CompareButton[n]: with the current version, or for the current one with the newest other version you may read). The differences are shown above the list as inline changes, block changes, the old or the new version, in colours that read on white. - Removing ticked versions is confirmed in place with the list of versions; the page then says which were removed
and which were not (new refusal
remove-nonewhen nothing was ticked). Making a new draft says which draft was made; untick "Stay on this page" (DoNotEditAfterCopy) to open it in the editor at once. - The edit warnings and the access notices (
content/history_access_messages.tpl, now also indesign/adminanddesign/admin4) are notices in the look of the redesigned pages. The same page is indesign/admin,design/admin3anddesign/admin4(the history section ofstylesheets/modules/content.css); thedesign/standardpage is unchanged and keeps working. Every POST name and template variable of the page is kept; new variableshistory_rows,history_overview,history_choices,history_links,history_filters,history_suffix,history_count,history_offset,history_limit,history_limit_choices,history_status_names,history_edit_languages,history_feedback,diff_language. - The view is a sequence of steps (Back, compare, version limit warning, remove, edit, copy, list); the list, its
filters and the actions of each version are
expContentHistoryList(kernel/classes/expcontenthistorylist.php), which needs no database. Who may open the page and whose content they see is unchanged (History::canOpen(),History::canSeeVersionContent()).
Added
- Guide: Versions of an object. Test:
expContentHistoryListTest(no database).
6 October 2026: My bookmarks
Updated
content/bookmarkis an overview and a list in the look of the section, link list and session pages: figures (bookmarks, folders, not in a folder, hidden, no longer available), the folders with their counts beside the list, the folder shown with its path, counts and Rename, Move (with Move up and down) and Remove folder, a search over names, classes, locations and folders, five orders (your own, name, recently added, type, recently modified) and paging (admininterface.ini [PaginationSettings] ItemsPerPageList_content_bookmark[], 25, 50 and 100 by default).- The bookmarks are grouped by folder, one card each with the class icon, the location, the last change, Hidden or Hidden by a parent, and Not found or No access for a bookmark whose item is gone or no longer readable (those were left out of the list before, so they could not be removed). View, Edit where allowed, Move up and Move down.
- Bookmarks move between folders by selecting them (Move the selected bookmarks to) or, as an extra, by dragging them onto a folder. Remove selected and Remove folder ask first; Remove folder says whether what is inside moves to the folder it was in or is removed with it. Add items adds to the folder shown and only items the user may read.
- Every POST answers with a redirect to the same folder, order and search and a notice. The Remove of the context
menu (
NeedRedirectBack) goes back only to a page of this site. Every POST name and the template variablesview_parametersandbookmark_noticeare unchanged; the page works without JavaScript, has 4.5:1 contrast in the light and dark modes and holds from 390 pixels and at 200% zoom.
Added
- Arranging on
content/bookmark, in Your order: a grip on every bookmark and every folder of the folder list is dragged to a new place in its folder (the folder is outlined) or a bookmark is dropped on a folder to move it there (labelled "Move into this folder"); the arrow keys on a focused grip move it one place and keep the focus; a position field moves a bookmark to any place in its folder, also across pages; Move up and Move down remain without JavaScript. One POST saves the new order (BookmarkOrderButton,OrderType,OrderFolderID,OrderIDs) througheZContentBrowseBookmarkFolder::setOrder(), which refuses ids of another folder or user and repeated ids;moveToPosition()places one entry. In the other orders the page says "Switch to Your order to arrange". Tests:expBookmarkPageTestBP-14 to BP-17,BookmarkFoldersTestBF-14. expBookmarkPage(kernel/classes/expbookmarkpage.php): what the page shows, from plain arrays;eZContentBrowseBookmarkFolder::shift(): one place up or down within a folder; the buttonBookmarkShiftButton.- Guide: Bookmarks. Tests:
expBookmarkPageTest(no database),BookmarkFoldersTestBF-13.
6 October 2026: the INI settings page (settings/view, settings/edit)
Updated
- Security:
settings/editand the remove action ofsettings/viewtook the INI file, the siteaccess and the place to write from the request as they were, so a POST with../inINIFile,SiteAccessorSettingPlacementcould write an.append.phpfile outside the settings tree. Every name is now checked against what the installation lists (the INI files found,RelatedSiteAccessList, the active extensions), block and setting names must be ones eZINI reads back, read-only settings ([eZINISettings] ReadonlySettingList) are refused, and the values shown on the edit page are escaped.eZINI::save()already refused line breaks and*/in values. - Security: secrets were shown in plain text (the values of six settings named like Password, Token or Key in
site.iniand one invelocity.ini), also in the edit field. Their values are now masked everywhere on both pages, never searched and never put back into a form; a secret left empty on the edit page keeps its value. - A saved setting did not take effect:
config.phpswitches eZINI's file time check off, and the page never cleared the INI cache, so neither PHP-FPM nor Velocity used the new value. Saving and removing now clear the INI cache and say which caches were cleared, and whether Velocity must be restarted for the setting. - The origin of array elements was wrong after a reset (
Name[]) in a later file: eZINI's placements keep counting past a reset, so 1833 of 21542 array elements on the demonstration server were attributed to a file they do not come from. The origin is now worked out from the files themselves in eZINI's load order (expSettingsChain), checked against eZINI for every setting of every file in every siteaccess (41728 settings, no difference). - The page is redesigned for the admin and admin4 designs: figures for the file, the files read in their order, a card per block with a jump list, every setting with its value in effect, its origin, its default when changed and its whole override chain (what each file does: sets, empties the array, adds an element, sets a key; in effect or overridden), arrays as numbered lists with the origin of each element. The edit page shows the chain before the form, explains each place to save, and fills an array's field with an empty first line so saving it as shown replaces the array instead of adding the same elements again. Every form field, button and template variable of the old pages is kept; the pages work without JavaScript.
settings/editaccepts the setting names eZINI accepts (Class_identifier, names with-,@,*), and an empty array can be saved.
Added
- Search across settings (block, name and value) in one file or in every INI file of a siteaccess, a filter for the settings changed from the default, and a comparison of a file between two siteaccesses.
- "Not in effect yet": the page compares the files with the values the serving process runs with (PHP-FPM on 443, Velocity on 8080) and lists the settings still waiting for an INI cache clear. Files of the load order the server cannot read are named.
site.ini [SettingsViewSettings]MaskedNameList[],UnmaskedNameList[]andMaskRevealLastCharacters(which names are secrets, on top of the rule ofexp:iniand the audit log), andRestartSettingList[](the settings Velocity reads only when it starts).- Classes
expSettingsChain,expSettingsSecretRule,expSettingsTargetandexpSettingsPage(kernel/classes/ini/). Guide: The settings page. Tests:expSettingsPageTest(no database).
6 October 2026: the cache preloader
Updated
- Setup > Preload (
setup/preload) is redesigned for the admin and admin4 designs: a status bar with the run going on (site, start, a progress bar of pages warmed against the page limit, the address being requested, Stop) or the last run, the live output with the figures of the run when it ends, a form that explains its fields, which server's caches a run warms (Apache/PHP-FPM and Velocity share the view cache, image aliases and compiled templates; Velocity's response cache keeps a pageDefaultTtlseconds only), the command and a crontab line for the choices made, and the last runs with duration, pages, image aliases made and failures with their addresses, status codes and linking pages. The page works without JavaScript (the form postsAction=start|dryrun|stop, the namessetup/preloadjobtakes) and picks a running run up again when it is opened. Every template variable of the earlier page is kept. - One preload runs at a time: a run holds a lock (
var/<var dir>/preload/run.lock) from the page, the shell and cron alike; a second one is refused (409 fromsetup/preloadjob, exit code 2 from the command). Each run writes a status file (<id>.json) beside its events, so a run whose process is gone is reported as ended instead of after two minutes without output, a fatal error is recorded with its message, and the runs of the shell are listed with the others. The last 20 runs are kept. bin/php/preload.phpruns the same crawler as the page (expPreloadJob::execute()) instead ofwgetandcurl, and writes nothing to/tmp. New options:--max-pages(default 1000),--max-depth,--images,--dry-run,--target. The last three lines of its output say how the run ended, the pages warmed and the image aliases made.- The address of a siteaccess is worked out from
MatchOrderand every match setting, as a request is matched (expPreloadAddress): aHostMatchMapItems[]entry with a third field (start,end,part) andHostMatchMethodreach the same siteaccess as for a visitor,URIMatchType=mapandHostUriMatchMapItems[]give their own prefixes, and an address the matching does not send to the siteaccess is warned about. setup/preloadstreamno longer runs a crawl inside the request (it held a worker for minutes and started a load from a plain GET); it answers the events of the run going on or the last one.- The preloader takes links and images from the page's own markup only: nothing inside
<script>,<template>,<style>or comments, and no value that holds a quote, a JavaScript or template expression (' +,${,{{ }}) or whitespace. Inline scripts that build markup in strings were reported as broken links (/' + esc(p.path) + '). Velocity's own panel (/Q/...) is never requested. - The preloader warmed only the front page of a site whose
SiteURLalready ends in its siteaccess prefix (uri matching,latest.demo.exponential.earth/site): the prefix was rightly not added twice, so the site was taken for one matched by its host, and every/site/...link was dropped as another siteaccess's. The site is now everything below the path ofSiteURLplus the prefix, in every matching mode (host, uri, host_uri, an installation in a folder). Phase 1 also warms the section pages, the top level pages the starting pages link to (/site/fitness), whenURLTranslationKeywordnames none.
Added
- What to warm: Also check the images on each page (
--images) requests every image the warmed pages show, once, and lists the missing ones with the pages that show them. Image aliases made counts the alias files the rendered pages created during a run. - Dry run on the page and
--dry-runfrom the shell: the address, the prefix, the starting pages, the limits and the command, without a request. GET setup/preloadjob/status(the run going on and the last runs),POST Action=dryrun.expPreloadAddress,expPreloadLock,expPreloadHistory; guide Preloading caches. Tests:expPreloadAddressTest,expPreloadLockAndHistoryTest(no database).
6 October 2026: classic menus (Setup > Menus (classic))
Updated
- Setup > Menus (classic) (
visual/menuconfig, until now "Menu management") explains what it is for: the top and left menus of sites built with the classic designs (base, ezwebin, the demo and simple designs), chosen per siteaccess inmenu.ini [SelectedMenu]. Sites built with Exponential Layouts set their menus in the layout editor, which the page links to. - The page finds for every siteaccess of
RelatedSiteAccessList, from the page layout its designs resolve to, whether it uses these settings, renders through Layouts, is an administration siteaccess or has a design that reads no classic menu. It opens on a siteaccess where they matter, folds the others away with the reason, says when none uses them, and asks for a tick before saving for a siteaccess they do nothing for. - For the siteaccess shown: the current arrangement, the file each value comes from, a warning when
settings/overridedecides it, the classes the menus list, and the file saving writes to. Each arrangement has a preview built from the installation's own pages, says what its menus list (parent page, classes, depth, hidden pages) and which template file draws it, and shows the lines saving writes. - Five recipes with their exact lines and what a visitor sees (top menu only; top plus a left menu for the current section; menus limited to folders and landing pages; hiding a page; a different menu per siteaccess), the designs of the installation that draw classic menus, and "Use in your templates": copy-ready template examples for a page layout, a top menu and a section menu of your own, and where to put them.
- Saving writes only
[SelectedMenu] CurrentMenu,TopMenuandLeftMenuintosettings/siteaccess/<siteaccess>/menu.ini.append.php, through the INI editor ofexp:ini(only the touched lines change, the PHP wrapper stays, a copy of the old file goes tovar/backup/ini, the write is audited), reads the file back and puts the old one back when it does not hold what was written. Before, it saved everymenu.inisetting the siteaccess read into that file, which froze the other menu settings for it. Only an arrangement ofAvailableMenuArrayis accepted, and only a known siteaccess whose directory lies insettings/siteaccess. - After saving, the INI caches, the compiled page layouts and the template blocks are cleared and the page says that no restart of PHP-FPM or Velocity is needed.
- The form keeps
CurrentSiteAccess,SelectCurrentSiteAccessButton,MenuTypeandStoreButtonand every template variable, and works without javascript; new is/visual/menuconfig/(siteaccess)/<name>. Redesigned indesign/adminanddesign/admin4, light and dark. - The Setup and Design menus list the page as "Menus (classic)".
Added
expClassicMenuSiteAccessInspectorandexpClassicMenuSettings(kernel/classes/classicmenu): which siteaccesses and designs read the classic menu settings, and reading and writing them; the template examples inkernel/classes/classicmenu/examples. Tests:expClassicMenuSettingsTest(no database).- Guide: Classic menus.
6 October 2026: publish without notification
Added
- "Publish without notification" next to the publish button of
content/editand of the version preview, shown when the new settingnotification.ini [NotificationSettings] PublishWithoutNotificationisenabled(defaultdisabled, the button hidden). The version is published as with the other button, but no notification event is made, so the subscribers of its location get no mail. The publish operation takes the new optional parameternotify(trueby default), kept in the memento of a publication a workflow holds back, andeZContentOperationCollection::createNotificationEvent()gives it to the filtercontent/notification/createinstead oftrue. Buttons in the admin, admin3, admin4 and standard designs, translated into German. Guide: Publish without notification, and section 8 of the notifications administrator's guide. Tests:eZPublishWithoutNotificationTest(no database),eZPublishWithoutNotificationLiveTest(live). - Policy function
content/publish_without_notification(no limitations;content/*includes it). The button is shown to, and counts for, users who have it; for anybody else, and with the setting disabled, a postedPublishNotNotifyButtonorPreviewPublishNotNotifyButtonpublishes with notification and logs a warning.eZContentOperationCollection::canPublishWithoutNotification( $user )andpublishWithoutNotificationEnabled().
Updated
- The conflict page of
content/edit(content/edit_conflict.tpl, admin, admin4, standard) offers "Publish without notification" again when it was pressed before the conflict was found; its Publish data button notified. - With
content.ini [PublishingSettings] AsynchronousPublishing=enabled, a publication without notification is published at once instead of queued (sendToPublishingQueue()takesnotify): the asynchronous publisher runs the operation with the object and the version only, so it notified. createNotificationEvent()takesfalse,0and"0"as no, and its debug line says when a version was published without notification rather than blaming a listener.
6 October 2026: Setup > Caches
Updated
- Setup > Caches (
setup/cache) is redesigned for the admin and admin4 designs. It lists every cache in groups (content and views, templates, INI and settings, images, pages, queries and the server, other) with what it holds, its id and tags, its directory relative to the installation, its size and number of files (measured on request,setup/cache/(sizes)/1, within three seconds), when it was last cleared where the kernel records it, and badges for a Velocity restart, Velocity's response cache and a disabled cache. Above it: figures (caches, enabled, groups, total size, last expiry), search and a group filter. - Clear selected, clear a group (
ClearGroupButton), clear by tag and clear all each name what goes in an in-place confirmation (details elements, so it works without javascript; "Clear selected" confirms with javascript), and end in a notice with the caches cleared, the time it took, the restart or response cache hint and the command. - The HTTP cache, query cache, SQL profile, Velocity's response cache (
ClearVelocityCacheButton, new) and PHP's OPcache and APCu are rows of the group "Pages, queries and the server" instead of separate boxes. The static cache and the commands that do the same from a shell are folded below. - OPcache was reported as "not loaded" under PHP-FPM when
opcache_get_statusis indisable_functions; it is enabled, and Reset OPcache now works there too (expCacheManager::phpCacheState(),resetOPcache()). - Every POST name and template variable of the earlier page is kept; the view is thin and its actions are methods of their own.
Added
expCacheCatalogue: the catalogue of the caches (groups, what each holds, masked directories, sizes within a time budget, last cleared, Velocity hints, commands); it only describes, clearing staysexpCacheManager's. Tests:expCacheCatalogueTest(no database).- "Last cleared" on Setup > Caches also reads the audit trail's
system.cache.clearrecords (one bounded query of the audit index and the end of the newest system file), with who cleared for users who may read the system channel: clears through the page, System information,exp:cacheand, since the next entry,bin/php/ezcache.php(and soexp:velocity deploy) are recorded. Times read "just now", "12 minutes ago". Tests:expCacheCatalogueTest(CC-07, CC-08). bin/php/ezcache.php, and soexp:velocity deploy, writes onesystem.cache.clearaudit record per run that clears something (--clear-all,--clear-tag,--clear-id, with--purgeand--expiry), with the ids cleared and the operating system user of the shell, througheZCacheHelper::auditCleared()and the sameeZCache::auditCleared()thatexp:cacheand the administration use. A failed audit write is a warning on stderr; the caches are cleared either way. Setup > Caches names a shell as "from a shell, os:root".expAuditReader::linesBackwards()lost records under Velocity: through a userland stream wrapperfread()returns 8 KB at most, and the rest of each 64 KB chunk was skipped. It reads until the chunk is complete. Test:expAuditReaderChunkTest.- Guide Caches.
6 October 2026: faster access arrays for users with many role assignments
Updated
- The access array of a user (
eZRole::accessArrayByUserID()) is built from the policies, limitations and values of all the user's roles loaded in three queries, the limitations of a policy are turned into their part once for all the assignments of its role, and the parts are merged in one call. A user with 600 subtree assignments (120 teamrooms × 5 member roles of 20 policies) took 3,550 ms and about 48,600 queries, now 244 ms and 4 queries. The array is the same, in the same order (the values of a limitation by value, as read one by one). New settingsite.ini [RoleSettings] AccessArrayPrefetch(enabled;disabledbuilds it role by role). Guide: Faster access arrays. Tests:eZPolicyPrefetchedAccessArrayTest(no database),eZRoleAccessArrayPrefetchLiveTest(live). - Fixed (in the same change, before release): the values of a limitation loaded ahead came in the order of their ids
instead of by value; while the rows were loaded, a role, policy or limitation outside them was answered as having
none (a policy then as unlimited) instead of asking the database; an IN () list could pass Oracle's 1000 (now at
most 500 ids per query); a failed query left policies without their limitations, that is unlimited (the array is
now built role by role then); a build inside a build dropped the outer one's rows. Compared on the demonstration server with
===against the code before for every user, assignee and throwaway roles of every limitation kind: identical; 177,651 queries and 14.3 s for 31 subjects before, 104 and 1.6 s now.
Required packages (root composer.json, 2026-10-01 to 2026-10-02)
The constraints of the root composer.json after the last commit of 2026-10-02. The last column gives the reason
of the repin commit in the range. Each package's own release notes are in the
extensions' release notes.
| Package | Required at the end of 2026-10-02 | Why it was repinned |
|---|---|---|
| exponential-velocity | ~0.0.4.42 | persistent workers report each request's own start time and read sessions without a warning |
| ezoracle | ~2.3.3 | Oracle driver in the SQL query cache (2.3.2) |
| expui | ~1.0.0.5 | jQuery 4 admin tables, dialogs, uploads, dates, autosave; the datatable of the subitems columns |
| ezjscore (bundled) | 1.5.4 | YUI removed (1.5.0); identical to its own releases 1.5.2 to 1.5.4 |
| explayouts / explayouts-ui / explayouts-ui-api | ~1.4.12 / ~1.3.9 / ~1.3.10 | Exponential names, jQuery 4 editor and admin |
| sevenx-themes-media, expsite-data-media, expsite-installer | ~1.5.31, ~1.0.6, ~1.0.2 | renamed blocks; ezinfo.php works on PHP 8 |
| ezwebin, ezdemo, ezwt, ezflow | ~6.0.16, ~6.0.9, ~6.0.9, ~6.1.5 | date fields on Exponential UI; YUI removed |
| ezautosave, eztags, ezmultiupload, ezstarrating, ezodf | ~6.0.8, ~2.4.11, ~6.0.8, ~6.0.8, ~6.1.6 | Exponential UI; YUI removed |
| cjw_newsletter | ~4.1.16 | YUI removed; tests; command helpers |
| ezupdate, git_manager, expchangeclass, bccie, ngclasslist | ~1.1.10, ~2.0.14, ~1.0.5, ~1.1.11, ~1.1.3 | Installed packages view; releases of the day |
| syndication, birthday, hcaptcha | ~1.3.2, ~1.3.2, ~1.2 | engine-neutral schema; about-page name, version, license and website |
Related pages
- Previous release: 6.0.14.
- The story: June 2026, second half, July 2026, August 2026, September 2026, first half, September 2026, second half, October 2026.
- How to upgrade: Upgrading guide.
- The extensions: release notes, Redis and Valkey caches.
6 October 2026: the upgrade check
Added
expFileConsistencyReport(kernel/private/classes/expfileconsistencyreport.php): compares the files withshare/filelist.md5and with the manifest an extension carries of its own, with result states (modified, missing, unreadable; unlisted, malformed and unordered as notes), counts, the time taken, a CSV and a text report. It needs no kernel;bin/php/checkmanifest.phpand the upgrade check both use it, so they agree on the same tree.expSchemaConsistencyReport(kernel/private/classes/expschemaconsistencyreport.php): the database check by table (missing tables, tables not in the schema, changed fields and indexes with their shipped and current definition), the SQL per table, what removes something, and engine notes. Display only; nothing is run.php bin/php/checkmanifest.php --all --extensionsalso checks every extension's own manifest;--csvprints the findings as CSV. Guide: The upgrade check.- Test:
expUpgradeCheckReportsTest(no database): matching, modified, missing, malformed lines, CRLF, unreadable files, extension manifests, unlisted and unordered lines, the downloads, and the schema report.
Updated
- Setup > Upgrade check (
setup/systemupgrade) is redesigned for the admin and admin4 designs, light and dark, styled instylesheets/modules/setup.css: the version, the manifest's entries and dates and how many active extensions carry a manifest; the two checks side by side; the file result grouped by state with a search, a filter by state and by extension and what to do about each kind of finding; the database result table by table with its SQL; the time each check took. Download CSV, Download text and Download SQL (DownloadFileReportButton,DownloadSchemaReportButton) send the report as a file.MD5CheckButton,DBCheckButtonand the template variablesmd5_result,failure_reason,upgrade_sqlandmongo_*are kept; new ones arefile_report,file_groups,schema_report,upgrade_infoandupgrade_check. - The database check no longer calls a database consistent while hiding differences, nor inconsistent over noise:
differences the engine's schema handler writes no SQL for (on SQLite, 19 tables whose
longtextreads astextor whoseAUTOINCREMENTkey reads asint) are listed as engine notes and do not fail the check. - A manifest with CRLF line ends is read like one with LF (the command line check reported every line as missing);
a manifest line naming a path outside the installation is skipped and reported instead of being read; an empty
or unreadable
share/filelist.md5fails the check with its own message instead of passing. - The audit event
system.upgrade.runcounts the findings of the new reports and names the format of a download. - The SQLite schema reader reads a key declared
integer NOT NULL PRIMARY KEY AUTOINCREMENTasauto_increment(it readint NOT NULL DEFAULT 0: expbookmark_folder and the five expmail_* tables), and the database check on SQLite compares only what SQLite can tell apart (eZSQLiteSchema::normalizeForComparison(): text and longtext, the width of an auto_increment key, a default left out or written as false), writing the SQL from the shipped definitions. The 19 engine notes on an SQLite installation are gone; a missing field, another type or length, NOT NULL and another default still show. Test:expSQLiteSchemaReaderTest(a throwaway SQLite file).
6 October 2026: the Extensions page
Updated
- Setup > Extensions (
setup/extensions) is one list instead of two: the separate Loading order card and the paged table with checkboxes are combined into one list of cards, redesigned for the admin and admin4 designs, light and dark, styled instylesheets/modules/setup.css. Each card shows the position inActiveExtensions, name, title, version, description, license and website, whether a siteaccess activates it as an access extension or uses one of its designs, its declared dependencies (requires, uses, extends) and the active extensions that need it, whether it is a git checkout, and its problems. Active extensions are moved with up and down buttons (without JavaScript too) or by drag and drop and the arrow keys on a grip; extensions are activated and deactivated in the same list, an activated one placed after what it requires and before what it extends, otherwise at the end. A search, a filter (all, active, inactive, problems) and an order (loading order or name) work in place and without JavaScript. - Nothing is written until Review changes shows the extensions activated, deactivated and moved, the risks
(deactivating an extension another one requires, the only provider of a design a siteaccess uses,
ezformtokenorezjscore; a move that lets another extension's full settings file win, as an installation settings extension moved afterezoracle; a reorder the declared dependencies undo) and the resultingActiveExtensions[]lines; red risks need a confirmation. Apply changes writes the new set and order together, clears the INI, template override, design and active extension caches, regenerates the autoload arrays and says whether Velocity needsexp:velocity restart. A list changed elsewhere in the meantime is not written over. - The page shows where each active extension really loads: with
ExtensionOrdering=enabledthe kernel sorts by the declared dependencies, and on a typical installation most extensions load at another position than written (51 of 55 on the reference installation); the former Loading order card showed only the written order. - Saving the list edits
settings/override/site.ini.append.phpas text: only theActiveExtensions[]lines change, and the comments, the layout and the<?php /*wrapper of the file stay.eZINI::save()had rewritten the file and dropped every comment in it. A comment directly above an extension's line moves with it. - The posts of the page before the redesign still work (
ActivateExtensionsButtonwithActiveExtensionList[], and the JSONReorderExtensions); the template variables of the earlier page are still set. The list is no longer paged, and(sort)takesorderorname(other values fall back to the loading order). - Guide: The Extensions page.
Added
expExtensionCatalogue(kernel/classes/expextensioncatalogue.php): what is known about each extension (metadata, dependencies, designs, settings files, git checkout, access extensions and design users per siteaccess) and the rows of the list.expExtensionChangePlan(kernel/classes/expextensionchangeplan.php): moving, activating and deactivating, the diff, problems, risks and the order the kernel really loads in;ezpActiveExtensions::replaceInText()andfromText()for the file.- Test:
ExtensionChangePlanTest(no database): reorder, activate, deactivate, dependency warnings and risks, no change gives no write, and a round trip of the settings file that keeps every other byte.
6 October 2026: the package pages
Updated
- Setup > Packages (
package/list) shows every repository by default (it showed onlylocal, so the setup wizard's own packages were out of sight): figures over all repositories, one tile per repository with its packages, size, install states, installer sources and the directories without a package definition it holds, and a warning naming every directory whosepackage.xmlcannot be read (one such file ended the whole page with a fatal error). One card per package with its version, type, install state, summary, repository, maintainers, the packages it requires and those that require it, size, files and last change; a search, filters by repository, type and state, a sort, and pages whose size follows[PaginationSettings]and keep the filter (the pager lost the repository). Packages the installer takes as a source (the setup wizard's repository, site packages and what they require) are marked. - The removal of packages asks on a page of its own (
package/confirmremove.tpl) that says what goes: each directory with its files and size, whether it is installed, the packages that require it and why it is an installer source; an installer source needs its own tick. It checkspackage/removefor each package's type (it checked nothing), removes only what the confirmation offered, refuses a package whose directory contains a link (the recursive delete would follow it), and is recorded assystem.package.remove. Its title read "Remove section?". - A package's page (
package/view/full/<name>[/<repository>]) leads with its actions (Compare, Install, Download, Uninstall, Remove, each by its own policy), then its details, what it carries (classes, content, extensions, settings files, files by kind) and its changelog, above the contents browser. A viewed file shows its first megabyte. The upload, install, uninstall and creation pages are redesigned the same way, indesign/adminanddesign/admin4(the package section ofstylesheets/modules/structure.css), light and dark. - Downloads (
package/export,package/viewfile) are sent in 256 KiB pieces after the page's output buffers are closed, answerRangewith 206 or 416, quote the file name, and end outside anytry/catch(Velocity).package/exportandpackage/viewfiletake the repository as a further parameter. - Fixed: an upload extracted every entry where the archive said:
../paths, absolute paths, symbolic and hard links and devices were written outside the package (zip-slip).eZPackageUploadInspectorrefuses such archives, wrong suffixes, files that are not gzip, archives overpackage.ini [UploadSettings](new) and malformed definitions before anything is written, with the reason on the page;eZPackage::import()refuses them too, so the setup wizard andezpm importare covered. A vendor that gives no directory name is refused (it put the package at the top of the storage). - Fixed: a package name or repository in an address or a form is checked before it is joined to a path
(
eZPackageRequestGuard);../7x/xmadeeZPackage::fetch()read apackage.xmloutside the repository. An unknown view mode is "not found" (it drew an empty page). Install and uninstall checkpackage/installfor the package's type. - Fixed:
eZPackage::parseDOMTree()returns false for a definition without a name and reads a missing<ezpublish>or<packaging>as empty (both were fatal errors);exportToArchive()writes to a temporary file of its own, so a stalearchive.tmpno longer makes every later export of the user fail.
Added
eZPackageCatalog,eZPackageRemovalPlan,eZPackageUploadInspector,eZPackageDownloadandeZPackageRequestGuard(kernel classes), with tests that need no database:eZPackageCatalogTest,eZPackageUploadInspectorTest(malformedpackage.xml, path traversal and links in archives),eZPackageRequestGuardTest,eZPackageDownloadTest.- Guide: Packages.
6 October 2026: roles, policies and unactivated users
Updated
- The role list (
role/list) shows each role as a card with its policies, the users and groups it is assigned to, and a Full access badge for a policy over every module or Can change roles for one over the role module. A search by name or id (?q=) and the orders name, id, number of policies and number of assignments, either way round ((sort),(dir)); Remove selected asks first, in place. - The role page (
role/view) shows every policy as a sentence ("May read content, in section Standard, of the classes Article and Folder"), with its module, function and limitations under it, ordered by role order, module (grouped), function or limitation; the figures say how many users the role reaches in all, members of its groups counted once. Removing assignments asks first. - The role editor (
role/edit) lists the policies the same way, says when the draft differs from the saved role, asks before the page is left with unsaved changes, and asks before policies are removed. Policies are reordered by drag and drop on a grip, by the arrow keys on the grip, by the up and down buttons and by a position field (MovePolicyTo,MovePolicyPosition[<id>]), all aseZRole::movePolicy()steps in the draft: Save keeps the order, Cancel drops it. Dragging is offered in the role's own order. - The policy wizard shows its three steps, explains each choice and warns before full access is added; the policy editor and the section step of "Assign with limitation" are redesigned the same way. Every value is escaped.
role/copyshows what the copy will hold; the copy is made by the page's form (CopyRoleButton), no longer by opening the address.- Unactivated users (
user/unactivated): the count is of the users the list holds; a search by login, e-mail or name; the orders registration date, login, e-mail and name; the age of each registration, old ones marked; Activate, Send the activation mail again (new,ResendButton) and Remove, each confirmed in place; Remove all (RemoveAllButton) for every unactivated user or every one the search matches, in batches of 50, each checked again before removal, never the anonymous user,adminor the user removing, reported as "N removed, M skipped" and recorded asaccess.user.removewith the counts. - All pages in
design/adminanddesign/admin4, in the look of the redesigned pages (stylesheets/modules/people.css), light and dark. Every POST name and template variable of the pages is kept.
Added
expRolePolicySentence,expRolePage(kernel/classes/role) andexpUnactivatedUsers(kernel/classes/user), withexpRolePagesTest(no database). Guide: Roles, policies and unactivated users.
Fixed
- A policy id posted to the role editor of another role removed that policy from its role at once
(
eZPolicy::removeByID()removes any policy); the editor removes only policies of its draft. Likewise role/view removed any assignment by id; it removes only assignments of the role shown. - "Assign with limitation" put the posted limitation type into the address unchecked; only subtree and section are
taken, and
role/assignchecks the role, the limitation and its value, and returns from the content browser only to a page of this site. role/policyedit/<id>for a policy that does not exist was a fatal error; it is "not available".- Activate on
user/unactivatedran the whole register operation again, which for an account without a pending registration sent a new activation mail and disabled the account again; it now publishes the user object only. Remove on that page removes only users who are still unactivated. - An unknown sort field on
user/unactivatedraised a notice; anAssignRolebrowse result returned torole/listwas a fatal error.
6 October 2026: the account page
Updated
- The account page (
user/edit, anduser/edit/<id>for another user) is redesigned in the look of the other redesigned administration pages, indesign/adminanddesign/admin4, light and dark, with its own style (the user section ofstylesheets/modules/people.css). Until now the administration drew the plain page ofdesign/standard(or that of the two-factor extension, without Cancel), outside the admin's card. - An overview card shows the name, login, e-mail, groups, roles, the last sign-in, the number of sign-ins, when the password was last changed (from the audit index) and the state of the account (active, disabled or locked).
- Security hints: disabled or locked, failed sign-ins, two-step sign-in off, a password stored with an old method or changed more than a year ago, never signed in.
- A card per page to go on to, shown only when the viewer may open it: profile, password, account settings,
two-step sign-in (when its extension is active), API keys, bookmarks, notifications and e-mail preferences; on
another user's page API keys and e-mail preferences open the administration pages of that user. Every POST name
(
EditButton,ChangePasswordButton,ChangeSettingButton,CancelButton) and template variable is kept; Cancel still goes back to the page the editor came from.
Added
expUserAccountOverview(kernel/classes/user), withexpUserAccountOverviewTest(no database); the view sets$account_overview. Guide: Security and audit, section 6.
6 October 2026: no privilege escalation through roles; activation completes the registration
Updated
- A user who may edit roles can only grant what their own access covers (
site.ini [RoleSettings] PreventPrivilegeEscalation=enabled, new, default on). The policy wizard takes a policy they do not hold out again; Save of the role editor refuses a draft with a new or changed policy they do not hold; the policy editor refuses a broader change;role/copyrefuses to copy, androle/assignandrole/viewto assign, a role with a policy beyond their access (an assignment's subtree or section limitation narrows the role first). Each refusal names the policies. Users with every function of every module may grant anything;disabledgives back the old behaviour. - Activate on
user/unactivatedsends the activated user the "registration approved" mail again, as the original flow does (site.ini [UserSettings] ActivationByAdministratorSendsApprovalMail=enabled, new;EmailRegistrationInfostill switches that mail off everywhere).
Added
expRoleGrantCheck(kernel/classes/role): whether an access array covers a policy, with subtree containment, nodes inside subtrees, value limitations, the limitations of roles assigned for a subtree or section, and a policy covered by several policies together;expRoleGrantCheckTest(no database).expUserActivation(kernel/classes/user): completes an activation by resuming a pending registration, or by publishing and sending the approval mail when none is pending.
Fixed
user/activate(the link in the activation mail) ran user/register afresh for an account without a pending registration: the account was disabled again and mailed a new link. It now completes the registration as above; a pending registration resumes as before.- The publish step of an activation (
eZUserOperationCollection::publishUserContentObject()) compared the object's version with!== '1'; a database driver that types its results (SQLite) gives 1, so every activation was cancelled there: the user object stayed unpublished and no approval mail went out.
6 October 2026: permission reads fail closed, roles leave no policies behind
Updated
- The access array fails closed. A read that fails while it is built (the roles of a user, the policies of a role,
the limitations of a policy, the values of a limitation, the role ids of the user cache) denies what it could not
read: the role grants nothing, the policy is left out. Before, a policy whose limitations could not be read became
unlimited (
*). Such an array is used for that request only and not stored in the user cache, and one error is logged per build (eZRole::accessArrayByUserID). When loading the rows ahead fails, the array is built role by role, as before. With a healthy database the array is the same: compared on the demonstration server with===for every user, assignee and the throwaway roles of every limitation kind. Guide: Faster access arrays. Behaviour change: October 2026. Test:eZAccessArrayFailClosedTest(no database).
Fixed
- Removing a role left behind the copies role/policyedit makes of its policies while one is edited (
original_idset);eZRole::removeThis()removes every policy of the role. - Discard in role/edit for a role that was still new removed only its row and left its policies behind; they are removed with it.
6 October 2026: two-step sign-in
Added
- A guide for two-step sign-in with sevenx_authentication_2fa, linked from the guides: setting it up as a user,
turning it on or requiring it as the owner, a lost phone, social login, what protects it
(
doc/eng-us/guides/two-factor-login.md).
Updated
- The admin4 login page shows the social login buttons of sevenx_authentication_2fa where that extension is active
and enables a provider (
{if ezmodule( 'user2fa/oauth' )}); nothing changes where it is not. - sevenx_authentication_2fa (released on its own): the second step, its setup, the social login answers and the 2FA
field are drawn in the look of the redesigned admin pages (admin4 light and dark, admin, Admin UI) and of the media
design; the password alone no longer signs in before the second step; codes are accepted once, limited per sign-in
and counted as failed logins; a pending second step is kept in the session only; TOTP secrets can be encrypted at
rest; the steps before signing in work in administration siteaccesses (
PolicyOmitList); every redirect passes the safe redirect rules. See the extension's README. - Two-step sign-in is on at the demonstration server, as opt-in (
Enforce2FA=disabled); the guide says how it is set up there and how to require it. The admin login, the standard registration and the standard profile include the extension's social login buttons and two-step sign-in link where it is active, instead of the extension shipping its own copies of those templates, which hid the kernel's newer ones. - Every role on the demonstration server that signs in has
user2fa/setupanduser2fa/verify(Editor and the custom editor role besides Member), so every user can set up two-step sign-in; the guide says so and that a new role needs both.
6 October 2026: kernel and library fixes from the logs
Fixed
- Run from the engine archive (
EXP_ENGINE_PHAR), the audit log looked for its directory inside the archive, so every record failed and was written to the error log instead.expAuditConfig::root()uses the installation directory (EXP_ROOT_DIR) when it is loaded from an archive. Test:expAuditConfigRootTest. - The same mistake in the repair queue of the missing-libraries page (
ezpRepairQueue::root()), the SQL, MongoDB and OPcache profile switches invar/tmp, andexpContentJob::rootDir(), which besides namedkernel/rather than the installation without eZSys. Test:EngineArchiveInstallationRootTest. eZModule::addGlobalPathList()warned "Undefined global variable" when no module path list had been set yet.- The SOAP server warned "Undefined array key 1" on every call to a plain function, and stopped with a TypeError when
no function was registered. Test:
eZSOAPServerFunctionNameTest. eZPolicy::saveTemporary()comparedoriginal_idwith=== 0; the database returns"0", so a policy that is not a temporary copy was not refused. Test:eZPolicySaveTemporaryTest.- A login without a
RedirectURIfield logged the error "No such action parameter: UserRedirectURI".
6 October 2026: the setup's package download
Fixed
- The setup's package download tried once and, when it failed, said only "Download of package 'X' failed": the
reason went to the debug output alone. A package server that is being republished answers with errors for a few
minutes, and an install started then stopped.
eZStepSiteTypes::downloadFile()now tries up to three times, waiting 2, 5 and 10 seconds ([RepositorySettings] DownloadAttemptsandDownloadRetryDelays[]inpackage.ini), with a timeout for the whole transfer and for the connection (DownloadTimeout=300,DownloadConnectTimeout=30). An attempt fails on a curl error, an HTTP status other than 200, an empty or truncated file and an.ezpkgthat is not gzip data, and leaves no partial file behind. The message names the reason, e.g. "Download of package 'sevenx_multisite' failed after 3 attempts: HTTP 404, 9379 bytes received", on the setup page, on the command line and invar/log/setup.log, where each failed attempt is a warning. The download without curl reports and retries the same way. Test:SetupPackageDownloadTest.
6 October 2026: permission checks for users with many role assignments
Updated
eZContentObject::checkAccess()loads the locations of the object, the parent paths of an unpublished object and the node of aNodelimitation once per call instead of once per policy, andcanCreateClassList()hands the locations from oneclassListFromPolicy()call to the next (new optional third parameter). For a member of 120 teamrooms, checking read access to an article took 120 queries and 27.8 ms, now 1 query and 1.0 ms; the create menu of a folder 601 queries, now 2.- The content fetches merge the read policies that differ only in
User_Subtreeand leave out those that cannot give access to a fetched node (eZContentObjectTreeNode::mergeLimitationList(),pruneLimitationList(),permissionFetchScope(), optional fourth parameter ofcreatePermissionCheckingSQL()). The children of a folder in a teamroom took 24.4 ms with 800 read policies, now 1.2 ms; the fetches return the same nodes. New settingsite.ini [RoleSettings] PermissionSQLOptimization(enabled;disabledORs every policy as before). Guide: Permission checks for users with many role assignments.
Fixed
- On SQLite every content fetch of a user with 1,000 read policies or more failed ("Expression tree is too large")
and listed nothing. Merged, the 1,203 policies of the member above are 5, with 240 subtree conditions, and OR
chains of more than 100 parts are written in groups (
permissionSQLOr()), so thousands of policies or subtrees stay below the limit. Tests:eZContentPermissionSQLOptimizationTest(no database),eZContentPermissionSQLOptimizationLiveTest(live). - Before it reached main, the merging was corrected: the subtrees of a merged policy are put in parentheses (without
them
Class AND subtree 1 OR subtree 2gave subtree 2 to every class and listed nodes users may not read), aClasslimitation without values no longer leaves its policy out under a class filter, class ids are compared as numbers and the class filter type as the fetches read it, and policies that add no condition, left alone after the others were left out, give0 = 1rather than no condition. Checked on the demonstration server for every user, the read policies of every role and throwaway users of every limitation kind: 3,294 fetches, 28,224 access checks and 2,712 create lists give the same answers as before.
6 October 2026: renamed nodes keep their old addresses as redirects
Fixed
- On MySQL, PostgreSQL, SQLite and Oracle, renaming or moving a node left its old address as a second original
address instead of a history entry: the old link kept answering 200 instead of 301 to the new address, and a node
could have several original addresses per language.
eZURLAliasML::storePath()had lost its SQL history step when the MongoDB support was added; it runs again for every driver except MongoDB (an old entry of the language becomes history and links to the new one, an entry shared with other languages only loses the language's bit, children are reparented, custom and global aliases are not touched). On an installation that ran without it, publishing a node again, orupdateSubTreePath(), turns its stale addresses into redirects; run such scripts with the siteaccess the editors publish from (-s admin), since a siteaccess with anotherRootNodenames the content root differently. Tests:eZURLAliasMLHistoryLiveTest(live),eZURLAliasMLMongoStorePathTest. - On MongoDB, a URL alias entry was stored without the always-available bit of its object, and every call of
storePath()warned about an undefined$alwaysMask.
6 October 2026: no read access lists no content, and a disabled user is signed out once
Fixed
- A user without any
content/readpolicy, and a user whose access array could not be read (fail-closed), was listed every node by the content fetches, search, trash, keyword lists and the admin's child counts:createPermissionCheckingSQL( false )gave no condition. It now gives0 = 1; full access still gives none. See the behaviour changes of October 2026. Test:eZContentPermissionSQLTest::testNoReadAccessGivesAConditionNoNodeMeets. - A disabled user in the session ended the request in "Allowed memory size exhausted":
eZUser::currentUser()signed the user out, the audit record of the sign-out askedcurrentUser()who acts, which signed out again, without end. While the sign-out is recorded,currentUser()now answers with the user being signed out (eZUser::$recordingLogout, reset in afinallyblock and by Velocity between requests). Test:eZUserDisabledCurrentUserLiveTest(live).
7 October 2026: logs, INI cache and expiry timestamps per site
Added
- Settings for sites with a VarDir of their own on one installation (multi-site hosting), all in
site.ini [FileSettings]and off by default:UseGlobalLogDir=disabledwrites the debug logs and the default logs ofeZLoginto the log directory of the site,LogDirmay be an absolute path,ExpiryDirkeepsexpiry.phpoutside the cache directory (for a cache on a memory file system),INICacheDir=sitecaches the INI files of a site in its own cache directory,LogVarDirandCacheVarDir(e.g.var_log,var_cache) put the logs or caches of every site into a tree of their own with one line (var/<site>→var_cache/<site>); Velocity serves the public caches there (expVelocity::staticPaths()),.htaccess_rootand the nginx example have the rule to enable.eZSiteAccess::change()derives all of them again on every change, for web requests, Velocity workers and scripts. New:eZSys::logDirectory(),eZSys::relocatedVarDirectory(),eZDebug::setLogDirectory()andlogDirectory(),eZExpiryHandler::filePath(),eZSiteAccess::updateINICacheDirectory()andupdateLogDirectory(),eZUpdateDebugLogDirectory(),eZSys::cacheDirectoryOf()(the cache directory of another siteaccess, which the classic menu, the toolbar and the package datatype now use instead of a path of their own). Guide: Logs, INI cache and expiry timestamps per site. Tests:eZSiteLogAndExpiryPathsTest,eZSiteAccessSitePathsTest,eZSiteLogReadersTest(no database), cases inexpVelocityEnginesTest.
Updated
- What reads the logs follows where they are written: the setup log counts the errors of a step in the
error.logeZDebug writes (the log directory of the site withUseGlobalLogDir=disabled), and Setup > System information names that directory, warns when it cannot be written and finds the cronjob logs there. Audit records stay inside VarDir (audit.ini [AuditSettings] LogDir), whichLogVarDirdoes not move; the old text files of 4.x audit names now honour an absolute[AuditSettings] LogDir, and an absolute VarDir is no longer put inside the installation. - Velocity serves the public caches below a
CacheVarDirset for some siteaccesses only (read from every siteaccess when it starts), and its warm-up puts the log and INI cache directories of the site it rendered back (eZSiteAccess::resetSitePaths()). eZDir::recursiveDelete()deletes inside the cache, log, expiry and INI cache directories of the site also when site.ini puts them outside the root, and the cache directory as a whole, as it does inside the root. A directory that contains the installation is never one of them. An[FileSettings] AllowedDeletionDirsentry that does not exist allows nothing (it allowed every path), and an entry is a directory, not a prefix:/srv/cacheno longer allows/srv/cache-other. An installation that relied on either lists the directory it deletes from.- Velocity reads the
CacheVarDirof every siteaccess on aneZINIof its own when it starts, withouteZSiteAccess::load(): no extension is activated, no INI or extension cache file is written and the current siteaccess, the exception handler and the global INI instances stay as they were. - The Velocity warm-up finds the installation root and the snapshot of the globals its entry script takes, which it
lost when its code moved into
VelocityWarmup::run()(the pool requires the entry script inside a function): a site in maintenance is no longer rendered (the marker was looked for at/var/maintenance.json), each render hasSCRIPT_FILENAME<root>/index.php, and the final sweep removes only the globals added after the snapshot instead of every global but the superglobals andQ.webserver.keepGlobals. Without a snapshot the sweep is skipped.
7 October 2026: the dialogs of the online editor and pasted styles
Fixed
- The dialogs of the online editor (
ezoe/dialog,ezoe/relations) open for whoever may read the object, as before, and for whoever edits the version named in the address: the editors of a draft of an object that was never published, which no one can read yet, could not insert a link, an image or an embedded object. "Edits the version" is decided ascontent/editdecides it: the version is a draft (a draft, an internal draft or one to be repeated) of that object made by the current user, the object is not in the trash, andeZContentObject::editAccess()allows it in the version's own language (a Language limitation applies, a listener ofcontent/edit/accesshas its say). Someone else's draft, a published, pending or archived version, or a version that does not exist opens nothing for who may not read the object.ezoe/dialogopens only a dialog whose name is a plain template name (letters, digits,_and-).Exponential\View\Extension\Ezoe\Ezoe\Dialog::mayOpen(),::mayEditVersion(),::isDialogName(). Tests:expOEDialogAccessRulesTest(no database),expOEDialogAccessTest(live). - A pasted style declaration without a colon (
style="color; text-align: right") raised two PHP warnings; it is left out now, and a value keeps the colons it holds (an address). Only a plain CSS property name becomes an attribute: a declaration namedxmlnsorxml...no longer puts a namespace declaration into the stored XML (withxmlns:xthe stored XML did not load again without a warning). An alignment (text-align,float,align) is taken only as a keyword, in lower case, without!importantand without a browser prefix (-webkit-centeriscenter); markup, a script address or an expression in it is left out. Tests:expOEPastedStyleRulesTest(no database),expOEPastedStylesTest(live).
7 October 2026: uploads, the embed dialog and the markup of the online editor
Fixed
- An upload from the online editor (
ezoe/upload) asked only for edit access to the object, whatever version number the address named, so it could create an object and relate it to someone else's draft or to a published or archived version. It now writes only into a draft of the user's own that they may edit in its language, decided ascontent/editdecides it (Dialog::mayEditVersion()), and checks that before it fetches or creates anything.ezoe/upload(the dialog) andezoe/tagsopen by the rule of the other dialogs together with edit access to the object: whoever could open them before still can, and who may edit but not read the object opens them only for a draft of their own.Dialog::mayOpenForEditing(),Upload::mayUpload(). - The uploaded object became a relation of version 1 (the version of the new object) instead of the version being
edited; it now goes to the version being edited.
Upload::addEmbedRelation(). - The online editor put the stored class, align, width, border, colspan, rowspan, href, target, title, id, view, size
and anchor name, the name of a custom tag and of an embedded object, the address of a custom tag image and the style
of a custom attribute into its markup unescaped, so a stored value with a quote could add attributes or markup in
the editor. Each value is escaped once where it goes into the markup (
eZOEXMLInput::markupValue()); values that are escaped already are not escaped again, and opening and saving gives the stored values back unchanged. The text of a header with an anchor is escaped as other text is. ezoe/relationswithout an EmbedID, or with a malformed one, raised undefined variable and undefined key warnings before its message; it ends with the message now. The EmbedID is an object id,eZObject_<id>oreZNode_<id>(Relations::parseEmbedId()).- The editor no longer raises deprecation notices for a missing user agent on the command line, no longer rebuilds the
style map of the custom attributes on every call when
ezoe.inigives none, and saving a paragraph whose custom tag was removed no longer reads a missing parent. Tests:expOEDialogAccessRulesTest,expOEMarkupEscapeTest,expOERelationsEmbedIdTest(no database),expOEUrlFetcherTest,expOEEditorViewAccessTest(live). ezoe/loadreads its EmbedID asezoe/relationsdoes (Relations::parseEmbedId()): a missing or malformed one answersfalsewithout a PHP warning. The content type ofezoe/uploadandezoe/relations, which goes into the path of a template (design:ezoe/upload_<type>.tpl,design:ezoe/tag_embed_<type>.tpl), must be one of the editor's types (objects, images, files) or a relation group ofcontent.ini [RelationGroupSettings], and a plain template name; anything else ends with a message.Dialog::contentTypes(),Dialog::isContentType(). Test:expOEContentTypeAndLoadTest(no database).
7 October 2026: copy the debug bar's messages
Added
- A Copy Messages button on the Messages tab of the debug bar copies the messages that are shown (level buttons
and filter box applied) as plain text: "Level: source", a tab and the time, then the message, a blank line between
entries. It copies with
navigator.clipboardand falls back to a hidden text field where the page is not a secure context; a status line says "Copied N messages" or that the copy failed. Translated in eng-US and ger-DE (contextdesign/standard/debugbar).expDebugBarReport::copyMessagesHtml(). Guide: The Exp Debug bar. Test:expDebugBarReportCopyMessagesTest(no database).
7 October 2026: downloads from streams that return short reads
Fixed
eZFile::downloadContent()added the size it askedfread()for to the bytes sent, not the size it got. A stream that returns less per read (a userland stream wrapper hands back one 8 KB buffer, network and cluster mounts return what has arrived) ended the transfer after the first read, and the client got a truncated file with status 200. It now counts what was read and stops at the end of the file or an empty read; ranges and plain files behave as before. The DFS/MySQLi cluster gateway (index_cluster.php) sent a range with a singlefread()and had the same result; it reads until the requested length. Test:eZFileDownloadShortReadTest(no database).
7 October 2026: content limitations of extensions in searches
Added
- A content limitation handler can also restrict searches in a search engine that filters by the policies of the
user itself, such as eZ Find: it implements
ezpContentLimitationSolrHandler(solrFilter()) besidesezpContentLimitationHandler. The search extension asksezpContentLimitation::solrFilter(), which gives the filter in parentheses, to join with AND to the other limitations of the policy. When the policy gives no access in the search the filter isezpContentLimitation::DENY_SOLR, which matches no document, and neverfalse: a search extension that left the policy out would filter by nothing once no policy is left. That happens without a handler, for a handler without the interface, for a kernel limitation, when the handler fails, and for an answer that is not a self-contained filter (string filters may not leave their parentheses or range brackets, nor use local parameters or nested queries). Field conditions (array( 'field' => ..., 'values' => ..., 'not' => false )) are written and escaped by the kernel (ezpContentLimitation::solrValue(), which also keeps a valueAND,ORorNOTa term); anotthat is not a boolean, or a value that is not UTF-8, gives no access. Guide: Content policy limitations of extensions. Test:ezpContentLimitationSolrTest, including an allow/deny matrix of mixed policies.
8 October 2026: the session cookie in every response, and the flags of is_logged_in
Added
site.ini [Session] CookieAlwaysAddToHttpResponse(disabledby default): with it enabled, the session cookie is sent with every response of a running session, not only when the session starts, for a load balancer that keeps a user on one server by the session id in the cookie. Same name, value, path, domain, lifetime and flags as the session cookie (a lifetime counts again from each response), sent once (not again when PHP already set it in the response), and nothing without a running session: an anonymous visitor without a session gets none. Such a response is kept out of shared caches: itsCache-Control(all its lines, read together) becomesprivate(public,s-maxageandproxy-revalidatedropped,max-ageand the rest kept for the browser;no-storeneeds noprivate), aSurrogate-Controlbecomesno-storeand anX-Accel-Expires0, so no proxy, CDN or service worker stores a session id for the next visitor. Velocity's response cache stores no response that sets a cookie anyway; the role-aware HTTP cache (httpcache.ini) keeps pages without their cookies, and a page it answers before the kernel starts carries no session cookie. Off, nothing changes.eZSession::sendResponseCookie(),responseCookie(),cookieOnEveryResponse(),sessionCookie(),privateCacheControl(),privateCacheHeaders(),listsCookie().
Updated
- The
is_logged_incookie (for HTTP caches) is sent with the path, domain, lifetime,SecureandSameSiteof the session cookie (site.ini [Session] CookiePath,CookieDomain,CookieTimeout,CookieSecure,CookieSameSite), so it goes wherever and as long as the session cookie does; it was sent without flags, for the installation directory and until the browser closed. It stays readable by scripts (notHttpOnly), as scripts of cached pages read it. WithCookieAlwaysAddToHttpResponseenabled it is sent with every response of a signed-in user, as the session cookie is. Anis_logged_inset before this change for another path or without the domain is not removed at sign-out and stays until the browser closes; a cache then only passes that browser's requests to the server.eZSession::siteCookieOptions(); the parameters of the session cookie are available without setting them:eZSession::cookieParams(). Test:eZSessionResponseCookieTest(no database; several requests in one process as in a persistent worker).
8 October 2026: updates of persistent objects on Oracle
Fixed
eZPersistentObject::storeObject()wrote the WHERE condition of the UPDATE of an existing row with the long field names, while the field list, the INSERT and the lookup of the row use the short names of a database that has them (Oracle,eZDBInterface::useShortNames()). A key with a short name named a column the table does not have, and the update failed. In the kernel this iseZEnumObjectValue(contentobject_attribute_versioniscontentobject_attr_version), whose row is updated when a value is stored again for the same attribute version;eZURLObjectLinkhas the same key but no other fields, so it is never updated. The condition now uses the short names too.- The other queries
eZPersistentObjectbuilds name a field with a short name by it as well: ORDER BY and GROUP BY offetchObjectList(), the SET and WHERE ofupdateObjectList(), the order column and condition ofnewObjectOrder(), and the UPDATEs ofreorderObject(). A long text field with a short name that is bound as a variable (more than 2000 characters on Oracle) was written into the UPDATE as a literal instead of its bind variable. A column with a short name that is NULL came back under its short name, and a definition whose fields are plain strings stopped with a TypeError on Oracle. No kernel definition hits these today; they matter for extension definitions with short names. bin/php/convertezenumtoezselection.phpremoved the converted enum values with its own SQL and the long column name; it removes them througheZEnumObjectValue's definition now.- Databases without short names (MySQL, PostgreSQL, SQLite, MongoDB) get exactly the same queries as before. Test:
eZPersistentObjectShortNameTest(no database; compares every query with and without short names). Verified end to end against Oracle Database 23ai Free with the ezoracle driver: every path above works there; the UPDATE, ORDER BY, GROUP BY,updateObjectList()andnewObjectOrder()failed there before.
8 October 2026: notifications for some classes only, and notify for eZContentFunctions
Added
notification.ini [NotificationSettings] NotificationFilterByClassIdentifierandIncludeClasses[]: with the setting enabled, only publications of the listed classes make a notification event; any other class counts as published without notification (the filtercontent/notification/creategetsfalse).disabledby default, so nothing changes without it. The list takes class identifiers; an entry of digits is taken as a class ID.eZContentOperationCollection::notificationIncludesClass().eZContentFunctions::createAndPublishObject( $params, $notify = true )andupdateAndPublishObject( $object, $params, $notify = true )passnotifyto the publish operation, so imports and scripts can publish without notification. Callers without the argument notify as before;false,0and"0"mean no, anything else yes (eZContentOperationCollection::notifyRequested(), which the publish operation's notification step uses too). Guide: Publish without notification. Tests:eZPublishWithoutNotificationTest(PN-13 to PN-15, no database),eZPublishWithoutNotificationLiveTest(PL-05 to PL-09, live, with the file mail transport set in-process).
Fixed
eZContentFunctions::createAndPublishObject()returned the object it had instantiated, which still said draft (status, no current version published, no main node) after the publish operation had published it; it now returns the object as stored after publishing.
8 October 2026: limits for the modes of new files and directories
Added
- The constants
EZP_DIR_MODE_MAXandEZP_FILE_MODE_MAX(inconfig.php, seeconfig.php-RECOMMENDED) set the widest mode any directory and any file the installation creates may get. A mode asked for is only narrowed (0777 becomes 0750, 0600 stays 0600), andautoload.phpsets the umask of the process from them, so files written without a mode of their own stay inside them as well. Without the constants nothing changes.eZFile::fileMode(),eZDir::dirMode(),eZFile::creationUmask()in place ofumask( 0 ),eZFile::applyCreationUmask(); used byeZDir::mkdir(),eZDir::directoryPermission(),eZFile::create(),eZLog,eZDebug,eZINI(cache directory, cache and saved files),eZPHPCreator,eZFileHandler, the autoload generator, uploads (eZHTTPFile), image variations (eZImageHandler), the cluster file handlers and the static cache. One constant gives the other limit; a value that is no mode, or a limit that would take rights from the owner (0), is ignored (no limit, as without it) and reported once to the PHP error log. With 0770/0660 for a web server and command line scripts of different users, the directories ofvar/need the set-group-ID bit of the common group (see the guide). Guide: Limits for the modes of new files and directories. Test:expFileModeLimitsTest(security suite, no database), which also fails for a new nativechmod(),mkdir()orumask()that bypasses the helpers; the calls still to be moved are listed in it.
Fixed
- With the limits set, the autoload generator creates
var/autoloadwith a directory mode instead ofEZP_INI_FILE_PERMISSION(a file mode, which left the directory without its search bit) and limits its files byEZP_FILE_MODE_MAX, which takes away the execute bits of the 0777 they got. Without the limits both stay as they were.
8 October 2026: every new file and directory within the limits
Updated
- Every native
chmod(),mkdir()andumask()in the kernel, the libraries, the scripts and the extensions of the repository now goes througheZFile::fileMode(),eZDir::dirMode(), the neweZFile::executableMode()(for the FrankenPHP binary and the scriptsbin/php/consoleruns, limited byEZP_DIR_MODE_MAXso they stay executable) oreZFile::creationUmask(): the audit trail, content jobs, Velocity and FrankenPHP, the INI editor, mail preferences, setup and installer, HTTP cache, debug bar, maintenance, notifications, packages, templates written in the admin, shop receipts, the query cache and others; command line scripts without the.phpending are checked too. WithoutEZP_DIR_MODE_MAX/EZP_FILE_MODE_MAXthe modes are the same as before; narrow umasks set on purpose stay at least as narrow.expAuditWriter::ownLikeParent()limits a directory as a directory and a file as a file. The list of calls still to be moved inexpFileModeLimitsTestis empty, so the test fails for any new call that bypasses the helpers. The HTTP cache early exit and the repair queue, which run before the autoloader, load the helpers themselves. Files that the web server and the command line both write need one user, or one group with 0770/0660 and set-group-ID directories, once limits are set. Files written without a mode of their own keep to both limits, also for a pair that does not match (0750/0600 gives 0600 and 0710). Guide: Limits for the modes of new files and directories.
8 October 2026: the column conditions of content limitation handlers
Fixed
- A column condition that a content limitation handler returns from
permissionSQL()(array( 'column' => ..., 'values' => ..., 'not' => ... )) was inverted by anynotthat PHP reads as true, so'not' => 'false'turned "only these" into "all but these", and with no values into a condition that matches every row;'not' => 0did the reverse. As in search filters, only a real boolean inverts now: any othernotmakes the answer no condition, and the policy gives no access in fetches (ezpContentLimitation::DENY_SQL).ezpContentLimitation::sqlCondition()andsolrCondition()share the check. Guide: Content policy limitations of extensions. Test:ezpContentLimitationTest.
8 October 2026: fatal errors of the kernel without E_USER_ERROR
Fixed
- PHP 8.4 deprecates passing
E_USER_ERRORtotrigger_error(). The kernel did so when a MySQLi query failed with the standard error handling, when the cluster gateway (index_cluster.php,ezpClusterGateway) answered with a 500, wheneZExpiryHandlercould not readexpiry.php, and ineZDebug::writeError()while errors are handed to PHP (a command line script beforeeZScript::initialize(), or a message written while a log file is written). They call the neweZFatalUserError::raise(), which does what PHP did, without the deprecation: the error handler that is set getsE_USER_ERRORwith the message, file and line, and the script goes on when the handler takes it (eZDebug's handler, set for every web request, logs the error and returns, as before); without a handler, or when it answers false, the script stops with an uncaughteZFatalUserError(anError, so code that catchesExceptiondoes not take it): exit code 255 on the command line, a 500 on the web, and a persistent worker (Velocity) answers the request with a 500 and serves the next one instead of ending. Test:eZFatalUserErrorTest, which also fails for any new call that passesE_USER_ERRORtotrigger_error().
9 October 2026: SOAP calls to servers with an internal certificate
Added
eZSOAPClient::setCAFile()trusts the CA certificates of a file of its own for the certificate of an HTTPS server, for a service whose certificate an internal CA issued.setVerifyPeer( false )turns the check off for one client only and logs a warning on every call; by default the certificate is verified, as before.caFile()andverifyPeer()read them;curlOptions()gives the cURL options of a call. A class extendingeZSOAPClientthat declares members of these names has to rename them. Guide: SOAP calls over HTTPS. Test:eZSOAPClientTlsOptionsTest(no server).
Fixed
eZSOAPClientwithout the PHP extension curl sent a call over HTTPS as plain text to the TLS port; it is refused with an error now (send()returns0,ErrorStringand the error log say that HTTPS needs curl). This is a behaviour change: see Behaviour changes of 1 to 2 October 2026, "Missing libraries". Test:eZSOAPClientTlsOptionsTest(in a PHP started without curl).eZSOAPClient::setCAFile(): a name with a NUL byte sets no CA file (reported), instead of aValueErrorinsend(); underopen_basedira CA file outside it is no longer reported as unreadable on every call, since cURL reads it anyway.
9 October 2026: leaving out a cronjob part
Added
runcronjobs.phpasks theezpEventfiltercronjob/part/runwith the part, the siteaccess, the scripts and whether a single script runs (--script) before the first script of a part runs. A listener insite.ini [RunnableSettings] Listeners[]returnstrueto run the part; anything else leaves it out with a notice and exit code 0, so a stop switch for a release no longer needs a patchedruncronjobs.php. A listener that throws ends the run with exit code 1 before any script runs. Without listeners every part runs as before. Also on Setup > RAD. Feature. Test:CronjobPartRunFilterTest(no database).
9 October 2026: sendmail without an envelope sender
Added
site.ini [MailSettings] SendmailEnvelopeSender=disabledmakes the sendmail transport leave out-f <sender>. msmtp with--read-envelope-from(for instancesendmail_path="msmtp --read-envelope-from -t") refuses-ftogether with that option, so every mail failed there. A mail without a sender of its own (no sender address and noFromextra header) then gets a From header withEmailSender, elseAdminEmail, when one of them is a valid address (also writtenName <address>); msmtp reads the envelope sender from it. The header is written for that one call: theeZMailobject keeps the sender it had. The defaultenabledkeeps-fand the options as before. Test:eZSendmailEnvelopeSenderTest(no database;mail()runs in a child process against a capturingsendmail_path, which the child checks before it sends, so nothing is sent).
9 October 2026: one User-Agent for the mails of the site
Added
site.ini [MailSettings] UserAgentsets the User-Agent header of every mail sent througheZMail(sendmail, SMTP and file transports; ASCII only), for instance the name of the portal. Empty (the default) keeps the mails as they were:Exponential, Version <version>over sendmail and file,Apache Zeta Componentsover SMTP.eZMail::setUserAgent()still sets it for one mail. Line breaks in the setting do not start a header. Guide: After installing, 10.4. Test:eZMailUserAgentTest(no database).
Fixed
eZMail::setUserAgent()reaches mails sent by SMTP. The mail library wrote its own User-Agent when it generated the headers, so the value was lost there;ezpMailnow keeps the one eZMail gave it. Code that already callssetUserAgent()therefore sends its own name over SMTP from now on instead ofApache Zeta Components; over sendmail and file nothing changes.ezpMail::$userAgentis public: a value set on it directly gets the same line break cleaning assetUserAgent(), so it cannot add a header either.
9 October 2026: who trashed what, in the database
Updated
- Who moved an object to the trash and from where are columns of the trash row now:
ezcontentobject_trash.trashed_by(user content object id) andtrashed_via(web <siteaccess>,cli <script>), written byeZContentObjectTrashNode::createFromNode()in the same transaction as the row.<VarDir>/trash/trashed.json, which was local to one web server and outside transactions and backups, is no longer written once the columns exist; the trash view still reads it for older rows, andupdate/common/scripts/6.0/movetrashrecords.phpcopies it into the columns. The filter by user runs in SQL. The database update of each engine adds the columns. Guide. Tests:TrashedByColumnsTest(no database),eZContentObjectTrashRecordTest(installation database). - Code and database update can come in either order. Until the columns exist,
eZContentObjectTrashNode::hasTrashedByColumns()says so (asked of the database's catalogue, never by a query that could fail inside a transaction, again on each trash move until the columns are found, so a Velocity worker needs no restart) anddefinition()leaves them out: moving content to the trash stores the row without them and records who did it in the file as before, and fetching a trash row, the trash view with its filter by user and restoring keep working. Before, every move to the trash ended with "A database transaction failed" until the update had run. Test:eZContentObjectTrashOldSchemaTest(an installation database without the columns; skipped on an updated one),TrashedByColumnsTest. trashed_viaholds printable ASCII within 100 bytes (anything else becomes?): a script name in another encoding is no valid UTF-8, which PostgreSQL refuses inside the trash move's transaction, and 100 multibyte characters exceed a column that counts bytes (Oracle). Test:TrashedByColumnsTest.movetrashrecords.phpstops with exit status 1 whentrashed.jsonholds no JSON object (cut short, edited by hand): it was read as empty, so--remove-filedeleted it with every entry. Its line of counts adds the rows whose entry names no user, so the numbers add up. An entry withoutnode_idortrashedmatches no row without a PHP warning. Tests:TrashedByColumnsTest,eZContentObjectTrashRecordTest.- The trash view no longer reads the whole trash for every page:
TrashListasks the database for what the page needs (the trash rows among the ancestors of its items, a count on the indexedpath_stringfor the nodes below each, counts in SQL for the summary and the user list), andezcontentobject_trashgets the indexezcobj_trash_trashed_by(in the same update files). With 100,000 rows in the trash a page took 2.7–3.2 s and 160 MB on SQLite, now 0.54–0.90 s and 2–3 MB; the measurements are in the guide. The user list and the "known for" count of the summary count trash rows, not objects. On MongoDB the summary and the user list come from aggregations (the driver translates neitherNOT EXISTSnorSUM( CASE ... )norGROUP BY, so they showed nothing); a path that ends in a line break is refused bybelowCondition()like any other non-path. Tests:TrashedByColumnsTest,TrashListPagedTest,eZContentObjectTrashRecordTest. - MongoDB keeps who trashed what in the trash documents too, and its trash view works.
hasTrashedByColumns()asks the driver's schema (expMongoDB::declaredColumns(), from the shipped.dba), which declares both fields; new trash documents carry them. The trash list and its count were one SQL statement with joins, which the driver cannot translate: on MongoDB the view listed and counted nothing, and so emptied nothing. They now come from one aggregation (eZContentObjectTrashNode::trashListMongo(),trashListFilterMongo(),trashListSortMongo()) with the same filters, sort keys and paging; a user whosecontent/readis limited by policies is shown an empty trash rather than all of it, and anAttributeFilterlikewise lists nothing. The class filter's options (TrashList::classOptions(), also a join) come from two single-table reads. Nothing changes on the SQL engines. Guide. Test:TrashListMongoTest. - The MongoDB driver reads and compares a field that a document lacks as the SQL engines read the column: a column
the
.dbadeclares NOT NULL with a default holds that default in every row of a SQL table, also in a row written before the column was added, while a document written before then has no field, which MongoDB's$eq,$inand ranges never match and$neand$ninalways do. So "WHERE trashed_by = 0" missed every trash document older than the field, andmovetrashrecords.phpreported rows as given atrashed_bythat its UPDATE never reached. The WHERE of UPDATE, DELETE and single-table SELECT now matches the missing field exactly when the default satisfies the comparison (=, <>, IN, NOT IN, <, <=, >, >=; IS NULL and LIKE are unchanged), and a SELECT that names such a column returns the default for a document without it, NULL for a declared column without one. Test:expMongoDBDeclaredDefaultTest(also against a real server withEXP_TEST_MONGO_SCRATCH).
9 October 2026: keeping items in the trash for a number of days
Added
content.ini [TrashSettings] KeepItemsForDaysmakes thetrashpurgecronjob part purge only what has been in the trash for at least that many days. Empty or 0, the default, purges everything as before; a value that is not a whole number of days up to 36500 purges nothing and reports an error.bin/php/trashpurge.php --trashed-days=<days>is unchanged. Items trashed before 6.0 have no trash time and go on the first run (see the guide). Guide: The trash view. Test:TrashPurgeKeepDaysTest(no database). The cronjob reads the setting past the INI cache, so a value just added counts on the next run instead of the cached "purge everything".
Fixed
bin/php/trashpurge.php --trashed-dayswith a huge number of days purged the whole trash: the date overflowed into the future. Some numbers overflow into a past that is not that many days back instead, which purged against the wrong date.eZScriptTrashPurge::trashedBefore()now accepts only a date within a day of that many days back and the purge refuses anything else with an error. Test:TrashPurgeKeepDaysTest.
9 October 2026: the HTTP cache with request-shield in front
Added
- The role-aware HTTP cache works with request-shield in front: a
request it marks
allow-uncached($_SERVER['REQUEST_SHIELD']) is answered and never stored (X-Exp-Cache: BYPASS (request-shield: not for a cache)), and withset cache-unknown-query hit-onlythe early exit answers a request with a query parameter no key holds from the page request-shield names inREQUEST_SHIELD_CACHE_LOOKUP(X-Exp-Cache: HIT (request-shield lookup)). Without request-shield nothing changes. Under Velocity only the first applies. Guide: With request-shield in front. Test:ezpHttpCacheContractTest(HC-18 to HC-20).
Updated
- With
ContentChangePurges=tagsa content change also purgesrlof every node androf every object, the tags Ibexa's HTTP cache purges, for pages an extension tagged that way. More than 10000 purged tags withinMaxAgenow purge every page instead, so the state every lookup reads stays small. Test:ezpHttpCacheContractTest(HC-21).
Fixed
- The lookup of
REQUEST_SHIELD_CACHE_LOOKUPis taken only when it names exactly the request's own path and parameters thatQueryStringParameters[]lists: a request-shield whosecache-queryleft one of them out had/news?page=2&x=7answered with/news. Its answer isCache-Control: private, so a proxy in front keeps no made-up address, and an expired page is served to it asSTALEwithout the lookup taking the page's refresh, which it would never store.
9 October 2026: the RAD survey's source sweep skips dependency trees
Updated
- Setup > RAD no longer walks into
tests,vendor,node_modulesor.gitdirectories when it collects the php files of the kernel, lib and the extensions. It already ignored what it found there, but it still entered them, and every check of the kept sweep looked at each of their directories again: an extension that carries thenode_modulesof its JavaScript build doubled the directories checked (8,558 to 4,146 on one installation), and the first load after a cache clear ran long enough to time out. The php files collected are the same.
9 October 2026: the online editor keeps an embedded gallery's view mode
Updated
settings/content.inilistsgallery_view_modeamong the custom attributes of[embed]. The embed view of a gallery reads it (slider or grid), but the online editor keeps only the custom attributes content.ini names, so every save of an article dropped it and the gallery fell back to the grid. Installations and the site settings the installers write add their own[embed]attributes to this one, so every siteaccess gets it.
10 October 2026: the content finder
Added
- Setup > Content finder (
/contentfinder/search, kernel modulecontentfinder) and a search box below the content structure in the Content tab: find the content object behind an object, node, attribute or version id, a remote id, a URL alias, a page URL, or the path or URL of an image, binary or media file belowvar/(exactly, or every file below a prefix). Each result shows the object's metadata, every version with a link to the version view, every location with its URL alias, and its trash entry (where it was, when and by whom it was trashed). From the result an object can be moved to the trash, deleted permanently, purged from the trash or restored, and a version removed, each through a confirmation page that lists every location, the nodes below them and the versions; what cannot be undone needs an explicit tick. The actions use the kernel's own operations (content/delete, the trash service's purge,content/restore, version removal as in the history). - The policies
contentfinder/search(use the finder) andcontentfinder/delete(its actions). Each result shows only what the user's content policies allow (content/read,content/versionread,content/restoreorcontent/cleantrash), and each action needs its content policies as well; something the user may not see is answered exactly like a miss. Only the Administrator role holds the new policies in a new installation. settings/contentfinder.ini(Enabled,SiteAccessList[],ResultLimit,FileRowLimit): the finder answers on administration siteaccesses only.- The audit events
content.finder.searchandcontent.finder.action(channelcontent, on by default): every search with its type, text and number of results, every action done or refused, with the kernel's removal events as its children. The catalogue has 143 names. - German translations of the finder.
Feature. Tests:
expContentFinderInputTest(no database),expContentFinderSandboxTest(writes, so only in a throwaway copy named byEXP_CONTENTFINDER_SANDBOX);--testsuite kernel-contentfinder.
10 October 2026: a per-request file named in config.php
Added
index.phpruns the file thatconfig.phpnames in the constantEXP_REQUEST_PREPEND_FILEat the start of every request, after the settings and before the kernel. Under a persistent application server (Exponential Velocity)config.phpruns once per worker, so code there cannot act on each request; this is the place for a request filter in front of the site. Onlyconfig.phpcan name the file. Without the constant nothing changes.
Updated
config.php-RECOMMENDEDdocumentsEXP_REQUEST_PREPEND_FILE, with request-shield under Velocity as the example, names the product Exponential in its comments and gives the real default ofCLUSTER_HEADER_X_POWERED_BY.
11 October 2026: the content finder by the address a visitor sees, on every database engine
Updated
- The content finder finds a page by the address a visitor sees on a siteaccess with a
PathPrefix:/site/showcase/test-product, its full URL and the bareshowcase/test-productnow find the alias stored asfit-healthy/showcase/test-product. The siteaccess is matched as the kernel matches it (URI, host, host and URI), its ownPathPrefixis put in front (honouringPathPrefixExclude[]), and a bare alias is tried with each distinct prefix after the exact match; Found by names the siteaccess and prefix. Tests:expContentFinderURLAliasTest. - The content finder matches remote ids and file paths in their case on every database engine: MySQL and MariaDB
compare without case, and so did a file prefix on SQLite. Its database tests run on MySQL/MariaDB, PostgreSQL,
Oracle and MongoDB too (in a throwaway database named by
EXP_CONTENTFINDER_SANDBOX_DATABASE); the test fixtures use single-table queries. Tests:expContentFinderSandboxTestCS-12. - The content finder finds a URL alias by its last part (
test-product) or a shorter tail (showcase/test-product) when nothing else resolves: every alias element with that text, checked upwards against the rest of the path, listed with its full path; old addresses (history) only when nothing current matches, and marked. Exact comparisons only. Tests:expContentFinderSandboxTestCS-13. - MongoDB: the driver translates
LIKE '…' ESCAPE '!'(the content finder's file prefixes found nothing) andLOWER( column ) = '…'/UPPER; the persistent object fetch on MongoDB compares aLOWER( login )condition in the server, soeZUser::fetchByName()andfetchByEmail()find the user again;eZURLAliasML::fetchByPath()(andfetchNodeIDByPath()) reads a path one element at a time instead of the join the driver refuses. Tests:expMongoDBWhereTextTest,expContentFinderSandboxTeston MongoDB. - The content finder finds a URL alias by a part of it when nothing else matches:
product,testortest productfindfit-healthy/showcase/test-product(words in order, any of-,_and space between, without case;%,_and quotes are text). Current entries first, history only when nothing current matches; the closest matches first; at least 3 characters (with a hint below that). Newcontentfinder.inisettingsPartialAliasSearch,MinimumPartLength,PartRowLimit. The MongoDB driver translatesLOWER( column ) LIKE. Tests:expContentFinderURLAliasTestCU-06,expContentFinderSandboxTestCS-14,expMongoDBWhereTextTest. - The content finder guide lists PostgreSQL 13, MongoDB and Oracle 26ai among the tested databases, with the cost of the part search on each. Feature.
11 October 2026: a JSON format for packages
Added
- A package can be kept as
package.jsonand JSON documents instead ofpackage.xmland XML documents. Every XML document of the package (the definition, the files of its items, the object files) is mirrored element for element in JsonML, rich text kept as its XML string, so the package handlers and datatypes read the same DOM from either format and a package converts from XML to JSON and back without loss. The classic format stays the default; a classic package, its archive and its installation are unchanged.share/package/schema/package-v1.jsonis the JSON Schema of the format (expJsonSchemaValidator), andpackage.jsoncarries the SHA-256 of every file. expPackageJson(mapping, conversion of directories and archives, validation, checksums) andexpPackageConflictReport(what an install meets here, by remote id).eZPackageexports in either format, detects the format of an import by its content and keeps package metadata (<metadata><entry key>) for tools.- Install as drafts, a generic install option: content objects become drafts, nothing is published and no published object changes; an existing object gets a new draft or is left alone, an object of another class is a conflict; other items are listed instead of installed.
ezpm export --format=json,ezpm convert,ezpm validate,ezpm checkandezpm install --as-drafts.- Setup > Packages: Download as JSON, the package's format and metadata, the install page's report by remote id
and the option to install as drafts. The upload check and the package catalog accept
package.json. settings/package.ini [PackageFormatSettings](RawXMLDatatypes[],MaxDocumentBytes). Guide: The JSON package format. Test:expPackageJsonTest.
Updated
- The JSON package format page shows rich text without the editor's namespace URIs.
11 October 2026: headers an extension decides per page in the HTTP cache
Added
- The filter event
httpcache/store/headers: a listener (site.ini [Event] Listeners[]=httpcache/store/headers@<callback>) gets the headers the role-aware HTTP cache keeps with a page when it stores it, and returns them with its own added. A page answered from the cache is assembled without the kernel, from the headers stored with it; those were only the content type and the security headers, so a header an extension adds for one page (the content license of xrowmetadata, for example) was missing on every cache hit. Guide: Headers an extension adds to a page.
11 October 2026: Exponential Velocity streams, deploys without a needless reload, and starts each worker clean
Added
settings/velocity.ini [ServerSettings]documentsStreamTimeout(seconds a stream may run instead of the request timeout; engine default 300),StreamShare(the share of the workers event streams may hold; 0.25),MaxStreams(the same as a number of workers) andRetryWithin(seconds within which a GET whose worker died is run again, once; 10), andexp:velocitypasses them to the engine. All are empty by default, so the engine's own defaults apply. Past the stream cap an event stream is answered 503 withRetry-After, so pages keep the rest of the pool. Guide: Streaming responses.[DeploySettings] WarmUpURL[]: the last step ofexp:velocity deployrequests each URL once, one after the other, from a detached background process, so the first visitor does not rebuild the compiled templates and the INI, override, translation, design and packed-file caches. The deploy does not wait for it.
Updated
[DeploySettings] PhpFpmReload=auto(the default) skips the PHP-FPM reload when the pool's OPcache revalidates files (validate_timestampson,revalidate_freqat most 60 s) and waitsrevalidate_freqplus one second instead, so the rendered-output caches are still cleared only after PHP-FPM runs the new code. A graceful reload waits for the longest request on every pool of that PHP version, which made the first admin request after a deploy take 50 to 100 s.alwaysandneverare the other values;deploy --fpm-reloadforces a reload (php.ini or pool changed, new extension, files removed or renamed in place). Guide: Deploying.- The Velocity warm-up clears the response it rendered (its state, the response object, the headers and the status)
before the workers are forked. The first request of every worker began with the front page's headers already set,
so a sign-in page, an access-denied answer or a JSON call could go out with the front page's content license,
X-Exp-Cache: MISSandCache-Control. - The users and access services of expservices declare their lists in the class body instead of assigning them after
the class: a persistent Velocity worker puts a class's statics back to their declared value before its next
request, so from a worker's second request on those 207 services answered "not declared". Test:
expUsersDeclarationsTest. Guide: Backend ezjscore services.
11 October 2026: read services as tools of the assistant
Added
- A service declaration of expservices may carry
'ai' => true(or'ai' => array( 'summary' => '...' )): the assistant of Exponential Intelligence may then call it. One declaration serves HTTP (ezjscore/call), MCP (tools generated from the catalogue) and the assistant, each with the caller's own permissions. Services stay private to the assistant by default, and a service that writes is never offered; the catalogue reports the flag asai. Opted in, as reads checked by the content's own read access:expnodechildrenNames,childrenCount,breadcrumb,tree,latestandfind;expsectionlistandofNode;expstategroupsandofObject;exptagsroots,children,tree,searchandofnode. Guide: Backend ezjscore services.
11 October 2026: the online editor's engine page
Updated
ezoe/enginelists every entry ofezoe.ini [EditorSettings] Engines[]with its name, version, description and status (available, in use now, not available because its files are missing, or why it cannot be used), so an engine an extension registers (Tiptap, for example) shows up with what it is. An engine may answer the optionalversion()anddescription();expOEEditorEngineBaseanswers both from[Engine_<id>] VersionandDescription, and the TinyMCE 3 and 8 engines read their version from the bundled script. English and German translations. Guide: The online editor engines. Tests:expOEEditorRegistryTest,expOEEngineViewTest.
11 October 2026: fixes found by the CI workflows
Updated
- The setup's package download and the Solr check read the HTTP status on PHP 8.0 to 8.3 too: they open the URL with
fopen()and take the headers from the stream'swrapper_data. On those versions a failed package download said "HTTP 0" and the Solr check called a running Solr unreachable. - The class-based views
content/versionview,error/viewanduser/forgotpasswordread their optional view parameters (FromLanguage,ExtraParameters,HashKey) from the view's scope, which static analysis can follow; no change in behavior. - Tests that expected old contracts: the REST layer's OAuth token test expects the
Bearerscheme it reads since the personal API keys (and that a personal key is never taken as an OAuth token), and the older MongoDB adapter test expects the statements the driver translates now. - The eng-US, German and untranslated translations carry the Syndication tab, the debug quick settings of the administration and the content finder's Setup entry and page path, so the debug output lists no missing translation for them.
12 October 2026: the autoload generator leaves out the tests of every extension
Fixed
bin/php/ezpgenerateautoloads.php -eand-otook thetests/directory of every extension intovar/autoload/ezp_extension.phpandezp_override.php. The exclusion compared the extension's path as the root gives it (extension/foo, relative) with the files, which are listed under their real path, so it never matched. A test bootstrap that defines stand-ins foreZDB,eZINIoreZSysthen became a kernel override, and withEZP_AUTOLOAD_ALLOW_KERNEL_OVERRIDEthe site loaded those stand-ins.eZAutoloadGeneratornow anchors the exclusion at the real path of the extension, as it already did for--excludeand.autoloadignore. Runphp bin/php/ezpgenerateautoloads.php -e(and-owhere kernel overrides are allowed) once after the update. Test:eZAutoloadGeneratorTest.
Updated
- The exclusion names the directory itself:
tests/of an extension followed by the separator or the end of the path, sotests_old/,testsuite/or a filetests.phpstay in the arrays. An extension whose path does not resolve is skipped (it lists no files anyway) instead of getting an expression built from the path as given;--excludeand.autoloadignorequote that fallback too. Test:eZAutoloadGeneratorTest. tests/andTests/of a Composer package vendored inside an extension (extension/<name>/vendor/<vendor>/<package>/tests) are left out ofezp_extension.phpandezp_override.phpas well: test cases and fixtures of that package, never runtime classes, and not in the package's own Composer autoloader either. Test:eZAutoloadGeneratorTest. On one installation the extension array went from 2481 to 2272 entries: 154 classes of extensions'tests/and 55 of a vendored package's tests left it, nothing else changed.- Test classes load through a tests autoload instead of the runtime arrays.
ezpAutoloader::enableTests()adds the test arrayvar/autoload/ezp_tests.php(ezpgenerateautoloads.php -s), andezpAutoloader::enableTests( $dir )the classes declared under a tests directory, read there and then (eZAutoloadGenerator::testsClassMap()), so a class added a moment ago is found without generating anything. The kernel's PHPUnit bootstrap (tests/bootstrap.php, also used bytests/runtests.php) turns it on;ezexec.phpturns it on for a script that lies in atests/directory (php bin/php/ezexec.php extension/foo/tests/bar_test.phpfinds the classes ofextension/foo/tests); an extension's own PHPUnit bootstrap calls it with its tests directory. The site itself no longer loadsezp_tests.php;define( 'EZP_AUTOLOAD_TESTS', true );inconfig.phploads it everywhere as before. Tests:eZAutoloadGeneratorTest.
Operator note
Run php bin/php/ezpgenerateautoloads.php -e once after the update (and -o where config.php allows kernel
overrides with EZP_AUTOLOAD_ALLOW_KERNEL_OVERRIDE), then reload PHP-FPM and restart Velocity
(./console exp:velocity deploy does all of it). Check afterwards that grep -c "/tests/" var/autoload/ezp_extension.php
prints 0. Test scripts and suites of an extension that relied on their classes being in ezp_extension.php load
them through the tests autoload above; run ezpgenerateautoloads.php -s where the kernel's own tests are run.
12 October 2026: a user account that exists only as a draft
Fixed
- The account of a user object whose
ezuserrow is not stored yet, but whose edit left a draft (login, e-mail address, password hash) in the attribute, now shows that draft:eZUserType::objectAttributeContent()returns a new account with the draft applied instead ofnull, so the edit form keeps what was entered and the object has a title. Without a draft it staysnullas before. Storing the first version of such an account and publishing its draft create theezuser_settingrow that a new account needs to sign in, enabled, unless it has one already (a disabled one stays disabled). Test:eZUserDatatypeDraftTest(no database).
Updated
- The setting row of a new account follows one decision wherever it is made (the default account of a user object
without one, the first version of an account, a published draft): an existing
ezuser_settingrow is never changed, so an account an administrator disabled, or one waiting for its activation, stays disabled. The default account used to store a new, enabled setting row over an existing one each time an object without anezuserrow was stored. A new row is enabled, as before, and disabled when the account has an account key (ezuser_accountkey, waiting for activation). Activation itself is unchanged:user/registerdisables the row and stores the key ([UserSettings]VerifyUserType),user/activateenables it. - A draft becomes a new account only with a login, and only when no other account has that login (or its e-mail
address, where
RequireUniqueEmail=trueandAuthenticateMatchincludesemail), the checks of the edit form. The draft may be older than the account that took them, or not come from the form at all: a copy of another user's object carried that user's draft (the published attribute keeps one afteruser/password), and storing the copy made a second account with the same login and password, able to sign in. Such a draft is not applied: the object keeps no account, the draft is cleared and the reason is logged as an error. - A copy of a user object into a new object (
content/copy) leaves the original's draft behind (eZUserType::initializeObjectAttribute()); a new version or translation of the same object keeps it. - What counts as a draft is narrower: the four fields as strings, numbers or null (not
true/false), a hash type thateZUserknows (PASSWORD_HASH_*), and a plaintext "hash" only on a site withHashType=plaintext, where on any other site it would set a password nobody chose. Anything else indata_text(invalid JSON, a list, a serialized PHP array, a login of blanks) makes no account and writes nothing; publishing it created a setting row for an object without account. The draft holds the password hash, never the password. - Telling a new account from a stored one when the first version is stored keeps its lookup of the
ezuserrow by its primary key: theeZUserin hand does not say whether it was read or made, and the first version is stored only while the account is created. The login (and e-mail) check and the account key lookup run only for a new account. Tests:eZUserDatatypeDraftSecurityTest(no database),eZUserDatatypeDraftTest.
Operator note
Nothing to run. User objects without an ezuser row, or accounts without an ezuser_setting row, are left as they
are: an account without a setting row cannot sign in (the sign-in fails closed) and user/setting cannot change it
either, so check for such accounts after importing users by SQL and give them a setting row on purpose. Setting
rows that belong to no object (left by an import or a removal outside eZUser::removeUser()) grant nothing; a new
user object whose id one of them has keeps that row as it is, where the default account used to enable it.
12 October 2026: faster Setup > Extensions, Info > About and audit dashboard
Updated
eZExtension::extensionInfo()no longer walks an extension's whole tree on every call to find its newest file ("Modified" on Setup > Extensions). The walk stats every file (52 000 files for 85 extensions here, close to a second) and Setup > Extensions, Info > About, the Online Editor dialogs and the update inventory asked it on every view.eZExtension::newestFileTime()keeps the result in<cache>/exp/extension-file-times.jsonfor an hour and walks again before that when the active extensions cache is cleared (ezcache --clear-tag=ini,--clear-all,exp:velocity deploy, a change on Setup > Extensions); the extension's ownextension.xml,ezinfo.phpandcomposer.jsonare looked at every time. Setup > Extensions went from 1.0 s to 0.3 s, Info > About from 0.6 s to 0.1 s. Test:eZExtensionNewestFileTimeTest.- The audit dashboard sums the records per channel of today and of the week from the per-day counts its chart
reads, instead of two groupings that read the whole index table on SQLite (half a second each with 240 000
records).
expAuditQueryfinds a family of event names (access.*,system.expai.request.*) on SQLite through a range of the name index together with theLIKE, which SQLite cannot plan with that index: the AI panel's counts went from 280 ms each to 2 ms. Other engines keep theLIKE. A dashboard built anew (its figures are kept a minute) takes about 1.5 s instead of 4 to 8 s. Test:expAuditDashboardVolumeTest.
12 October 2026: one count of extensions on the dashboard, Info > About and the update dashboard
Fixed
- The administration showed three different numbers of extensions for one installation: the dashboard 61, Info >
About 63 and the update dashboard "85 Extensions, 64 active", each counting something else under the same word.
The dashboard counted
ActiveExtensionsonly; About added the siteaccess's ownActiveAccessExtensions(here two extensions, switched on for the admin and editor siteaccesses only); the update dashboard counted every directory inextension/and called active what any siteaccess switches on (addingexp_adminui, active in theadminuisiteaccess only). All three now take their figures fromeZExtension::extensionCounts()and say what they count: "63 active in this siteaccess, 85 on disk" on the dashboard and About, "85 Extensions on disk, 64 active in any siteaccess" on the update dashboard and its installed packages page.
Added
eZExtension::extensionLists()andeZExtension::extensionCounts(): the extensions of the installation by kind,on_disk(every directory of every extension root,ExtensionDirectoryandAdditionalExtensionDirectories, each name once),global(ActiveExtensions),here(active in the current siteaccess, in loading order),anywhere(active in at least one siteaccess ofAvailableSiteAccessList),inactiveandmissing(named in the settings, not on disk). Every active kind counts extensions found on disk only. Template fetch:fetch( 'setup', 'extension_counts', hash( 'all_siteaccesses', true() ) ); withoutall_siteaccessesthe kinds that read every siteaccess's settings (anywhere,inactive) are left out, which keeps the dashboard at 0.5 ms for the count. Test:eZExtensionCountsTest.
Updated
- Info > About lists the extensions of
eZExtension::extensionLists()here, in the loading order the kernel uses (ExtensionOrdering), where it listedActiveExtensionsand thenActiveAccessExtensions. - The update dashboard's inventory reads every extension root, and an extension named in the settings but not on disk is listed under "Need attention" only, no longer counted as an extension or as active.
12 October 2026: Info > About and Copyright select the Setup tab
Fixed
ezinfo/aboutandezinfo/copyrighthad nodefault_navigation_part, so the administration fell back to the first navigation part and highlighted "Content structure" with the content tree as the left menu. Both views now nameezsetupnavigationpart: the Setup tab is selected and the Setup menu is shown, as on the other pages about the installation.ezinfo/is_aliveanswers with plain text and is unchanged.
12 October 2026: Exponential Intelligence in the distribution
Added
composer.jsonrequiresse7enxweb/expai~0.0.1.0: Exponential Intelligence, the writing assistant of the administration interface (feature page, its own documentation in the expai repository). Composer puts it inextension/expai; it stays inactive until it is activated for the admin siteaccesses, its tables are created and a provider is configured. It writes drafts as the signed-in editor, with that editor's permissions, and never publishes on its own.- The job queue
se7enxweb/expqueue~0.1.0(required since 11 October), which runs the queued work of Exponential Intelligence and of any extension that registers job types (feature page). - The kernel parts the assistant builds on are generic and documented on their own: extension sources of the audit
console (below), registries declared in
rad.ini(below), read services opted in with'ai' => true(11 October), install as drafts and the JSON package format (11 October), the eventpackage/install/drafts(below), and the marked version in the admin4 node header (below).
12 October 2026: Setup > Packages end to end
Added
- Validation of every upload before it is imported: each document well formed, the JSON documents against the
schema, the checksums of
package.json. A package that fails is refused with the list of what failed; the result is shown on the package's install and view pages until the next action on it. Validate on a package's page runs the same validation (a POST, then a redirect back) and shows what was checked or every problem. - Install in the background: the content job type
packageinstall(expContentJobPackageInstall,content.ini [ContentJobSettings] JobTypes[packageinstall]) installs a package with the wizard's choices as the requesting user. The install page offers it, and suggests it above the content jobs'SynchronousLimit; the package's page shows its progress object by object and lists the drafts when it is done. - The install page's report lists every object with what the install does with it and where its main location goes (at the top, below a node here, below a node of the package, or below a missing one), with the counts of each.
- The notify event
package/install/drafts(eZPackage::notifyDraftsInstalled()) after every install as drafts: the admin's,eZPackage::install()(ezpm) and a background install, so an extension can mark or record the drafts. For the command line and the background worker the listeners of this event are attached fromsite.ini [Event] Listeners[]. - The contents browser lists JSON documents by their real kind (Package definition, Content object, Content class)
and shows them as JSON or as the XML they mirror, and an XML document as JSON; long lines wrap, with a No wrap
switch. What a file is comes from its content: PNG, JPEG, GIF and WebP images are shown inline, SVG only as text,
any other binary as a card with its type, size, SHA-256 and whether it matches
package.json.package/viewfilesends only such images inline, everything else as an attachment. - New package asks in its information step which format the package is kept in (Classic XML or JSON); a JSON
package is converted in place when the wizard finishes (
expPackageJson::convertInPlace()).
Updated
- The content handler reads the remote id of a per-object file of any length (only an md5's 32 characters were read, so objects with readable remote ids missed their post-install step) and reports its progress to an install that asks for it.
- The documents of a package (its license) open in the package's contents browser
(
package/view/full/<name>/<repository>/(file)/<index>) instead of a link into the storage, which the web server answered with 404. Guides: Packages, The JSON package format. Test:eZPackageFileBrowserJsonTest.
12 October 2026: the audit console's extension sources, a streamed and background export, repeated records
Added
- Sources of extensions: an extension that records its own audit events registers a source in
audit.ini [AuditSourceSettings] Sources[<identifier>]with a class implementingexpAuditSource(best extendingexpAuditSourceBase). The dashboard shows a panel per readable source (cached a minute per user); console, recent, charts, alerts and export show a line of its links; the charts draw its series per day; an event of its channel shows the source's fields and links; alerts name the source of a rule; Setup of the audit lists the registered sources and what keeps one from working; theChannellimitation ofaudit/readnames an extension's channel after its source. Without a registered source every view is as before. Exponential Intelligence (channelai) is the reference source. - A source declares its own CSV columns (
expAuditSource::exportColumns()), added after the generic ones to the CSV of its channel inaudit/exportandexp:audit export --channel=<channel> --format=csv. - The audit export runs as the content job
auditexport(expContentJobAuditExport, "Export in the background", any of the three formats), up to[AuditConsoleSettings] MaxBackgroundExportRecords(1000000). The export page lists the user's background exports with their state and progress and offers the finished file ataudit/export/(download)/<id>, only to the user who asked for it, recorded assystem.audit.readwith the job. fetch( 'audit', 'hidden_menu_links' ): the audit left menu leaves out a link into a channel the user may not read (an audit view filtered on the channel, or a link thatmenu.ininames withAuditChannel_<key>=<channel>).
Updated
- The download of
audit/exportis streamed (expAuditExportStream): the file starts at once and is sent while it is read, also from a Velocity pool worker (X-Accel-Buffering: no). A large filter is read in order by id in segments instead of OFFSET pages: the content channel's CSV (26,889 records) took 38.3 s on Apache and was a 504 on Velocity; it now starts within about a second and ends in 3.3 to 4.1 s, with the same bytes. - A process forked with
pcntl_fork()no longer writes its parent's buffered audit records a second time: the child drops its copy on its first event, flush or reset and takes a request id of its own, and keeps the job, the run and the parent stack. Before, the same records reached the channel once per child, each copy with a newseqand hash. exp:audit verifyreports lines that repeat a record id as arepeated_idsnotice per file (not a break) and prints the distinct count next to the record count;exp:audit exportleaves a repeated id out, as the index does.- A test run writes its audit records into a directory of its own under
var/tmp(expAuditConfig::setTestLogDir(), set bytests/bootstrap.php), never into the site's live audit trail. - The alerts of the audit dashboard break rule names only at their underscores and say "1 address" and "1 problem"
for one.
Guide: The audit guide (extension sources, streaming the export,
repeated records). Tests:
expAuditSourceTest,expAuditExportStreamTest,expAuditForkTest.
12 October 2026: registries declared in rad.ini
Added
- setup/rad counts the registries that the kernel and extensions declare in
rad.ini:[RegistrySettings] Registries[]and a[Registry_<key>]block naming the INI blocks (Ini,Section,Entries[<variable>]=<check>) or the class list (List,Implements). The survey counts every entry, checks it (a class or interface, callable, listener, template,section:,in:,one-of:, name) and shows a failing entry or declaration as broken;InTotal=disabledlists a registry without adding it to the total. Only active extensions are read.settings/rad.inideclares the kernel's event listeners, cronjob part scripts and audit console sources. - The survey counts every policy limitation of every module, with its class, and its event sweep finds events
announced on a variable or property holding
ezpEvent(package/install/draftsand others); the catalogue has the mechanisms registry and filter.
Updated
- ezjscore (server functions, key
ezjscserver) and ezoe (editor engines, keyezoeengines) declare their registries in their ownsettings/rad.ini.append.php; the survey's code no longer names them. - A later file's
Registries[](orSections[]in a registry's block) without a value empties the list, as it does in the settings. rad.inideclarations are no longer counted as settings. Guides: The RAD extension surface, RAD extension points. Tests:RadSurveyDeclaredRegistriesTest,RadSurveyRegistriesTest.
12 October 2026: Setup > System information under Velocity
Updated
- Setup > System information shows the sizes of one shared measurement:
var/, each cache directory and a database on a server are measured in one walk (within ten seconds) and kept for ten minutes invar/<site>/cache/exp/system-report-sizes-<key>.json, shared by PHP-FPM and Velocity. Measure again measures anew (posted with the form token), and the note under the buttons says when the sizes were measured. The page took about 2 s under Apache and 4 to 7 s under Velocity on every view. Each part of the report is a debug accumulator. - The Velocity box reads the process table once and sends its own server no health check request
(
expVelocity::status( false ),expVelocity::reuseProcessTable()); it took 0.4 to 0.7 s of every view. - The page lists a closure autoloader by its file and line: under Velocity
spl_autoload_functions()includes a closure, which the template could not print, so the page answered 500 on the Velocity port. Guide: System information. Test:expSystemReportTest.
12 October 2026: smaller fixes in the administration
Updated
- The PDF export's table operator counts its parameters (
count( $operatorParameters ) > 2): it counted a boolean, which PHP 8 refuses with aTypeError, so every PDF export with a table answered 500. Guide: PDF export. user/preferences/setopened directly, from a bookmark or from another site (Sec-Fetch-Sitenoneorcross-site) shows a confirmation (user/preferences_confirm.tplin design standard, admin and admin4) whose form posts with the form token, then stores the preference and returns to the page the address named. Before, it stored nothing and redirected to the front page. A click on the site's own link stores it at once, as before.- The show/hide tabs button of the content view shows the icon of the state the tabs are in (admin, admin3, admin4); the two images were swapped.
- The version in the header of a node (admin4) is marked (
span.exp-object-versionwithdata-object-id, andspan.exp-object-version-number), so a page script can show a newly published version without a reload. - The extension file time cache sets its file mode through
eZFile::fileMode()alone, so the site's mode limits apply. - The editor siteaccess the setup makes keeps the node view tabs of its access extensions: its
AdditionalTabs[]list (roles, policies, eztags, authors) is followed by the tabs each extension of the admin'sActiveAccessExtensions[]declares in its ownsettings/admininterface.ini.append.php(eZStepCreateSites::editorAdditionalTabs()). The editor's list starts empty and an access extension's settings are read before the siteaccess's, so expai was switched on there without its Intelligence tabs. Test:EditorSiteaccessTabsTest. - An installation without a package language map (
exp:install, the kickstarter) falls back to the language of site package content that is in none of the chosen languages: the language is added after the clean data's toSiteLanguageList(no siteaccess is made for it) and exists before the import, so the import and the site package's steps find that content (eZStepCreateSites::contentFallbackLanguages(), reading every object of the content packages witheZContentObjectPackageHandler::objectLanguageLists()). The CJW Multisite starter content is in ger-DE with English translations of a fifth of it; installed with eng-US alone, the install logged 125 errors ("pathWithNames() failed to fetch path" for every image, the starter's hidden folder and privacy page "not installed") and the site could not show that content. Test:SetupContentFallbackLanguagesTest. - The response cache clear that switching maintenance on starts (
exp:velocity cache clear, a process of its own) carries the setup run's id when a setup run starts it (EXP_SETUP_RUN), and its audit entry looks no user up then (expAudit::canLookUp()): it ran with the settings of before the installation, and every installation logged four failed connections to the database those name. Test:SetupRunCommandAuditTest. - A file attribute of a type without a text extraction handler (a video, an image, an archive) is indexed by its name alone with a notice, not a warning ("Mimetype video/mp4 not supported for indexing" on every publish of the multisite demo content's videos); a handler that is configured but missing still warns.
12 October 2026: the main branch resolves as 6.0.x-dev
Updated
composer.jsoncarries the branch alias"dev-main": "6.0.x-dev". Installed frommain, the root package wasdev-main, which satisfies no version constraint, so a package that requiresse7enxweb/exponential^6.0.14(expqueue 0.1.0 does) could not be resolved and the dependency installs of the test and quality workflows stopped. With the alias the constraint accepts the main branch. Guide: Getting the code.
12 October 2026: tests read their servers from the environment
Added
expAuditConfig::siteLogDir()names the directory the site's own requests write their audit records into ([AuditSettings] LogDir), also while a test run writes into a directory of its own. Test:eZSiteLogReadersTest.
Updated
- The MongoDB integration tests (
expMongoDBIntegrationTest,sevenxMongoDBIntegrationTest) read their servers from the environment and skip when they are not set:EXP_TEST_MONGO_URIandEXP_TEST_MONGO_DATABASE(defaultexp), andEXP_TEST_MYSQL_USER,EXP_TEST_MYSQL_PASSWORD,EXP_TEST_MYSQL_DATABASE,EXP_TEST_MYSQL_HOST(defaultlocalhost) andEXP_TEST_MYSQL_PORT(default 3306). They carried an account name and password of their own before; an installation that used those values for a real account should change that account's password.bin/mongodb/import_all.shneedsMONGO_URIset, it has no default connection string any more. ApiKeyLiveTesttakes the administrator's password only fromAPIKEY_LIVE_ADMIN_PASSWORDand skips the administrator's revocation (AL-06) without it.- The live API key tests (
ApiKeyLiveTest,RestContentPermissionLiveTest) count the refused uses the web servers recorded in the site's audit directory again (expAuditConfig::siteLogDir()). Since a test run writes its own records into a directory undervar/tmp, they read that directory instead and found none ("one scope refusal per server, 0 is identical to 2"); the servers had recorded every refusal. Guide: Tests that need a live server.
10 to 12 October 2026: Exponential Layouts
Updated
- explayouts 1.4.22, explayouts_core 1.0.7 and explayouts_ui_api 1.3.15 (required since 10 October): Copy in the legacy layout list and Duplicate block in the layout editor copy a block with its collection items, its dynamic query with parameters, offset and limit, its item view type and, for a container, its child blocks with their own collections.
- explayouts 1.4.23 and explayouts_ui 1.3.14 (required since 12 October): the layouts lists (dashboard, layout list,
shared layouts) show the minutes of the modification time,
2026-10-04 01:17instead of2026-10-04 01:Oct(the date format used%M, the abbreviated month, where the minutes are%i). - explayouts 1.4.24 (required since 12 October): the legacy layouts administration pages select the Layouts tab
instead of Setup, the pages that render in the administration layout (Dashboard, Setup, Layouts, Edit Layout, Edit
Block, Layout Rules, Edit Rule) show the Layouts menu in the left column, and their breadcrumb starts at "Layouts",
linked to the layouts dashboard ("Layouts / Dashboard", "Layouts / Edit Block"). The template editor renders its
page through the template engine (it listed nothing before), reads and writes only
.tplfiles inside[TemplateEditorSettings] AllowedTemplateRoots[], also after symbolic links are resolved (earlier versions accepted any file name underextension/: update from 1.4.23 or earlier), loads and saves through the current siteaccess, and no longer writes a debug log on every failed read. - On the main branches of explayouts_ui and explayouts_ui_api, after their releases 1.3.14 and 1.3.15, so in their next ones: the same breadcrumb starting at "Layouts" and the Layouts menu on Setup and Edit Block of the Layouts UI pages, and the layouts editor and its JSON views name the Layouts navigation part. Guide: Exponential Layouts.
12 October 2026: uncaught errors under Velocity, the rich text editor's upload form
Updated
- An uncaught exception or error under Exponential Velocity gets the site's error page (error.ini
StaticErrorPage[500]) with its reference, and the line inerror.log, as under PHP-FPM.eZExecution::registerShutdownHandler()installsdefaultExceptionHandleragain whenever no exception handler is in place (eZExecution::currentExceptionHandler()): a persistent worker takes away the handler a request installed once the request has ended, while the class's statics keep the flag that it was installed, so from a worker's second request on such an error got the server's own error page and was written only to Velocity's console log. A handler other code installed is kept. Test:eZExecutionExceptionHandlerTest. - The upload view of the rich text editor reads its text fields (
uploadUrl,location,objectName,ContentObjectAttribute_*) throughUpload::postString(): a field sent as a list (name[]) counts as empty, where it was aTypeErrorand HTTP 500. Test:expOEUploadPostStringTest. - Upgrade note: a changed kernel class reaches Velocity only with a restart (
exp:velocity deploy); until then its workers keep the classes they had loaded. A real PNG uploaded from the editor's dialog answered 500 on the Velocity port while the search index's new integer range check (eZDBInterface::clampInteger()) was on disk and the workers still had the database classes from before it; Apache, which loads every class again on each request, was not affected. The upload works on both servers after the deploy.
12 October 2026: clean-up handlers and shutdown functions for each request under Velocity
Updated
- Under Exponential Velocity's persistent workers every request gets its own clean-up and fatal error handlers and
its own shutdown functions (
eZExecution's clean-up handler andeZSession::stop()), as under PHP-FPM. A worker puts the statics back to what they were after its warm-up before each request, and the flag that the shutdown functions were registered was among them: from a worker's second request on none was registered (no clean-up and no error page after a fatal error), and every request ran the warm-up's handlers beside its own. The handlers, the clean exit flag and the shutdown functions now belong to one request (eZExecution::requestKey(), the request'sREQUEST_TIME_FLOAT); a handler added twice in one request runs once; a shutdown function left by an earlier request (the warm-up's) does nothing (eZExecution::runForRequest()). The cluster file handler's cleanup handler, the SQL profile, the query cache's counters, the log context's repeat counts and the audit's handlers are registered per request the same way. Nothing changes under Apache/PHP-FPM or on the command line. Test:eZExecutionPerRequestRegistrationTest. Guide: Velocity. - Upgrade note, for developers: a registration that must happen once per request is guarded with
eZExecution::requestKey(), not with atrueflag, which a worker keeps from its warm-up. - Exponential Velocity 0.0.4.51 ends the warm-up's request itself before it forks its workers, for every application
on the engine, not only this kernel: the shutdown functions the warm-up's render registered are dropped without
running, and its headers, error and exception handlers, session, ini and environment changes and open output
buffers are put back. A worker's first request (with ForkPerRequest, every request) used to run the warm-up's
shutdown functions beside its own;
eZExecution::runForRequest()stays as the kernel's own guard. A request's$_SESSIONnow also ends with the request, as under PHP-FPM.
13 October 2026: the location in the version preview
Fixed
- The version preview (
content/versionview) of a version with more than one location marks the location it shows: the templates of every design read$placement, which the view did not hand over, so the error log had "Unknown template variable 'placement'" for each preview and no location was checked.kernel/content/versionviewframe.phpsets it from the view's placement. Test:eZContentVersionviewPlacementTest.
13 October 2026: links to content of another site of a multisite installation
Fixed
- A page of one site that lists content of another site of the same installation links to that content on the site
that serves it. On the Bold Agency tag pages (
/bold/tags/view/Topics/HIITand seven more) every Fit & Healthy article linked to/bold/fit-healthy/..., which answered 404 (kernel (20), module not found); it now links to/video/...(or/site/video/...where Fit & Healthy is on/site). Links within a site, module links and every link of an administration siteaccess are unchanged.
Added
- The extension
ezurlmultisite(se7enxweb/ezurlmultisite 1.1.1) is required and activated by the multisite installers (sevenx_multisite,sevenx_multisite_clean,cjw_multisite). Their new post-install steppostInstallWriteMultisiteURLSettingswritessettings/override/ezurlmultisite.ini.append.php([MultisiteSettings] SiteAccessList[],RootNodeList[]) from the PathPrefix,RootNodeandIndexPagethe install wrote, so the node ids are those of the new installation. - The media theme's own link operators (
exp_path,exp_url,content_link...) follow it (sevenx-themes-media 1.5.55). - Links to content of another site of a multisite installation.
Operator note
- An existing multisite installation adds
ActiveExtensions[]=ezurlmultisiteand its ownsettings/override/ezurlmultisite.ini.append.php(the page above), regenerates the extension autoloads and clears the INI, template and content caches. Without the settings file the public siteaccesses with a PathPrefix take part, with the root node of theirRootNodeorIndexPage.
14 October 2026: the query cache on Oracle
Fixed
- On Oracle the query cache never hit across requests: ezoracle sets
NLS_NUMERIC_CHARACTERS,NLS_LENGTH_SEMANTICSandCURSOR_SHARINGwith oneALTER SESSIONon every connect, whicheZDBQueryCache::noteWrite()counted as a write of unknown tables, so every request made every cached result stale. AnALTER SESSION SETof only session parameters that change a result by their value alone (NLS_*,CURSOR_SHARING,TIME_ZONE) is no write now (eZDBQueryCache::isNeutralAlterSession()); any otherALTER SESSION(CURRENT_SCHEMA,EDITION,CONTAINER,EVENTS, ...) still makes everything stale. Measured on Oracle 11g XE with ezoracle 2.3.6: from 2 hits of 12 lookups per request to 10 of 12. Test:eZDBQueryCacheTestQC-16. - The parameters such a statement sets are part of the key of every statement of that connection
(
eZDBQueryCache::sessionSettings(),sessionKey(), kept per driver object): a result is answered only to a session with the same settings. WithOracleCaseInsensitive=enabledthe driver addsNLS_COMPandNLS_SORT, and the cache now hits across requests there too; a session with anotherNLS_SORTis never answered a result sorted by the first. Tests: QC-19, QC-20. The key of every statement changes once with this release, so results cached before it are not answered again (they expire afterMaxAge).
14 October 2026: the query cache in the debug output
Added
- With the debug output on, the query cache shows what it did for the request: the accumulators of the group
Query cache (hits, misses, stores, statements not cached), a timing point where its state is read, and a
Query cache report at the bottom with the request's counters, the hit rate, the database time the hits saved
(each entry now records how long its statement took when stored), why statements were not cached and the writes
that made everything stale (string literals masked). Switch:
querycache.ini [DebugSettings] Report(defaultenabled); with the debug output off nothing is measured. Described in the query cache page. Tests:eZDBQueryCacheTestQC-17, QC-18. - The literal masking (
eZDBQueryCache::maskLiterals()) reads a literal both as SQL-standard quoting ('') and as MySQL's backslash escapes (\'), and masks an unterminated one to the end, so no part of a literal reaches the report under either reading. Test: QC-21. - After
eZDebug::reset()in the same process (a worker between two jobs) the report starts again with its own counters, and the timing point comes again, at once when the process already holds the state. The report's counters are its own, so Setup > System information flushing the request's counters leaves them as they are; with the debug output switched off before a reset nothing is registered. Test: QC-22. - With the debug output off the cache tests per statement only whether it is on, without a call (measured within the noise of a lookup hit).
14 October 2026: Oracle 11g and 12.1 (ezoracle 2.3.7)
Fixed
- URL aliases resolve again on Oracle before 12.1 (11g): since ezoracle 2.3.1 every lookup by
text_md5(eZURLAliasML,eZURLAliasQuery) failed with ORA-00904, asSTANDARD_HASH()only exists from 12.1 on. Before 12.1 the driver hashes the string literals the kernel passes in PHP, which is the valuetext_md5holds; from 12.1 on nothing changes (se7enxweb/ezoracle pull request #2). - The DFS cluster backend for Oracle lists its files with a limit before 12.1 too (ROWNUM instead of
OFFSET/FETCH), and the driver reads the server version again after a reconnect.
Updated
composer.jsonrequiresse7enxweb/ezoracle~2.3.7.
14 October 2026: the query cache in the Exp Debug bar
Added
- The Exp Debug bar shows the SQL query cache at a glance: a chip in the pinned bar with the request's hit rate and
the database time it saved (
QC 83 % · -186 ms), amber below 50 % or when a write made everything stale, red below 10 % or insharedmode without APCu (debugbar.ini [DebugBarSettings] Thresholds[querycache_hit_rate]). The cache's report and accumulators stand in the SQL tab. The Cache tab shows what the server holds: APCu entries and memory, the counters since the last reset, the state, the cached results grouped by the tables they read (entries, rows, size, hits) and the largest entries, with buttons to clear the cache and reset the counters (expdebugbar::cache by=querycache_counters). New:eZDBQueryCache::debugSnapshot(),inventory(),expDebugBarSummary::queryCacheLevel(). Tests:eZDBQueryCacheTestQC-23, QC-24,expDebugBarQueryCacheTest. - Refined on merging: the level of the chip and the figures of the summary have one source,
expDebugBarReport::queryCacheLevel()andqueryCacheFigures(), whichexpDebugBarSummaryuses too (the bar's own fallback had a copy of the rules).debugSnapshot()gives the figures of the report's own counters and reads no state, so the chip and the report agree aftereZDBQueryCache::flushStats()and aftereZDebug::reset().inventory()leaves out the state file's path itself. In the Cache tab the counters button reads "Reset", the section is read again after a clear or a reset, and the query cache is no longer listed a second time under "By ID". Tests:eZDBQueryCacheTestQC-23, QC-24 extended,expDebugBarQueryCacheTestDQ-05. - The query cache tables of the Cache tab have a header row and their figures right aligned, from classes of
expdebugbar.csswith its light and dark tokens (no inline style); checked in a browser on Apache and Velocity at 1440 px and at 960 px with scale 2, light and dark.
14 October 2026: the query cache in files
Added
sharedmode keeps its results in files where there is no APCu:querycache.ini [QueryCacheSettings] Storage,auto(default: APCu where the PHP process has it, else files),apcu(only APCu, without itsharedworks asrequestas before) orfile(always files, for web servers that sharevar/on NFS or a SAN). Upgrade note: a web server withMode=sharedand without APCu now writes files instead of keeping only the request's memo; schedule the cronjob partcache_cleanupor setStorage=apcufor the old behaviour. Cronjobs and scripts without APCu keep the memo only underauto. One file per result invar/<site>/cache/querycache/entries/<2>/<key>, read withallowed_classesoff, written to a temporary file and renamed; age and state are checked as for APCu. New cronjobquerycache_cleanup.phpin the partcache_cleanupremoves the files older thanMaxAgeor from before the last clear; a clear removes them at once. Setup > System information, the debug report, the Exp Debug bar's chip and Cache tab show the storage; the chip is red only whensharedhas no storage. New:eZDBQueryCache::storage(),entriesDir(),cleanupFiles(). Described in the query cache page. Tests:eZDBQueryCacheTestQC-25 to QC-28,expDebugBarQueryCacheTest.- Refined on merging:
Storage=autouses the files in the workers of Exponential Velocity too (their SAPI iscli, so they had kept only the memo); a result file is only ever named by a key (hex digits only); without the cronjob the requests sweep the files themselves, at most every 10 minutes (orMaxAge) for the installation, one process at a time and for at most half a second (eZDBQueryCache::sweepFiles()), so they cannot grow without bounds; the Exp Debug bar never receives the path of the result files. Tests: QC-29 to QC-31.
14 October 2026: an APCu reserve for the query cache
Added
- The query cache leaves room in APCu for the PHP-FPM pool's other users:
querycache.ini [QueryCacheSettings] ApcuReserve(percent, default 25,0off). With that share or less of APCu free,sharedmode stops adding results to APCu and keeps them for the request only. APCu full with valid entries empties itself completely, and the HTTP cache, API keys or a request filter's counters in the same pool went with it. The free share is read fromapcu_sma_info()before every store. Since APCu frees expired entries only when they are deleted by key or an allocation fails, below the reserve a miss deletes its key's old entry andeZDBQueryCache::purgeExpired()deletes the cache's expired entries once a minute for the pool (up to 30,000 APCu entries and below 80 % ofmemory_limit; in a web request after the page). Setup > System information and the Exp Debug bar's Cache tab count the results held back (counterreserve;eZDBQueryCache::$statshas the new keyreserve), the debug report lists them. New:eZDBQueryCache::apcuRoom(),purgeExpired(). Tests:eZDBQueryCacheTestQC-32 to QC-34. - Refined on merging: in a worker of Exponential Velocity the purge runs at the end of the request, registered once
per request, instead of while the visitor waits (Velocity's SAPI is
cli); a script still runs it at once. The APCu reserve warning of Setup > System information uses the shared admin components (a4-notice is-warn): its old class was no longer styled.apcu_sma_info()measured at 0.2 µs a store. Tests: QC-35,AdminRetiredClassesTest. Setup > System information and the Cache tab of the Exp Debug bar show the settingStoragenext to the storage it gives, andApcuReserve(status()hasstorage_setting).
14 October 2026: a resizable Exp Debug panel
Added
- The open panel of the Exp Debug bar can be resized: a handle on its top edge is dragged with the mouse, a pen or a
finger, or focused (
role="separator"witharia-valuenow,aria-valuemin,aria-valuemax) and moved with the arrow keys (16 px, with Shift 64 px; Home and End for the limits); a double-click gives the default height (60 % of the window) back. The height stays between 120 px and the window's height minus the bar, also when the window changes size. It is saved once at the end of a drag or 0.6 s after the last key, in the user preferenceexpdebugbar_panel_height(new server functionexpdebugbar::panelheight, POST with the form token), and the report prints it on the panel asdata-exp-debug-panel-height, so the panel opens at that height in any browser of the user. An anonymous visitor (the bar shown by IP) keeps the height in the browser'slocalStorage(exp-debug-panel-height, read back and clamped before the panel opens): no preference is read or saved for one and no request is sent, so the bar never starts a session; a logged-in user's preference is authoritative. New:expDebugBarReport::panelHeight(),savedPanelHeight(), three English and German texts. Guide: The panel's height. Tests:expDebugBarReportPanelHeightTest,expDebugBarReportCopyMessagesTest.
Updated
- The page cache (
exphttpcache) no longer keeps a page that carries the debug report of its request (X-Exp-Cache: BYPASS (debug report)). A signed-in user's front page was answered from it with the report of an earlier request, so the resized debug panel opened at the height from before the last resize after a reload, the timings and queries were those of the stored request, and a report shown by IP could reach every visitor sharing the anonymous context. Test:ezpHttpCacheContractTestHC-23. - The debug bar's Velocity tab says where the request entered and which process rendered the report, in place of
"Not running on Velocity.": "Received by Velocity on port 8080 and rendered by a Velocity worker." or "Served by
Apache / PHP-FPM (port 443), not by Velocity.", also for the Velocity parent, php-cgi started by Velocity, mod_php
and FrankenPHP, with the rows Entered (port and forwarded headers), Received by, Rendered by, Rendered at and
Cached. A report that still comes out of a cache is marked as such, with when and by which process it was rendered.
On :8080 the tab had shown PHP-FPM: the signed-in front page came from the role-aware page cache, stored by PHP-FPM
for a request on 443 and answered by Velocity from the same store. The OPcache box says when
opcache_get_status()is switched off. English and German texts. Guide: Velocity: where the request entered. Test:expDebugBarReportEngineTest.
Fixed
- A page with the debug report is sent
Cache-Control: private, no-store: Velocity's response cache had kept an anonymous page with the report of a request from an address inDebugIPListand answered every visitor of the Velocity port with it.
14 October 2026: the query cache's saved time against the page's time
Added
- The query cache chip of the Exp Debug bar also gives the database time saved in percent of the page's time
(
QC 83 % · -186 ms (186 %)); its title says how long the page would have taken without the cache ("without the cache about 286 ms instead of 100 ms"). The debug report's row Database time saved (estimated) gives the same share of the request so far. A share above 100 % means the hits saved more than the page now takes. New:eZDBQueryCache::savedShare(),debugSnapshot()hasrequest_msandsaved_share. Translated in ger-DE and eng-US. Tests:expDebugBarQueryCacheTestDQ-02, DQ-06,eZDBQueryCacheTestQC-17, QC-23. - Refined on merging: the report row and the chip measure the same span (
eZDBQueryCache::requestMs()): from the script start the web kernel sets for every request, also in a persistent worker of Exponential Velocity (elseREQUEST_TIME_FLOAT), to where the bar's output starts (setRequestEnd()); a start that would give a negative time or more than a day gives no share. From 100 % on both also say how many times as long the page would have taken (savedFactor(): "286 ms, 3 times as long"), so "186 %" no longer reads like a part of the page. Translated in ger-DE and eng-US. Tests: QC-36, QC-37, DQ-07.
14 October 2026: Bootstrap Icons in admin4
Added
- The icon theme
bootstrap-icons(share/icons/bootstrap-icons/): Bootstrap Icons 1.13.1 as SVG, unchanged, with their MIT licence, forclass_icon,mimetype_icon,classgroup_icon,iconand the content tree'sicon_info. It maps every class and MIME type ofcrystal-admin, the classes of the website interface and demo packages and the OOXML, EPUB, RTF, CSV, XML, JSON and SVG types; one file serves every size. An admin siteaccess switches to it in itsicon.ini(Theme=bootstrap-icons,AdditionalThemeList[]=crystal-admin); nothing changes until then. A class the theme does not map keeps itscrystal-adminPNG inclass_icon, but the content tree (icon_info, first theme only) shows the theme'sDefaultfor it: add it toClassMap. Guide: the admin4 design: icons. - The shared admin components
.a4-iconwith.a4-bi-<name>(a Bootstrap icon as a mask in the text colour, 1em or 12, 16, 22 px) and.a4-icon-btn(a link or button that is only an icon), in both copies ofcomponents.css. Guide: Styling the administration.
Updated
- admin4 draws its icons with Bootstrap Icons in the text colour of the light and the dark mode: the messages, the sorted columns, the tabs switch, the sub-items table (row menu, image preview, the buttons above it and their menus), the search box, the context menus and the content tree (a caret right and down on the column's background, so the connector line no longer crosses the symbol) in its stylesheets, in place of the GIF and PNG sprites; edit, invert selection, up one level, trash, move, the current user and the inline SVGs of buttons, badges and the header in its templates. The classic designs keep their images.
- The image buttons of class editing (top, up, down, bottom) and of the Locations window (move) in admin4 are
<button>s that post the same variables (MoveUp= the attribute id, aseZHTTPToolmade ofMoveUp_<id>_x). The invert selection images are buttons a keyboard reaches. - In dark mode a filter turns the
<img>icons of thebootstrap-iconstheme light. - Test:
AdminIconsTest(every file the theme, a class or a stylesheet names exists; no file is left over; every file is an unchanged Bootstrap icon; admin4's templates draw no GIF icon, inline SVG or image button).
14 October 2026: language codes instead of flags in admin4
Added
eZLocaleattributecontent_language_code(contentLanguageCode()): the locale's language tag from its locale file ([HTTP] ContentLanguage, e.g.de-DE), whichsite.ini [RegionalSettings] HTTPLocaledoes not override.- The include
design:parts/language_badge.tpland the shared admin components.a4-langand.a4-langsin both copies ofcomponents.css. Guide: the admin4 design: languages.
Updated
- admin4 shows a content language as a short code badge (
DE,EN; the language part only since 15 October) and its name instead of a flag: the node view's meta line, the sub-items table's Translations column, the Translations tab, the edit page and its language choice, version view, history, drafts, pending items, edit conflicts, URL aliases, states, classes and the top node. The badges have the language's name as title and, where it is not written next to them, as accessible name; the meta line marks the language shown witharia-currentinstead of dimming the others. The classic admin design shows the code in the sub-items table's Translations column too (its own copy of the include and ofezajaxsubitems_expdatatable.js, which stays identical to admin4's); its other templates and the other designs keep their flags. - Test:
AdminLanguageBadgeTest(no flag in an admin4 template or script, every badge through the include, the sub-items column escapes and names its badges, the component in both stylesheets, the include's optional parameters carry the prefixbadge_and every include passes only known parameters, no flag in either design's sub-items table).
Fixed
design:parts/language_badge.tplno longer reads a variable of the template that includes it: its optional parameters arebadge_name,badge_named,badge_plainandbadge_class. Calledclassandname, they read the content class ofclass/view.tplandclass/edit.tpl, andclass/viewended in a fatal error (HTTP 500).
14 October 2026: admin4 context menus from the tokens and the keyboard
Updated
- admin4's context menus (content tree, sub-items, class icon, bookmarks and their submenus) are drawn from the admin tokens: card, edge, radius and shadow of the light and dark mode, 14 px text in compact rows of about 27 px (at least 24 px, the WCAG 2.2 target size), a width that fits the longest item up to 22rem (longer items end in an ellipsis), the title as a quiet row instead of a fixed grey bar (4.39:1), the accent tint instead of a blue for the hovered and focused row, a forced-colors fallback. Before, the shadow was a hard light grey that glowed around the menu in dark mode. Guide: the admin4 design: context menus.
ezpopupmenu.js(admin4 and admin) gives the menus the roles of the WAI-ARIA menu pattern when it shows them and handles the keys: Enter, Space, Shift+F10 or the context menu key open a menu under its button with the focus on the first item; arrows, Home and End move; Arrow Right and Left open and close submenus; Escape closes and returns the focus; Tab closes. A menu opened with the mouse is reached with Arrow Down from its button. Its public functions are unchanged.- The content tree's menu button is a named
role="button"witharia-haspopup="menu"; the sub-items row button letsezpopupmenu.jsplace a menu opened from the keyboard. - A submenu opens beside its parent menu's right edge (the position used the submenu's own width).
- Test:
AdminContextMenuTest(no colour literal in the menu's styles, rows and focus outline, roles and keys in the script, the tree button's attributes, thedesign/admincopies identical, a keyboard click told by its click count and not by its position on the page).
Fixed
- On a scrolled page, a context menu opened with Enter, Space or Shift+F10 opened in the window's top left corner
and the focus stayed on the button:
ezpopupmenu.jstook the click for a mouse click because itspageX/pageYcarry the scroll offset. A click now counts as made with the keyboard by its click count (detail0).
14 October 2026: landmarks and names in admin4
Added
- admin4's frame has a skip link ("Skip to main content", the first stop of the Tab key, visible only while focused)
and landmarks: the header as banner, the top menu, side menu and breadcrumb as named navigation, the right column
as complementary, the footer as content info and
#maincontentas main, also on the edit-type pages that bring their own#maincontent; a page without the left column whose template brings none (remove, copy, move ...) is wrapped in#a4-main, a main landmark without styles. The roles are on the existing elements, so no selector changes. Component.a4-skip-linkin both copies ofcomponents.css. Guide: the admin4 design: landmarks and names. - English and German texts for the skip link, the landmark names, the menu button and the new control names.
Updated
- admin4: the breadcrumb and the top menu mark the current page (
aria-current); disabled top menu entries arearia-disabledand out of the Tab order; the logo, "Open the site" and the phone-width menu button have names, and the menu button answers Enter and Space and reportsaria-expanded; the section select of the Details tab, the sorting selects, the selection checkboxes and main language and location radios of the Translations and Locations tabs (with the row's language or location), the embed code fields of the relations list, the year, month and day fields of the date attribute and the disabled header search have names; the version preview frame has a title; the duplicate image link of the current user is hidden from screen readers and the Tab key. - Every header cell of admin4's templates has
scope="col". - Test:
AdminLandmarksTest.
14 October 2026: keyboard use of admin4's tabs, content tree and date fields
Updated
- admin4's node tabs follow the WAI-ARIA tabs pattern (tablist, tab with
aria-selectedandaria-controls, tabpanel): only the selected tab is in the Tab order (the first one when none is selected), the arrow keys, Home and End move between the tabs and open them. A click on an element inside a tab's link opens that tab (the handler took the link's parent of the clicked element). Guide: the admin4 design: keyboard. - The show/hide tabs toggle is a button named "Show tabs" whose
aria-pressedsays whether the tabs are shown; its title says what a press does; Space presses it. - The content tree's open/close link is a button named "Items below <node>" with
aria-expanded(aria-busywhile loading) that keeps the focus after a press; Space presses it. In the admin and admin4 designs (same script). - The calendar of the date and date/time fields of admin4 opens from a button the keyboard reaches (the classic designs keep the image).
- English and German texts for the tree button, the calendar button and the tabs toggle. Test:
AdminKeyboardTest.
14 October 2026: the content tree is a tree for screen readers and keyboards
Updated
- The content tree of the admin and admin4 designs follows the WAI-ARIA tree view pattern: a
treenamed "Content tree", each node's name link atreeitemwith level, position,aria-expandedand (current node)aria-current="page", the sub items agroupthe item owns. One item is in the Tab order; Up/Down, Right (open, or first sub item), Left (close, or parent), Home, End and first-letter type-ahead move through the shown items, Enter follows the link, Shift+F10 or the context menu key opens the node's own menu. The open/close button and the node icon stay for the mouse, out of the Tab order and hidden from screen readers. A load error is read out througharia-describedby; a node whose sub items are all hidden has noaria-expanded. Guide: the admin4 design: keyboard. - English and German text "Content tree". Test:
AdminTreeViewTest.
14 October 2026: every form field of admin4 has a name
Updated
- admin4: every
input,selectandtextareahas an accessible name, about 230 fields in 76 templates that had only a title, a placeholder or their position: template creation, the toolbar editor, package export and creation, workflow events and the workflow editor, shop forms and lists (registration, currencies, VAT, discounts, order and product lists), the Setup > RAD wizards, RSS/OPML export, the version preview, relations, URL aliases, trash, browse lists and the remaining tables. Visible text names a field (<label for>, a label around the text of a radio or checkbox, the column header and the row's name, the card heading); radio groups are named groups. Fields that a title used to name keep the title and carry the same text asaria-label. - New component
.a4-inline-labelin both copies ofcomponents.css: a label around text that was plain before, drawn like that text (core.cssdraws every label bold, larger and as a block). design/standard/templates/shop/country/edit.tpltakes an optionalselect_id, so a label can name the country select.
Fixed
- The Cancel button of the remove-location confirmation (
node/removenode.tpl) lost its closing quote.
Added
- Test:
AdminFormLabelsTest: no form field of admin4 without a name; every<label for>andaria-labelledbypoints at an id of its template (or one an include is given); noaria-labelreplaces a label that has text; the includes that take their id as a parameter (select_id) get it, with a label, from every admin4 template that includes them; and the checker's own cases. - English and German texts for the five new field names.
14 October 2026: contrast of the admin4 colours
Updated
- The focus ring token
--a4-ringis opaque:#c2410clight (5.2:1 on a card, 4.6:1 on the page),#ff9a5cdark. The translucent orange it replaces was 1.8:1 in light mode, below the 3:1 a focus indicator needs. - The current item of the header menu is drawn in
--a4-accent-strong(white text 5.2:1);#f26a21gave 3.1:1. - Filled buttons keep 4.5:1 when hovered: the hover fill is
--a4-accent-pressand its text the new token--a4-on-accent-hover(white in light mode, dark ink on the light orange of dark mode). It replaces three dark-mode rules that borrowed--a4-code-bgas a text colour; the password wizard's primary button had none and was 2.3:1 in dark mode. - Fields have one edge colour,
--a4-field-line, raised to#80879alight and#6f7a93dark (3:1 on every surface); the fields of#pageused--a4-line-strong(1.6:1). --a4-chart-3,-4and-5are darker in light mode (3:1 on a card).- The header search placeholder, the current item of the left menu, the dashboard's version line and the focus outlines of the package pages take the tokens instead of five other oranges and a grey placeholder that was 1.7:1 in dark mode.
- The Info pages (
ezinfo.css), package compare (packagecompare.css), the extension order page (setup-extensions.css) and the package view take every colour from the tokens, so they follow the dark mode. - A focused default button keeps the accent;
theme/rounded.csspainted it teal with yellow text. - Status edges of package compare (changed rows, import entries, the import result) are drawn in the state colour itself (3:1), not its pale line colour; the package view's "ok" badge is the ok tint like the other pages; the community block's edge is the strong line colour.
- A just-moved bookmark or role policy is marked with the accent tint and edge instead of a second focus-like ring.
- Tests:
AdminTokenContrastTestcomputes the contrast of the token pairs in both modes, each state's text on its own tint included (a translucent tint laid over the card);AdminStylingRulesTestchecksadmin4.cssand the page stylesheets for colour literals.
14 October 2026: admin4 focus indicators
Updated
- The header search's button, scope switch and the scope pane's close button show the focus ring again; their
:focus-visiblerules took the outline away and left a tint of about 1.1:1. - The frame's fields that draw their focus as a box-shadow ring (the fields of
#pageoutside.a4-page, the header search field, the light/dark switch, the sign-in fields and the password wizard's fields) set a transparent outline instead of none, so Windows high contrast (forced colors), which drops shadows, still shows the focus. - The sidebar buttons' focus is the 3px ring of every other control (it was a 2px orange outline, 2.9:1 on the soft background).
- Test:
AdminStylingRulesTestfails on a:focusrule of an admin4 stylesheet that removes the outline (none,hiddenor width0in any order;:focus:not(:focus-visible)and:focus-withinaside).
14 October 2026: admin4 and the modes the browser asks for
Updated
- Each admin theme sets
color-scheme, so scrollbars, select popups, date pickers and autofill are dark in dark mode. - Forced colors (Windows high contrast): buttons, fields, cards, badges and chips get an edge, the current tab and
selected rows and cards an outline in
Highlight, a checked chip and the current step of a wizard aHighlightfill, and the data drawn as fills (status dots, meters, bar charts) keep their colours. prefers-reduced-motion: reduceends every transition and animation of admin4 at once (the sidebar slide, the dashboard's lift, the fades); the twotransition: allrules name their properties.- On touch screens the text fields, selects and text areas of admin4 use 16px text (date and time fields included), so a phone's browser does not zoom in when one gets the focus.
- A window narrower than 1024px no longer stores the sidebars as closed: there they open over the page one at a
time, and a wide window shows them again as the user last left them there. Only a click on a wide window
saves the choice, as the user's eZPreferences
admin_left_sidebarandadmin_right_sidebar(like the column width), which the page layout writes into<body data-a4-sidebars>and its cache key, so it follows the user from browser to browser. The browser storage keys used before (ezpAdminLeftSidebar,ezpAdminRightSidebar) held the "closed" every narrow visit had written and are removed, so both sidebars show again once. The width comes frommatchMedia. - The narrow padding of the content wrapper and the frame's narrow rules start below 1024px, not at 1024px too.
- Test:
AdminStylingRulesTestchecks the colour scheme, forced-colors, touch and reduced-motion rules (with the selectors that must win), that no stylesheet transitionsall, and that the sidebar script stores only in one place, inside atry.
14 October 2026: one spacing and type scale for admin4
Added
- Tokens
--a4-space-7(48px),--a4-text-2xl(26px),--a4-text-3xl(32px) and--a4-leading-tight(1.25, for headings), in both copies ofcomponents.css.
Updated
admin4.cssandcomponents.csstake every font size and spacing from the scales: about 30 pixel sizes between 11 and 32px and some 240 spacing lengths (3, 5, 6, 7, 9, 10, 11, 14, 18, 20, 22px …) are the nearest step now. No text of the frame is smaller than 12px (the dashboard's bar values and weekday labels and the node badges were 11 and 11.5px).- Targets of at least 24 x 24px (WCAG 2.5.8): the sidebar tabs are 24px wide (the content column keeps a 24px gutter on both sides, so they cover no content), the search scope switch takes the click in a 24px area, the dialog close, the search scope pane's close and the page numbers are 24px. The content tree's open/close takes the click in a 20 x 20px area (more would cover the next row) and the row checkboxes are 18px; both rely on the spacing exception of 2.5.8 (rows about 27px apart). The light/dark switch and the search field are both 36px high.
- The password wizard keeps room for the wider show/hide button, and an invalid field's text does not move when its border grows to 2px. Its success heading is 16px again, below the page heading.
- Page numbers and the dashboard's bar labels wrap on a 360px phone instead of running out of their card.
- The node view's tabs on/off control (
a.show-hide-tabs,#maincontent-show/#maincontent-hide) was a 16px icon in the gap above the tabs; it is a 36 x 24px switch (track and knob in the tokens, accent when on, forced-colors edges) with the visible label "Tabs" (new string indesign/admin/parts/my/menu, ger-DE "Reiter"), at the right end of the tab row. It is styled by its class, ids andaria-pressed, so a button witharia-pressedin place of the link keeps the look. - Test:
AdminStylingRulesTestfails on a pixel font size or an off-scale spacing length inadmin4.cssorcomponents.css(a few layout offsets listed per selector aside), and checks that the 24px targets stay.
14 October 2026: admin4 content editing says which fields are required and which failed
Added
- Components
.a4-required-mark,.a4-required-hintand.a4-error-summaryin both copies ofcomponents.css;.a4-bi-x-circle-fill; the module classes.a4-content-attr,.a4-content-attr-errorinmodules/content.css. New string "Required field" indesign/admin/content/edit(ger-DE "Pflichtfeld"). - Test:
AdminFormErrorsTest.
Updated
- Content editing in admin4 (
content/edit_attribute.tpl,edit_validation.tpl; the attributes of edit, multiedit, user registration and the template look editor): each attribute is a named group (role="group",id="ezcca-attr-<attribute id>",aria-labelledbyon its name; a grouped input keeps its fieldset and legend as name). A required one shows a red*with "required" for screen readers instead of the visible "(required)"; one hint above the attributes explains the mark, once per page and only when a field is required. An invalid one shows its message under the label (.a4-field-errorwith an icon,id="ezcca-error-<id>", the description of the group or of the grouped input's fieldset) and a red edge. The page's script setsaria-required,aria-invalidandaria-describedbyon the data controls the datatype templates draw, for every datatype at once: not on buttons, file or reset inputs, remove/selection checkboxes, the original side of a translation, or a control inside.a4-no-field-mark(the opt-out for datatype templates). The summary of a failed store is an alert that takes the focus on load, and its entries link to the fields; every attribute it names is marked invalid (also one that failed without a message of its own) and its collapsed group opens. Field names and posted values are unchanged.
14 October 2026: admin4 card parts
Added
- The card component (
.a4-card, both copies of components.css) has a foot,.a4-card-foot: the card's actions on a strip at its bottom edge;.a4-card-actionsat the end of.a4-card-head; a<p>in the head is the subtitle..a4-card.is-flushlets a table run to the card's edges;.a4-card.is-linkmakes the whole card follow the link in its title (focus ring around the card where the browser has:has(), the controls in body, foot and head actions stay clickable);.a4-card.is-busywitharia-busy="true"veils the card with a spinner while it loads (no motion when reduced motion is asked for; a script also setsinert). Foot and flush head take the card's padding from--a4-card-pad-x/-ywhen a density setting defines them, else from the spacing scale.
Updated
- 16 cards that opened with a section head use the card's head and body: the maintenance page (3), RSS import (4) and export (3), the policy limitations (3, the node and subtree pickers with their buttons in the card's foot), the class view and edit, global URL aliases. Field names and posted values are unchanged.
- Test:
AdminCardsTest.
14 October 2026: admin4 layout polish
Added
- Token
--a4-control-h(32px): the height of the controls that stand in a row. Token--a4-zebra(light#f2f4f7, dark#212734) for the even rows of data tables. In both copies ofcomponents.css.
Updated
- Zebra rows in every data table:
table.list,.a4-tableand the sub-items table stripe every second data row with--a4-zebra, counting only rows with data cells (a header row ofthintbodyor ahiddenrow is skipped, so the first data row is plain; a row a script hides still counts); a row header (th scope="row") takes its row's colour. The hovered row takes--a4-info-bg(it was--a4-soft, the same as a stripe), a selected row keeps--a4-accent-soft. Thebglight/bgdarkrow classes no longer set the colour; the RSS import's own unstriped list follows the others. Layout tables stay plain, and the VAT rule and currency forms, laid out as atable.list, takea4-plain.--a4-accent-textis#bc3f0bin light mode (was#c2410c): 4.7:1 on a hovered row too. - The node view's language choice is as high as the buttons beside it (32px; Chrome kept a native select at 19px): drawn by the page with the button edge and radius and a chevron in the tokens; forced colors keep the native control.
- The sub-items toolbar (Select, Create new, Create multiple, More actions, Table options) and the pager buttons draw like the node's action buttons: 32px high, the button radius and edge, ink text in weight 600, the same hover, the global 3px focus ring (the toolbar was 24px with 3px corners and orange text, the pager 27 x 33px). The current page number is the accent tint.
- The side columns: no rule above the first heading of a card, which starts at the card's top (the right column's first heading sat 19px down, now 8px). The content tree's rows are 24px apart at the small and medium column sizes (a 20px line and 4px), the smallest spacing WCAG 2.5.8 allows for its links; the large size gets a 1.25em line. The connector elbow and the open/close symbol sit on the line's middle. In forced colors the language choice is the native control without the page's chevron.
- The left column's width grip was a grey gradient bar; it is a small card on the column's edge with two rows of dots,
the resize cursor and the accent on hover, drag and focus. It is a focusable separator now (
role="separator",aria-valuenowin pixels): the arrow keys change the width by 16px (Shift: 64px), Home and End go to the narrowest and widest (the widest follows the window), the width is saved when the key is let go. It is named by itsaria-label"Width of the left column" (new string indesign/admin/pagelayout, ger-DE, eng-US). - Tests:
AdminStylingRulesTestchecks the shared zebra and that no admin4 stylesheet paints rows in a colour of its own, the controls' common height and the grip;AdminTokenContrastTestchecks text on--a4-zebra.
14 October 2026: admin4 lists and the path in the content tree
Updated
- Data tables (
table.list,.a4-table, the sub-items table): cells sit in the middle of their row (.a4-plainform tables keep their labels at the top); a row whose selection checkbox is ticked takes the selection tint without a script. - Empty lists say so in the
.a4-emptycomponent (28 templates: roles and policies, shop lists and currencies, trash, content jobs, relations, states, workflows, template overrides). - The content tree shows where the page is: the current node's name in the accent tint with a straight bar on its
left (square left corners; a Highlight edge in forced colors), the nodes above it in a stronger weight. CSS only,
from the existing
currentnodeclass and:has(). - The choices above a list (
p.table-preferences: 10 | 25 | 50 items, the shop archive's sort field and order, the filters of Setup > RAD) are a segmented control on the tokens: surface background,--a4-lineedges, links in the accent text colour, the current choice in the accent tint with ink text in weight 650, targets of at least 28px, Highlight for the current choice in forced colors. Before,theme/rounded.css(admin4's copy only) drew them as a light grey gradient with fixed colours, a light box in dark mode whose current value could not be read. - Tests:
AdminStylingRulesTest::testListsMarkTickedRows,testTreeShowsThePathToTheCurrentNode,testEmptyListsUseTheEmptyState,testTablePreferencesAreASegmentedControlOnTheTokens.
14 October 2026: admin4 stacking layers and the side column tabs
Added
- One scale of stacking tokens
--a4-z-*in both copies ofcomponents.css(raised, sticky, frame-grip, frame-control, hover-preview, popover, scroll-top, navbar, sidebar, sidebar-handle, controlbar, drag-target, header, sidebar-toggle, menu, menu-sub, overlay, modal, toast, drag, drag-mask). All 29 z-index numbers of admin4's stylesheets take one of them, in the order they had; a number is left only for 0, 1 and -1 inside one component. The duplicate, deaddiv.popupmenu#EditSubmenurule is gone; the base#overlay-maskrule oftheme/modalwindow.csstakes--a4-z-overlaytoo. Guide: Styling the administration.
Updated
- Context menus lie above the fixed header (
--a4-z-menu1040, the edit submenu 1050; they were 1010 and 1020), so a menu that opens upwards is not cut off. - A dialog's dimming mask and the dialog (
#overlay-mask,.modal-window) lie above the header, the side column tabs and menus (--a4-z-overlay1060,--a4-z-modal1070; they were 121 and 122, under the header). - The tabs on the window edges that show and hide the side columns are named after their column ("Left column",
"Right column"), say with
aria-expandedwhether it shows and witharia-controlswhich it is; on a narrow window Escape closes a column open over the page and gives the focus back to its tab. New strings indesign/admin/pagelayout. - Tests:
AdminStylingRulesTest::testStackingComesFromTheLayerTokens(no other z-index, the order of the layers),testSidebarTabsSayWhatTheyControl.
14 October 2026: admin4 page header and breadcrumb
Added
- Components
.a4-pagehead(pretitle, title, subtitle or meta line, actions on the right that wrap under the title on narrow windows) andol.a4-crumbs(the breadcrumb), in both copies ofcomponents.css.
Updated
- The breadcrumb (
page_toppath.tpl) is an ordered list in the#pathnavigation landmark ("Breadcrumb", new string indesign/admin/pagelayout, ger-DE, eng-US): the separators come from the stylesheet and are not read out, the current step carriesaria-current="page", "You are here:" is read by screen readers only,role="list"keeps the list semantics in Safari. Names are no longer cut to 18 characters: a long name ends in an ellipsis (28 characters, the current page 48) and keeps its full name as title (from 20 characters on, as the cut is by width). On windows up to 600px the middle steps fold into one "..." step for the eye only: they stay readable for screen readers and come back when they take the keyboard focus. An empty path renders nothing. The old#path spancolour, which beat the breadcrumb's own colours, is gone. - One title style for every page: the kernel's
h1.context-title, the node view's title and the dashboard's greeting take--a4-text-xl(22px, weight 650; they were 1.35em, 1.45em and 26px),--a4-text-l(16px) on windows up to 600px, one breakpoint for all three. The node view's header row is built from.a4-pagehead. - Test:
AdminPageHeaderTest(the components in both stylesheets, the breadcrumb's list, current step and full names, the landmark, the one title size).
14 October 2026: admin4 notices with icons, and toasts
Added
- Toasts:
window.a4Toast( message, kind )inadmin4.jsshows a short feedback of an action done in the page in one of two live regions of the frame (#a4-toasts, polite;#a4-toasts-alert, assertive, forkindbad). A toast does not take the focus, closes after 5 s, waits while the pointer or the focus is on it, and has a close button; without motion when the system asks for reduced motion. Components.a4-toasts,.a4-toastin both copies ofcomponents.css. Used by the node view's copy chips (node, object and remote id) and the menu configuration's copy buttons, whose tick was only seen. .a4-notice-title(a bold first line) and.no-iconfor.a4-notice.- New strings: "Dismiss" (design/admin/pagelayout), "Copied to the clipboard" (design/admin/node/view/full, design/admin/visual/menuconfig), in ger-DE and eng-US.
Updated
.a4-notice.is-ok,.is-bad,.is-warnand.is-infoshow their state by an icon too, not by colour alone (WCAG 1.4.1): check, cross, triangle andiin the state's colour left of the text; in forced colors in the text colour. New iconinfo-circle-fillinshare/icons/bootstrap-icons/svg/.- The classic
div.message-*that modules render after an action look like.a4-notice(a state tint and edge, the heading's icon in the state's colour) instead of a 6px black or coloured frame, and carry a role: 25message-errorarerole="alert", the 48 warnings, feedbacks and confirmationsrole="status". A status region that is already in the page is usually not announced when the page loads, so a warning about a standing state (audit switched off) does not interrupt; screen readers differ here. A message without a heading takes the icon on the block itself. - The setup wizards' notices lose their hand-written "!" and tick, which stood next to the icon now; a tick notice is
is-ok. - A copy in the node view or the menu configuration that fails says so ("Could not copy", a toast for errors), and
the textarea fallback gives the focus back to the chip. Toasts with the same text replace each other, at most three
stay; a toast closed with its button gives the focus back to the element the action came from.
li.a4-noticehas no list marker. - Test:
AdminNoticesTest(an icon for every state, the forced-colors rule, the toast component and live regions, the toast writes text only and never moves the focus, every classic message has its role, every copy chip names its toast).
14 October 2026: admin4 follows the system's light or dark mode on request
Updated
- The header's mode switch (admin4, admin4l) steps light -> dark -> system. "System" follows
prefers-color-scheme, also when the operating system changes it while the page is open. The icon (sun, moon, new half circle.a4-bi-circle-halffrom Bootstrap Icons 1.13.1) and the button's accessible name say the mode in use ("Colour mode: light", "… dark", "… system"); the title names the next step ("Switch to dark mode"), and a status region beside the switch announces a press. The button no longer carriesaria-pressed, which can only say two states. admin4l's switch draws its sun and moon as admin icons too (they were inline SVGs in black, lost in dark mode and forced colours). Other open admin tabs follow the change. Without a stored choice the admin stays light, as before; "system" as the default would be a separate decision. The stylesheets keep readinghtml[data-a4-theme](light or dark);html[data-a4-theme-choice]is new. An older copy of the switch withoutdata-label-systemkeeps stepping between light and dark. New strings "Color mode: light/dark/system" and "Switch to light/dark/system mode" in design/admin/pagelayout (ger-DE "Farbmodus: …", "Zum … Modus wechseln", eng-US). Test:AdminThemeSwitchTest.
14 October 2026: a timeline for admin4's audit events
Added
- The component
.a4-timeline(both copies of components.css): an ordered list, newest first, one event per item with its time (<time datetime>, the UTC instant), the event, its meta and a state badge; a dot on a line marks each event in the state's colour (.is-ok,.is-warn,.is-bad,.is-info), which the badge also says in words. Two columns, one on narrow windows; the line is an edge and the dots keep their colour in forced colors. For short chronological lists without actions per row; a list with checkboxes or buttons per row stays a table. design/admin4/templates/parts/audit_timeline.tpl(events, optionalshow_reason): audit events as a timeline, actor and result named for screen readers.
Updated
- admin4 draws the dashboard's audit block (
dashboard/audit.tpl) and the node view's Audit tab (tabs/audit/node.tpl) as a timeline instead of a four-column table; the fetches and links are unchanged, the other designs keep their tables. The full event list (audit/recent, six columns) and the version history (a removal checkbox per row) stay tables. - Test:
AdminTimelineTest(the component in both stylesheets, the dots per state, the line as an edge and the forced-colors rule; the part as an ordered list withdatetime, washed values and named actor and result; the two audit lists use the part).
14 October 2026: one release of each jQuery library in ezjscore
Removed
- ezjscore no longer ships the older jQuery copies that nothing loaded:
jquery-3.7.1.js,jquery-3.7.1.min.js,jquery-migrate-3.4.1.min.jsandjquery-ui-1.10.3.custom.min.js(jQuery UI 1.10.3 has known vulnerabilities, among them CVE-2016-7103, CVE-2021-41182, CVE-2021-41183 and CVE-2021-41184). No template, setting or script of the distribution named them;ezjsc::jqueryandezjsc::jqueryUIload jQuery 4.0.0, Migrate 4.0.2 and jQuery UI 1.14.2 as before. - Test:
ezjscoreVendoredLibrariesTest(one release of each library in the folder, everyLocalScripts[]file of ezjscore.ini exists, the CDN entries and extension.xml name the shipped release; mutation-checked; and no other extension's, siteaccess's or override'sezjscore.ininames a jQuery file that no design holds). - ezjscore 1.7.0 (the kernel copy and the standalone package) carries this change; sevenx_themes_simple 1.0.22 no longer sets the older files for its siteaccesses.
Upgrade notes
- A template or extension of your own that names one of the removed files directly (for example
{ezscript_require( 'jquery-3.7.1.min.js' )}) gets no script any more. Useezjsc::jqueryorezjsc::jqueryUIinstead. - An
ezjscore.iniof your own (insettings/override, a siteaccess or an extension) that setsLocalScripts[jquery],[jqueryMigrate]or[jqueryUI]to one of the removed files loads nothing for that key: remove the line, so ezjscore's default applies, or namejquery-4.0.0.min.js,jquery-migrate-4.0.2.min.jsorjquery-ui-1.14.2.min.js.
14 October 2026: normal or compact density in admin4
Added
- A density switch in admin4's header beside the light/dark button: a button named "Compact view" whose
aria-pressedsays whether the compact density is on (new strings "Compact view" and "Switch between normal and compact view" in design/admin/pagelayout, ger-DE, eng-US). The choice is kept in the browser (localStorage"exp-admin4-density", guarded for blocked storage) and applied before the page is drawn, like the mode. - Density tokens in both copies of components.css:
--a4-list-pad(table.listcells,.4emas before),--a4-cell-pad-y/-x(.a4-table),--a4-card-pad-y/-x(.a4-card);html[data-a4-density="compact"]sets only them and--a4-control-hanew (.25em, 4/8 px, 8/12 px, 28 px). Every target keeps 24 px (WCAG 2.5.8); the content tree keeps its row pitch. The normal density looks as before. - Test:
AdminDensityTest(the compact block sets only density tokens, compact controls stay at 24 px or more, the tree is not tightened, tables and cards read the tokens, the switch is a pressed button applied in the head script, the texts are translated; mutation-checked).
14 October 2026: the admin4 dashboard's figures
Updated
- The key figures of the admin4 dashboard (
content/dashboard.tpl) have an icon tile each (Bootstrap Icons 1.13.1: newgraph-up-arrow,person-check,arrow-up-right,arrow-down-right,dashand their.a4-bi-*classes), and a figure that is a link shows an arrow in its corner. Tiles are at least 180px wide, one column on a phone. - "Published in the last 7 days" says how its full days compare with the 6 days before them:
+5 against the 6 days before, without today(today is not over, so the trend leaves it out), with an arrow, in the muted text colour (a rise is not always good). Both weeks are summed from the 14 days the chart already counts, so the dashboard runs one query fewer than before. - The chart's numbers fold out under it as a table ("Show the numbers"), so the data is there in words for everyone, not only as bar heights.
- New strings in design/admin/dashboard (ger-DE, eng-US). Test:
AdminDashboardTest.
14 October 2026: admin4 left menu groups
Added
- menu.ini can group a long left menu:
Groups[](order),GroupNames[<group>](title),GroupLinks_<group>[](link keys). admin4'sparts/ini_menu.tpldraws each group as a<details>with a small-capital title (summary.a4-nav-title, a caret that turns), open unless the user closed it (admin4.jskeeps the choice per browser in localStorage) and always open when it holds the current page; links in no group follow under "More". Leftmenu_setup is grouped into Content model, Site and URLs, Workflows, Users and messages and System. WithoutGroups[], or withGroups[]reset to no values, a menu is one list, as before; a link named in two groups shows in the first; the access checks per link are unchanged. - The current page's link in a left menu carries
aria-current="page"and takes the accent tint with a straight bar on its left (square left corners; a Highlight edge in forced colors); a plain menu's list is named by its heading (aria-labelledby). The links are drawn by the new includeparts/ini_menu_items.tpl. - The left column is a navigation landmark named "Side menu" (
page_leftmenu.tpl). - New strings: "Side menu" (design/admin/pagelayout), the group titles and "More" (design/admin/parts/setup/menu), ger-DE and eng-US.
- Test:
AdminLeftMenuGroupsTest(every setup link in exactly one group, every group named, the details markup and the flat fallback, the unchanged access checks, aria-current and the current link's square left corners, the landmark, the stored state never closing the current page's group).
15 October 2026: a flat admin4 header
Changed
- admin4's header is one row of 56 px from 1280 px: logo and site link, the main menu, the search, light/dark, the user menu and logout. From 576 px it is two rows (48 px for the logo and the tools, 40 px for the menu), on a phone one row of 56 px with the phone menu as before. It was 145 px at 1320 px with the menu of a few extensions: the tabs wrapped onto two lines and "Logout: admin" took a third.
- The main menu stays on one line at every width from 576 px. The tabs that do not fit go, in their order, into a
"More" list at the row's end (a disclosure:
aria-expanded,aria-controls; arrow keys, Home, End and Escape), which looks like the selected tab while the current page's tab is in it. A ResizeObserver refits the row. Without the script the row scrolls sideways. - Logout is an icon button of its own (
box-arrow-right) beside the user menu, named "Logout (admin)" in itsaria-labeland tooltip, on every width. The new user menu (person icon) shows the user's name and login and the links of the "Current user" box (change information, change password, e-mail preferences), which stays in the right column. On edit pages both are drawn disabled, as the menu tabs are. The markup is the new includeparts/a4_header_user.tpl; admin4l'sadmin_theme_switch.tplincludes it, and itsadmin_tab_menu.tplhas the "More" item instead of the logout link. - The header search is 36 px high (its button was 43 px) and narrower from 1280 px (9 to 15 rem); "/" outside a
field puts the focus in it (
aria-keyshortcuts="/"). --header-heightlives on the root element: admin4.css sets the drawn heights as defaults per width and admin4.js sets the measured height on<html>instead of.dashboard-flex, so the side columns, the sticky edit bar andscroll-margin-topread one value.- The header's cache blocks (admin4 and admin4l
pagelayout.tpl) are keyed by the user's object id as well: the header names the signed-in user. - New strings: "Logout (%login)" and "User menu (%name)" (design/admin/pagelayout), ger-DE and eng-US. New icons:
box-arrow-right.svg,chevron-down.svg(Bootstrap Icons 1.13.1), classes.a4-bi-box-arrow-rightand.a4-bi-chevron-down. - Test:
AdminHeaderTest(the rows and their heights, the "More" disclosure and its script, logout outside the user menu with its name, the cache keys, the "/" key, no literal colours in the new rules).
Upgrade notes
- A site that overrides
page_topmenu.tplkeeps its header. To take the new one, add{include uri='design:parts/a4_header_user.tpl'}and theli.a4-nav-moreitem of the new template; the old#header-usermenu-logoutid is now on the logout icon. - Clear the template cache after the update: the header's cache blocks have a new key.
15 October 2026: admin4's content edit forms take its components
Changed
- The attribute forms of content/edit (also translate, multiedit, user registration and the template look editor)
draw the datatype templates' legacy markup as admin4's components, from
modules/content.css: inside an attribute (.a4-content-attr) every text field, select and textarea looks like.a4-input(32 px, 14 px text, the field edge, the ring on focus, a drawn chevron on a one-line select), everyinput.button,input.button-disabled,button.buttonandinput.defaultbuttonlike.a4-btn(outlined, the main action filled, a disabled button faded instead of hatched), a checkbox or radio like in.a4-check(18 px in the accent), a file field's button like a button. The form itself carries.a4-content-edit, so the buttons of the left column and the relations box take the same look; the sticky button bars keep their own rules. Forced colors get the native select back and keep the main action's thicker edge; touch screens get 16 px field text. - The attribute's name is a
labelorlegendof the class.a4-content-attr-label(13 px, semibold) without the trailing colon; a grouped input (author, matrix, date and time, image, user account ...) is a fieldset of the class.a4-content-attr-groupwithout a box or grey background of its own; the parts of an attribute take the.a4-labellook (12 px, semibold) above their field, and a datatype template that still floats its parts (div.block div.element) has them in a row. - admin4 carries its own edit templates of the date, date and time, time, URL, price, image, file, option and user
account datatypes: their parts stand in a row (
.a4-content-parts) with a label above each field and no colon, headings without a control of their own are.a4-labelparagraphs, the calendar is an.a4-btn-iconwith the new icon class.a4-bi-calendar3instead ofcalendar_icon.png(admin4'stheme/rounded.cssdrops its#page .datepicker-iconrule, which the button no longer carries), the account status is a badge, the option list's columns name its fields and an empty option list is an.a4-empty. The left column's "Base translation on" radios are.a4-checkrows. admin4 also carriesezmedia,ezmultioption,ezrangeoption,ezmultiprice,ezinisetting,ezpackageandezobjectrelationlist: no colons, parts in a row, list fields named by their columns, the relation list's radio and checkbox lists as.a4-checkrows and its invert-selection control as an icon button instead oftoggle-button-16x16.gif, its "create new object" field with a label and shown through thehiddenattribute instead ofstyle, empty lists as.a4-empty, the INI setting warning as a status. - The text of the rich text editors (TinyMCE 3 and 8) follows admin4's light and dark mode:
admin4.jsaddscomponents.cssand the neweditor_content.css(named by the edit form'sdata-a4-editor-css) to each editor's document and copieshtml[data-a4-theme]there; text, links, literal blocks, custom tags and embeds take the tokens. The editor's toolbar keeps its skin. Upgrade: an override of one of these datatype templates in another design keeps working. An extension's own datatype template inside the edit form now gets the field and button look; one that wants to keep its own look gives its controls a class with more weight than.a4-content-attr input(one class plus the element). AdminContentEditFormsTestchecks the rules and the templates (the copied datatype templates post the same fields, ids and label targets as design/standard's);AdminKeyboardTestandAdminFormErrorsTestfollow the new classes.- The label of an attribute in admin4's edit form is the label of its field (
forthe field's idezcoa-<class attribute id>_<identifier>, in the edit and the translation branch), so a text line, text block, number or e-mail field has an accessible name of its own; a grouped input keeps its fieldset and legend. Test:AdminFormLabelsTest. - design.ini
[AdminDesignSettings] IconList[]follows the mask rules of components.css (the width switch's and the date fields' icons in, the removed compact view's icon out).
15 October 2026: one density in admin4
Removed
- The compact view of admin4 and its header switch ("Compact view"): admin4 has one density, tuned for 1440 to
1920 px windows. Removed: the button in
page_topmenu.tpland admin4l'sadmin_theme_switch.tpl, the head script that appliedhtml[data-a4-density], thehtml[data-a4-density="compact"]token block and the switch's styles, the class.a4-bi-view-stacked(the icon file stays:theme/rounded.cssuses it). Upgrade: nothing to do. A signed-in user's eZPreferenceadmin_densityis no longer read and does nothing (delete it withDELETE FROM ezpreferences WHERE name = 'admin_density'if you like); a value an anonymous page kept in the browser (localStorage"exp-admin4-density") is removed on the next page. The strings "Compact view" and "Switch between normal and compact view" stay in the translation files, unused. An extension that styledhtml[data-a4-density="compact"]keeps its rule, which no longer applies.
Changed
- One control height:
.a4-btn,.a4-inputand the fields of.a4-field,.a4-tabs,.a4-chip, the pager sizes and the confirm summary take--a4-control-h(32 px; they were 30 to 38 px), with 4 px block padding. The legacy text fields take the field text size (--a4-text-m, 14 px) like.a4-input. table.listcells are 8 px above and below like.a4-tablecells and 6 px a side (--a4-list-pad, was.4em, about 5 px): a trash or class list of ten columns still fits a 944 px content column.- No line between blocks: the dashed rule above every
.box-header(theme/rounded.css) and admin4's hairline are gone, in the side columns, the node's children and every box; blocks in a side column are 24 px (--a4-gutter) apart (the header's padding, so no margins collapse), the width switch under a left menu too. The right column is--a4-right-width(16rem) wide, so its card keeps 208 px beside the gutter. - The density tokens (
--a4-gutter,--a4-control-h,--a4-list-pad,--a4-cell-pad-*,--a4-card-pad-*) keep one value each in the token block. Card padding is unchanged (12/16 px).
14 October 2026: each admin links to its own site
Added
site.ini [SiteSettings] FrontSiteAccess: the site an admin siteaccess belongs to. Its "Open the site" link (admin3, admin4, admin4l) and the node view's "View on site" go there; empty (the default) keepsDefaultAccess. Before, every admin of an installation that serves several sites from one tree linked to the same default site. Set it in the admin siteaccess'ssite.ini.append.php. The template operatorsiteaccess_url()without a parameter now calls the newezpSiteAccessURL::front(); with a parameter, andezpSiteAccessURL::root(), nothing changes. A siteaccess that is not inAvailableSiteAccessListfalls back toDefaultAccesswith a debug warning. The header cache blocks key on the address, so each admin caches its own link. Compiled templates still call the old code until the template cache is cleared: after updating, clear the caches of each admin siteaccess (php bin/php/ezcache.php --clear-all -s <admin siteaccess>). Tests:ezpSiteAccessURLTest(front()without, with and with an unknownFrontSiteAccess, padded value),eZKernelTemplateOperatorsTest(the compiled template callsfront()).
14 October 2026: the admin4 accessibility series, merged and refined
The 24 changes above (icons, language badges, context menus, landmarks, keyboard, tree view, form names, contrast, focus, modes, scale, tables, lists, layers, form errors, page header, notices, mode switch, timeline, density, dashboard, cards, menu groups, front site access) were merged together and refined:
Added
window.a4Pref(the first script ofpage_head_style.tpl) anddesign:parts/a4_preference_attributes.tpl: the frame's remembered choices are the signed-in user's eZPreferences, written on<html>before the page is drawn:admin_color_mode(light, dark, system),admin_density(normal, compact),admin_left_sidebarandadmin_right_sidebar(the columns on a wide window),admin_navgroups(closed left menu groups). An anonymous page keeps them in the browser only; a choice the browser held before is taken along once. Test:AdminPreferencesTest.window.a4CardBusy( card, on, message )(admin4.js): a busy card setsaria-busyandinerttogether and announces a short message in a polite live region.- Layer tokens
--a4-z-skip-link,--a4-z-controlbar-top,--a4-z-card-veil,--a4-z-card-spinner; the frame tokens--a4-explayouts-nav,--a4-explayouts-nav-edgeand--a4-pw-toggle-room(each with its dark value).
Updated
- admin4l (the admin siteaccess): its page layout has the skip link, the landmarks, the named side and top menus,
the width grip as a focusable separator and the toast regions of admin4; its header blocks are copies of admin4's
top menu again (Bootstrap icons, column tabs, mode and density switches), and
AdminIconsTestkeeps them in step. - No text on an admin4 page is below 12px:
core.csssets the body to--a4-text-s(13px) and every em size of the base and page stylesheets that would land below 12px takes--a4-text-xs. The styling test keeps no color or spacing exception list any more. - Tables: the zebra's row filter sits in
:where(), so a hovered or selected row shows its state; compact list rows keep 24px targets. - Context menus: Enter runs an item without a link too, a letter moves to the next item starting with it, the title
is no item of the menu, the tree's menu buttons are named "Actions for <node>", and the class and node icon menus
carry
aria-haspopupbefore their first use. - The content tree's buttons name a node as it is called (no doubled entities for umlauts or
&), its items name Shift+F10 inaria-keyshortcuts; the node tabs move with Arrow Left and Right only; each calendar button is named after its field. - Language badge links: the accessible name starts with the code shown (WCAG 2.5.3).
- Contrast: the current and just-moved row bars, the header search edge, the language select edge and the tabs switch's on edge reach 3:1; mask icons and status dots stay visible in forced colors; reduced motion also stops jQuery's effects.
- Side column tabs name their column, report
aria-expanded, and on a page without that column name no target; the narrow and wide rules meet at 1023.98px. - Content editing: every validation message is escaped exactly once in the summary and at the field, and a summary
link scrolls its field below the fixed header. A warning that reports a failed form is
role="alert". - Toasts lie on
--a4-z-toastabove dialogs and above the debug bar; an error toast stays until dismissed; Escape closes a focused toast. - Breadcrumb: the last step is the current page also while editing; a name over 20 characters keeps its full name as title.
- Dashboard: the trend compares full days only ("against the 6 days before, without today"); the bar chart leaves its numbers to the table for screen readers.
- Audit timeline results, the left menu's "More" group and the mode switch ("Color mode") are translated; the
left menu marks only the page itself as
aria-current="page". ezpSiteAccessURL::front()warns about an unknownFrontSiteAccessonce per request also on Velocity's persistent workers.
Upgrade notes
- Clear the template, template-override, template-block and content caches after the update (new templates and stylesheets), and hard-reload a browser that still shows the old stylesheet.
14 October 2026: admin4's base stylesheets on the tokens
Fixed
- Links of the base stylesheets (
core.cssa,ul.tabs, the datatable's editable cells) take--a4-accent-text: the old#0b4e7awas 1.8:1 on a dark card (now 7.7:1; 5.5:1 in light mode). - The diff marks of
content.cssare drawn from the tokens everywhere, as the history page already was: added text--a4-ok, removed text--a4-bad, the shown side's changes ink on--a4-warn-bg(light text on yellow was 1.1:1 in dark mode, now 10.5:1), block changes on the ok and bad tints. - The focused header search field kept a pale yellow background under light text in dark mode (1.1:1); it is the field colour. The search button's hover is the header's own, not a teal border.
- A focused
input.buttonlooks like a hovered one (theme/rounded.csspainted itrgba(0, 0, 0, .1)over admin4.css); the ring marks the keyboard focus. .a4-meterfills with--a4-accent-text: 5.1:1 (light) and 7.0:1 (dark) on its track, where--a4-accentwas 2.9:1 in light mode (both copies of components.css).- The context menu's focus ring is
var(--a4-ring)in both modes, without a dark-mode rule of its own. - The search scope pane fits a phone (
min(300px, 100vw - 32px)); its title line takes--a4-line. - The tabs on the window edges that show the side columns take
--a4-field-linewith the card colour on it (3.2:1 and 4.0:1 on the page, the arrow 3.6:1 and 3.8:1 on the tab), instead of a fixed grey. - In forced colors (Windows high contrast) every module state shown only by border, background or shadow (a current
chip or tab, a selected or moved card or row, a pressed preset, a changed settings bar) gets a Highlight outline
(
modules/*.css, both copies).
Added
- A print style for admin4: the content alone on the full width, no header, menus, side columns, control bars, toasts or footer, no shadows; the head script prints in the light colours and goes back to the chosen mode afterwards. No dark fallback without JavaScript: the admin stays light then, its documented default.
- Tests:
AdminStylingRulesTest::testBaseStylesheetsUseOnlyColourTokens(no colour literal in core.css, content.css, pagelayout.css and the three theme stylesheets; the allowlist for kept literals is empty and only shrinks),AdminBaseStylesheetsTest(links, diff marks, forced-colors outlines, meter, button focus, menu ring, header parts, removed files, no vendor-prefixed leftovers, print) and the meter pairs inAdminTokenContrastTest; each new assertion was mutation-checked.
Updated
- The rest of the colour literals of the base stylesheets are tokens as well (messages, rulers, the datatable and modal window headers, the default button's base colours, the footer links, the phone menu); the ones admin4.css overrode keep looking the same.
Removed
design/admin4/stylesheets/responsive.css: all but one rule was commented out. That rule (the padding of the status messages) is inpagelayout.css;BackendCSSFileList[]=responsive.cssis gone fromsettings/design.ini, and admin3 names its ownresponsive.cssinpage_head_style.tpl, so admin3 looks the same.setup-extensions.cssin admin4 and admin: no kernel template loaded it. The Admin UI design (exp_adminui) loaded it through its design fallback and ships its own copy from exp_adminui 1.0.0.17 on.- Rules of admin4's base stylesheets for markup no admin4 template writes: the admin3 login page (
.loginpage,.login-input-wrapper,#icon-loginand the old#logintext/#loginbuttonlook, which admin4's sign-in page replaced),#header-topmenu,#header-usermenu,#canvas-top/-bottom,p.versions; and-moz-,-ms-,-o-transitions, shadows and gradients,filter: alpha(),zoom: 1and the invalidlinear-gradient(top, …).
Upgrade notes
- A site whose own
design.inioverride still listsBackendCSSFileList[]=responsive.csskeeps working: admin4 then finds admin3's identical file through the design fallback. - An extension that styled admin3's login page through admin4's
pagelayout.css(.login-input-wrapper...) needs its own rules; admin4's sign-in page never used them.
14 October 2026: names and request values in the admin designs are inserted as text
Fixed
- Node, class, section and language names in the content tree menu, the popup menus and the relations list are
inserted as text, and the sub-items name filter is escaped. The content tree keeps each node's menu data in the
script and its menu button carries only
data-node-id; one click listener opens the menu (mouse, Enter, Space, Shift+F10 and the context menu key as before).ezpopmenu_showTopLevel()keeps its parameters; itsmenuHeaderis now set as plain text, and the%...%values of a menu item'scontentgo into the item as text and attribute values (in all three copies of ezpopupmenu.js).ezpopmenu_submitForm()also fills the placeholders of the form'saction. - admin, admin3, admin4 (and standard where the same line exists): request values and stored texts that were
printed raw are escaped for where they stand (the login form's redirect address, the template list filter, the
shop list offset, URL and workflow fields, currency and package error texts, validation messages, relation
attribute and translation names, left menu names); names passed to a popup menu from an
onclickattribute are escaped for the script and then for the attribute. The link class preview links only to http(s), mailto and addresses without a scheme and shows anything else as text. wash( 'javascript' )also escapes<,>and&(as\x3C,\x3E,\x26), line breaks (\n,\r) and the line and paragraph separators, so its output is safe inside a script element and inside an HTML attribute as well as in a JavaScript string; the value a script reads is unchanged. A JS-escaped value in an HTML attribute (anonclick) is HTML-escaped as well (|wash(javascript)|wash). The sub-items name filter reaches the server URL-encoded and as typed.- After the confirmation,
content/hidereturns to the page the user came from: the view has theadministrationUI context, so its confirmation page is not stored as the page to return to. content/hidehides or reveals a node only on a POST; a link (GET) shows the confirmation form, in English and German. The Advanced popup menu's "Hide / unhide" and the Hide and Reveal buttons of the Locations tab (admin, admin4; new shared class.a4-link-button) submit a form.- The content tree icon's title is escaped once (a translation with an apostrophe showed
'). The redirect page ofDebugRedirectionescapes its address. - Tests:
AdminTreeMenuEscapingTest(also: no kernel design links tocontent/hide),eZTemplateOperatorMatrixTest(wash javascript in a script and with&).
Upgrade notes
- A custom template or extension that links to
content/hide/<node>now gets the confirmation page first; post a form to the same address to act at once. AmenuHeaderpassed toezpopmenu_showTopLevel()with markup in it shows the markup as text. Output ofwash( 'javascript' )used outside JavaScript now shows\x3Cfor<and\x26for&. Without ezformtoken,content/hideis protected by the POST and the browser's SameSite default only; ezformtoken adds the form token. Clear the template and template-override caches after the update: compiled templates keep the old escaping until then.
14 October 2026: the search of the administration
Fixed
- content/search and content/advancedsearch take the section, the class, the class attribute, the period, the
subtrees and the time limit of the request as integers; anything that is no positive number is none. The ezjscore
search (
ezjscServerFunctionsJs::search()) passes only positive integer ids (makePostIDArray()) and an offset and limit of at least 0 and 1. A class may still be named by its identifier (SearchContentClassID=article,SearchContentClassIdentifier): both searches look it up to its id (makePostClassIDArray(),AdvancedSearch::classID()), and an identifier that names no class is no class filter. Before, the values reached the search engine as they were sent, and eZ Find wrote them into its filter queries. Updating eZ Find as well is recommended: its query builder now casts the ids itself. - A section that does not exist no longer stops content/search and content/advancedsearch with a fatal error: the search is not limited to a section then.
- content/advancedsearch: a class attribute that does not exist, or that belongs to another class than the one searched, is no attribute filter (before: an error with eZ Find, or no result at all). The page opened without anything to search for runs no search; before it listed every object.
- A negative offset is 0; a search text is cut to
site.ini [SearchSettings] MaximumSearchTextWordswords (16) andMaximumSearchTextLengthcharacters (1024), with a note on the page. Every word is a clause of the search engine's query, and a very long text made the search fail. - The pages of a search keep where it searches: every subtree as
SubTreeArray[]and the section (before: the subtrees joined by commas, which the view dropped, and no section); also in the browse mode. The advanced search's pages pass on the words as typed, not with the phrase in front of them a second time. - The title of content/advancedsearch is escaped, as content/search's already was.
- The built-in search engine (
eZSearchEngine, without eZ Find) writes the class attribute and the subtrees into its SQL as integers (eZSearchEngine::subTreeNodeIDList()); a subtree id that was not above 0 no longer leaves a stray comma in the node query.
Updated
- admin4, content/search and content/advancedsearch: a search the engine could not run shows an alert
(
.a4-notice.is-bad,role="alert") instead of "No results" and search tips; an empty text shows a prompt instead of "Search for <>"; the search form is a search landmark with a visible label and a search field. Each result shows the path of its location, so objects of the same name can be told apart. - admin4 pager (
navigator/google.tpl): a navigation landmark "Pages", the current page marked witharia-current="page". - admin4 header search: the sections of its popup are fetched once for both copies (
page_topmenu.tpl), the name of the node searched in comes from the page's path when it is on it, and the browse mode's page size never exceedsMaximumSearchLimit. - admin4 list filters of class groups, classes, workflow groups and sections say that they search only the page shown when the list has more than one page. Setup > RAD survey: the filter reacts to every input (typing, pasting, dictation), announces its count and is translated.
- New strings (design/admin/content/search, design/admin/navigator, design/admin/setup/rad and the four lists), ger-DE and eng-US.
- Tests:
ContentSearchRequestTest,ezjscServerFunctionsJsSearchIDsTest,AdminSearchTest,eZSearchEngineIdsTest.
Upgrade notes
- Clear the template and INI caches after the update. A site design of your own that overrides
content/search.tpl or content/advancedsearch.tpl can read the new template variables
search_requestedandsearch_text_shortened, and$search_extras.error(eZ Find) for a failed search. - These rules apply on every siteaccess, the front ends included: without
AllowEmptySearch, a search without a text (a link such ascontent/search?SubTreeArray[]=<node>used to list a subtree) runs no search and shows the prompt; a search text is cut to 16 words and 1024 characters (MaximumSearchTextWords,MaximumSearchTextLength). A value that is neither a positive number nor a class identifier is ignored, so the search is then not limited by it.
14 October 2026: administration hardening
Action required
exporting extensions is now off by default — list the extension in package.ini
[ExtensionExportSettings] AllowedExtensions[] (or * for every extension) and grant the policy
package/export_extension to the role. Without both, Setup > Extensions shows no download buttons and the package
wizard does not let you choose the extension; the command line (ezpm.php add) needs the list entry.
Updated
- Fixed: the AJAX uploader keeps the stored file on the server between its steps; the location form carries a
one-time token bound to the user and the upload target instead of a file path (
ajaxuploader/location.tplof design/admin and design/admin4), and the file is taken only from the storage directory. The upload step checks the handler'scanUpload()as the other steps do.[ezjscServer_ezajaxuploader]declaresFunctions[]=ezajaxuploaderwithRequirePolicy=disabled;enabledalso asks forezjscore/callwith that function list. - Fixed: the AJAX uploader's browse step (
ezajaxuploader::browse) asks the upload handler'scanUpload()like the other steps, before it reads the node; the call names the handler (ezajaxuploader::browse::<handler>::<node>::<class>) and posts its target (ajaxuploader/browse.tpl,ajaxuploader/location.tplandexpajaxuploader.jsof design/admin and design/admin4). - Fixed: audit/recent shows only the channels the Channel limitation of
audit/readallows, refuses a channel outside it, and its "Verify now" button needsaudit/manage, as on the console; the button keeps the channel filter. - Fixed: Setup > Extensions downloads a package through a button of its form (POST) instead of a link; it says why
when it cannot and removes the temporary package. No package takes an extension that holds secret values or key
material: one rule,
eZExtensionPackageHandler::packRefusal(), refuses it for Setup > Extensions, the package wizard (which shows the reason and removes the package it had begun), the command line andaddExtension()itself. The check runs on the extension before anything is copied and once more on the copyaddExtension()makes in the package, which decides: what is packed is exactly what was read, and a refused copy is removed. It walks the same entries the package copies (packEntries()), hidden files and links included, and fails closed: a pattern that fails (PCRE backtrack or JIT limit), a name over 256 bytes, a value over 64 KB, more than 200,000 entries, a missing or unreadable directory and any other error refuse the extension. It refuses an extension with:- key material by name, any case:
*.key,*.p12,*.pfx,*.jks,*.ppk,*.p8,*.gpg..., also*.key.*(server.key.old),credentials*,*service-account*,.envand.env.*(not.env.example,.sampleor.template),id_rsa*,.htpasswd,.netrc,.pgpass, and every file in a directory namedjwt,.ssh,.gnupg,*.pemor*.key; - a symbolic link that leads outside the extension, or anything that is neither file, directory nor link;
- a copy of a settings file (
*.bak,*.orig,*.dist,*.old,*~...) belowsettings/or named*.ini*; - in every file, text or binary, read whole and byte for byte as it is packed (and once more without NUL bytes,
for UTF-16 text without a byte order mark): a private key
(
-----BEGIN ... PRIVATE KEY-----),Authorization: Bearer ..., service tokens (sk-...,AKIA...,AIza...,ghp_...,github_pat_...,xoxb-...,glpat-...,sk_live_...),define( '..._KEY', '...' )(KEY, SECRET, TOKEN, PASS ...) and an address with a password; - in settings files (below
settings/, any depth, or named*.ini*), everyname = value, read as text, not as eZINI reads it (commented lines,Key[]=, blanks, quotes, BOM, CRLF), and in configuration files (*.yml,*.yaml,*.conf,*.cfg,*.properties,*.toml,.env*...) alsoname: valueand YAML values on the next line: under a secret name (password, secret, token, key, pass, pw, auth, bearer ...) every value but an empty one, a whole placeholder (...,xxx,***,<your key>,Enter your key here), a switch (enabled,disabled,true,false,none,null,0), a template placeholder (%name%,{name},${NAME},{{ name }},%env(NAME)%) or a measure or version the name announces (MaxTokens=4096,IEDisableWithPassword[]=11); an address, header name, type, mode or method (TokenURL,ApiKeyHeader) and a public key (PublicKey) are read only for a secret inside them; under another name a value with a real secret in it; - a file with a UTF-16 or UTF-32 byte order mark, a settings or configuration file with a NUL byte or over 4 MB,
and a file or directory that cannot be read to its end.
The text is read as it is: no byte order mark is removed, no line end changed, nothing decoded. A line holds the
one pair it starts with, and the rest of the line is its value, read for secrets inside it; lines end at CR, LF,
FF and VT; a YAML value may stand on any following, deeper line. Certificates (
*.pem,*.crt,*.der,*.asc, such as a CA bundle) are refused only for a private key in them. In comments, constants and addresses of documents and tests, the usual example values pass (# Password=changeme,https://user:pass@...,Bearer test-...); an active setting is taken at its word (Password=secretis refused). The package wizard accepts only names of extensions.
- key material by name, any case:
- Added: an extension can list findings of the check that are known examples in a
package_check.iniat its root (AllowedFindings[]=<path>:<setting or (kind)>); key material, private keys, copies of settings files and links can never be listed. Guide:doc/eng-us/bc/6.0/extension-package-secret-check.md. - Updated: an extension package leaves out
node_modules(the dependencies of a JavaScript build), which the check does not read. The extensions that ship example values under secret names carry apackage_check.inifrom ezauthorize 1.4.1, ezodf 6.1.8, hcaptcha 1.3 and recaptcha 1.4.6 on; on a development installation every extension packs except those with stray local files (editor backups of settings files, a test key file), which the check names. - Updated: audit/recent shows its "Verify now" button only to users with
audit/manage. - Updated: the AJAX uploader's token lives an hour; files whose token expired or was dropped leave the storage. A location that is refused keeps the token and the stored file, so another location can be chosen, and the temporary copy of each upload stands in a directory of its own and is removed in every case.
- Added:
package.ini [ExtensionExportSettings], closed by default:AllowedExtensions[]lists the extensions that may be exported (empty, the default: none;*: every extension the check lets through; applies to every way, the command line included), andRequirePolicy=enabled(the default) makes exporting an extension in the administration (Setup > Extensions, the package wizard) need the new policy functionpackage/export_extension(the Administrator role with all functions has it;disableddrops that requirement). Where exporting is not allowed, Setup > Extensions shows the reason instead of the download buttons, the package wizard disables the extension, and both say in a notice how to switch exporting on. - Fixed: Setup > System information > PHP leaves out the web server's request headers and environment and masks
secret values (names ending in pw, pass, passwd, auth, bearer or authorization included,
expSystemReportMask::isSecretSettingName()); Setup > Site data masks secret values; Setup > Sessions no longer gives session keys to the template (design/standard posts and selects session references like the admin designs). The system information names the front site of the admin (FrontSiteAccess) as the public site. - Fixed: the dashboard reads a block's
Priorityas a number. - Tests:
ezjscServerFunctionsAjaxUploaderTokenTest,expAuditRecentChannelsTest,ExtensionDownloadTest,SetupInfoMaskTest.
Upgrade notes
- Action required: exporting extensions is now off by default — list the extension in package.ini [ExtensionExportSettings] AllowedExtensions[] and grant package/export_extension.
- Clear the template and INI caches after the update. A site with its own override of
ajaxuploader/location.tplreplaces the hiddenUploadFilefield with<input type="hidden" name="UploadToken" value="{$upload_token|wash()}" />. A site with its own override ofajaxuploader/browse.tplputs the handler in its links:ezajaxuploader::browse::{$handler_id|wash()}::<node>::<class>.
14 October 2026: admin4 clean-up
Fixed
- On a siteaccess that requires a sign-in (
RequireUserLogin=true, normally the admin),ezjscore/callneeds a signed-in user: ezjscore'ssite.inino longer lists it in[SiteAccessSettings] AnonymousAccessList. Front ends (RequireUserLogin=false) do not use the list and are not affected. The debug bar shown by IP on an anonymous admin page (the sign-in page) then shows "Could not load" in its server panels. - The header's logo names the release only to signed-in users; the anonymous pages of the admin (the 401 page,
user/register,user/forgotpassword) draw the same frame and said "Exponential <version>" (admin4, admin4l). menu.ini: twelvePolicyList_*entries named a view instead of a policy function (visual/toolbarlist,class/grouplist,role/list,workflow/processlist...); those modules have no policy functions, so any limited policy answered "no". They are<module>/*. The Design tab openscontent/view/full/58(the design top node,DesignRootNode) instead of the URL aliasDesign/.- setup/designextension: the file anchors used the operator
simplify_tag_name, which does not exist; they are numbered now (admin, admin4). - The version preview's full-screen link has a text alternative; the product previews translate "Category".
- The crystal-admin icon of the job class was missing in the ghost and original sizes;
MimeMap[video/video/...]in the crystal and crystal-adminicon.iniisvideo/vnd.rn-realvideo. - Scripts (admin4, and the identical copies in admin):
ezpopupmenu.jsdeclares its variables, places a submenu without a parent menu instead of throwing, and hides the menus on an outside press through an event listener instead of replacingdocument.onmousedown; the content tree draws a node of a class its cached parameters do not know (its parent stayed busy) and loads a node again when it is pressed after an error; both tab switch links are handled bynode_tabs.js(the show link reloaded the page through a GET that changed the preference); the relations search useskeydown/e.keyand readserror_textonly from an answer; the upload dialog decodes its answer inside atryand adds its hidden fields once; the sub-items table has no repeatedid="thumbfield", and Create here is disabled where no class may be created (the item id was passed with quotes).
Added
- admin4 carries the 21 templates it took from
design/admin:audit/*(eleven),parts/audit/menu.tpl,tabs/audit/node_header.tpl,contentfinder/*(six),infocollector/collectionlist.tplandview.tpl, adapted to admin4's checks (scoped header cells, status messages, an icon button and named checkboxes).AdminSelfContainedTestfails for a template the kernel or an admin4 template names that onlydesign/adminordesign/admin3has. - German translations of 87 admin4 texts (mostly the content jobs pages) and the same sources in eng-US.
- The Quality workflow and the pre-commit hook check changed JavaScript files with
node --check. - Tests:
AdminMenuPolicyListTest,AdminScriptsTest,AdminSelfContainedTest,ezjscoreAnonymousAccessTest, the crystal themes inAdminIconsTest, the brand name inAdminPageHeaderTest.AdminStylingRulesTestscansoverride/templatestoo and fails for a listed design that does not exist (it listedadmin2).
Removed
- From admin4: six templates nothing used (
test.tpl,page_menuhead*.tpl,pdf/edit_validation.tpl,content/confirmtranslationremove.tpl), the user tab overrideswindow_controls_user.tplandwindows_user.tpl(no override rule has matched them since 4.6), 37 images no admin4 template, stylesheet or script reaches (images/2/*, most ofimages/3/*, old message and sprite images),leftmenu_widthcontrol.js(design/admin's column grip), and the unused functionswrapTable()andadjustHeaderHeight()ofezadmin_menubar.js, whose other functions are no longer globals. - The inline
styleofembed_image.tpl(admin, admin4) andtemplate_look_edit.tpl(admin).
Upgrade notes
- An admin siteaccess that relied on anonymous
ezjscore/call(none of the shipped pages does) must sign the user in first. A front end withRequireUserLogin=false(the default for front ends) is unaffected; a front end withRequireUserLogin=truewhose pages callezjscore/callanonymously (rating, captcha refresh, …) addsAnonymousAccessList[]=ezjscore/callto its own siteaccesssite.ini.append.php. - The Design tab opens
content/view/full/58, the nodecontent.ini [NodeSettings] DesignRootNodenames in a standard installation; a site whose design node has another id overrides[Topmenu_design] URL[default]inmenu.inias well asDesignRootNode. - A role that should open Setup > Classes, Roles, Workflows, Triggers, URLs, Search statistics, Settings, the
template list, the toolbars or the menu settings through the left menu needs the module (
class/*...), as before; a customPolicyList_*that names a view of these modules is better written as<module>/*. - A site whose admin override rules point at
window_controls_user.tplorwindows_user.tplremoves the rules, as since 4.6. Clear the template and template-override caches after the update.
15 October 2026: admin4 on phones, tablets and wide windows; one size for legacy controls
Updated
- admin4's legacy buttons, selects and text fields (
input.button,input.defaultbutton,button.button, fields without a class) take the size of its buttons and fields (--a4-control-h), with a select chevron in the tokens and the native select back in forced colors; every left menu shares one look and ends with one width switch (parts/left_width_control.tpl: three icon buttons in a POST form, the current width pressed) and one trash link (parts/left_trash.tpl); the node's tab box is a grid, so the end of the open tab is no longer cut off. New:.a4-tabs.is-segmented/.is-icons. An extension's navigation part without theezprefix finds its left menu in admin4 and in admin4l. Test:AdminLegacyControlsTest. - admin4 fits phones, tablets and wide windows: an opaque phone menu, a top menu on one line from 576 to 1279px, the
right column as a drawer below 1280px, a content column capped at
--a4-content-max(1680px), body text from 13 to 14px on very wide windows (--a4-text-body), one gutter (--a4-gutter), the node's tabs scrolling on a phone, search results with their path under the name, 24px targets on touch screens. Tests:AdminResponsiveTest,AdminSidebarDrawerTest. - The right drawer remembers whether it is open: the eZPreference
admin_right_drawer(localStorage on an anonymous page), saved and restored on windows from 576 to 1279px (960px at 200% zoom included), never on a phone, and apart from the wide-window choiceadmin_right_sidebar. Opening a drawer with its tab moves the focus into it, Tab and Shift+Tab stay inside the open drawer and its tab, Escape closes it and returns the focus to the tab.
15 October 2026: the online editor's message for a dead link
Fixed
- A link of the online editor whose target is gone no longer says only "Invalid link: ... Target node does not
exist". The message names the link by its text and, when the target is in the trash, its name, class, place and the
time it was trashed, and says how to repair the link; a link by location id (
eznode://42) also says that restoring the target does not repair it, because the restored object gets a new location. Name, class and place are shown only to a user who may read the target; a trashed object has no location, so a user whose read policy is limited to subtrees or nodes gets the short message that the target is in the trash. The trash is looked up only for a link whose target is gone, so a link to a live target costs no query. German translations included. - The messages of the validation log are plain text, and every design prints them escaped exactly once: the online
editor's link messages keep the target's name, its place, the link text and address as typed, the XML text
parsers store their messages as characters (
eZXMLInputParser::plainText(): several translations spell tag names as<and>), anddesign/admin,design/admin4anddesign/standardprint every validation, location, custom rule and field message withwash(content/edit, info collection, user registration, the PDF export form); the step in admin4 that turned<and>back before washing is gone. exp_adminui 1.0.0.18 and sevenx_themes_media 1.5.56 do the same in their own edit_validation.tpl. Test:expOEValidationMessagesEscapedOnceTestrenders each design's template with a name holding markup,&, quotes and umlauts. - Test:
expOEDeadLinkMessageTest. - A script link or an invalid e-mail address in the online editor marks the input invalid
(
eZOEInputParser::isValid()is false): the parser setisInputValid, a property it does not have, instead ofIsInputValid, which PHP 8.2 reported as a dynamic property, and the e-mail check read the undefined propertyerrorLevel. The messages and the saving of the text are unchanged; ezoe does not readisValid()when it saves. Test:expOEInputValidFlagTest. - The online editor recognizes a script link in every spelling a browser follows (character references with or
without
;,:,	, tabs and line breaks in the scheme, leading spaces and control characters, any case) and refusesjavascript:,vbscript:,livescript:anddata:with anything but a plain image type; "javascript" in a path, query or file name stays an ordinary link. Every address of amailto:link is checked (several addresses, percent-encoding, a query), and internationalized domains are accepted (kontakt@bücher.de, anxn--top level domain), which the previous pattern refused. The script link message is translatable. Test:expOELinkAddressCheckTest.
Upgrading
- A design of your own that overrides
content/edit_validation.tpl(or the info collection, registration and PDF export messages) prints each message with|wash, once. The messages no longer carry</>; a template that printed them raw now shows a tag name as markup, and one that turned the entities back keeps working but can drop that step.
15 October 2026: the online editor's parser and a line break with attributes
Fixed
- The online editor's input parser no longer warns "foreach() argument must be of type array|object, false given"
(
eZOEInputParser::setAttributes()) for a<br>that carries attributes. The output tagbrdeclares'attributes' => false, and the override that maps attribute names in any case iterated over it. The handler writes every empty table cell as<p><br data-mce-bogus="1"/></p>, so an editor that posts the text back unchanged (switching the editor, saving without an edit) raised one warning per empty cell; where errors are displayed it was printed before the doctype and put the page in quirks mode. The stored XML is unchanged. Test:expOEInputBreakAttributesTest.
15 October 2026: findings of an end-to-end test of the administration
Fixed
- content/history: a version's Compare button orders the two versions by number (
History::comparedPair()); an explicit From/To choice stays as chosen. Test:expContentHistoryListTest. - The AJAX uploader refuses a missing or incomplete
AjaxUploadHandlerDatawith "Arguments error." instead of anArgumentCountError. Test:ezjscServerFunctionsAjaxUploaderHandlerArgumentsTest. - Browse: the quick select of the current location is pre-checked only for radio buttons ("Add locations" added a
location under the start node). Test:
AdminBrowseCurrentNodeTest. - content/hide: after the confirmation the view returns to the page the form was opened from (
RedirectURI, checked as a local return address), not to the page last viewed in the session. Test:expContentHideReturnTest. - The "Edit in" menus name languages like the other menus; German and American English for the content job pages;
a keyboard-opened context menu stays at its trigger and below the fixed header; the edit bar's save status moves no
button and the editor toolbars stay below the bar (TinyMCE 3 and 8). Tests:
AdminJobTranslationsTest,AdminContextMenuTest,AdminEditBarTest.
Removed
- Script and stylesheet links carry no version query: with packing off the packer writes each file's own address
(the packed files keep their time in their name), and the TinyMCE 8 edit template no longer appends its cache key
to its links. A browser that kept an old copy is reloaded by hand. Test:
ezjscPackerNoVersionQueryTest(no template, script or class of the kernel and its extensions puts?v=on a link). - ezjscore 1.8.0 (dual release with se7enxweb/ezjscore): the uploader's browse step asks the handler, its arguments are checked, unpacked links carry no version query.
15 October 2026: the admin4 Setup overview
The Setup tab opens an overview of every Setup function as a tile, grouped as the Setup left menu. Feature.
Added
setup/menuin admin4 (setup/setupmenu.tpl): a page header and one section per group of the Setup left menu (Content model, Site and URLs, Workflows, Users and messages, System, and "More" for links an extension adds). Each tile is a card with a Bootstrap icon, the function's name as its link (the whole card is the target, one focus ring around it) and one line on what it does; the description is the link's description (aria-describedby). The grid counts the content column (container queries): one column on a phone, then two, three and four. Light and dark, forced colors.- The tiles are the left menu:
parts/ini_menu.tpltakesmenu_layout='tiles'and draws the same links, groups and access checks (PolicyList_<link>[],CheckViewAccesswithfetch( 'user', 'can_open' ),NoAccessLinks) through the new includeparts/ini_menu_tiles.tpl. A function the user may not open has no tile (withNoAccessLinks=disabled: a tile without link and without figure); a link an extension adds to[Leftmenu_setup]shows under "More". - Icon, description and figure of a tile are settings of the menu's section, per link key, so an extension sets them
for its own link in its
menu.ini.append.php:LinkIcons[<key>](an icon of the newdesign.ini [AdminDesignSettings] IconList[], every.a4-bi-*mask rule ofcomponents.css; an unlisted name or none draws the plug icon),LinkDescriptions[<key>](translated in the menu's context, likeLinkNames[]) andLinkFigureTemplates[<key>](adesign:template included with the key). The core Setup links carry theirs insettings/menu.ini. An extension adds an icon of its own with its SVG, a mask rule in a stylesheet ofBackendCSSFileList[]andIconList[]=<name>. - Small figures where one cheap fetch gives them (
setup/setupmenu_figure.tpl): classes, sections, content languages, the active extensions that exist (expinfo( 'active' )), signed-in users; one and many are two strings. Only on a tile the user may open. - menu.ini
LeadLinks[]: links shown before the groups of a grouped left menu, in none of them.[Leftmenu_setup]starts withLinks[overview]=setup/menu("Overview"), its lead link, marked witharia-current="page"on the overview; the overview itself leaves it out. - The classic designs show a short page at
setup/menu(design/admin/templates/setup/setupmenu.tpl): the heading, a pointer to the Setup menu and thesetuptoolbar a site configures in toolbar.ini, as before. - 35 icon classes
.a4-bi-*in both copies ofcomponents.cssand 25 Bootstrap Icons 1.13.1 SVGs, unchanged (MIT), inshare/icons/bootstrap-icons/svg/. - New strings in design/admin/setup/setupmenu (title, subtitle, the figures) and in design/admin/parts/setup/menu ("Overview", one description per function), ger-DE and eng-US.
- Test:
AdminSetupOverviewTest(the tab's address and the lead link, every Setup link with an icon and a description in menu.ini, every icon listed with a rule and a Bootstrap file,IconList[]equal to the rules, no icon used twice, each grouped link once, the access checks before the tiles, cheap figures in the link branch only, no image but the icon masks, the grid and focus rules, the translations; the tiles of a fixture menu drawn by the template engine, with an extension's icon, description and figure, an unlisted icon falling back to the plug and a tile without access; who openssetup/menu, evaluated byeZUser::hasAccessToView()).AdminLeftMenuGroupsTestallows the lead links outside the groups.
Updated
[Topmenu_setup] URL[default]issetup/menuinstead ofsetup/cache.- The view
setup/menuopens with any one of the setup module's functions (setup,administrate,managecache,managecronjobs,preload,system_info) instead ofsetuponly, so a role that reached the Setup tab throughsetup/managecachekeeps it; the page shows only what the user may open. A user with none of them gets the access denied page, which names the last function asked (system_info).
Added: tabs into Setup
[Leftmenu_setup] GroupTabs[<group>]=<tab>: a group of the Setup menu and overview that stands for a top tab and is drawn only while that tab is not in the top menu (left out ofTabs[]or named inHiddenTabs[]); its pages then mark the Setup tab and draw the Setup menu (parts/a4_navigation_part.tpl, used bypage_topmenu.tpl,page_leftmenu.tpland admin4l's header and left menu blocks).GroupSections[<group>]=<section>adds the links of another left menu to the group with their names, access checks and tile settings; a Setup link to an address that menu has too shows there only. Feature.- Groups Design (
design_node, the Design tab's page, plus[Leftmenu_design]) and Audit ([Leftmenu_audit], its channel limits as in the audit menu) in[Leftmenu_setup], for the Design and Audit tabs. Every tab stays by default:HiddenTabs[]is empty. LinkIcons[]andLinkDescriptions[]in[Leftmenu_design]and[Leftmenu_audit]; six Bootstrap Icons 1.13.1 SVGs (archive, brush, exclamation-triangle, list-ul, speedometer, terminal) with their.a4-bi-*rules andIconList[]entries; the descriptions in design/admin/parts/design/menu and design/admin/parts/audit/menu, "Design", "Design content" and its description in design/admin/parts/setup/menu, ger-DE and eng-US.[TopAdminMenu] SetupTabs[](empty by default): tabs that leave the top menu only for a user who may opensetup/menu, who finds their group under Setup; a user who may not (a role with only the tab's own policy) keeps the tab and its menu. Read by the topmenu operator (eZTopMenuOperator::canOpenSetupOverview()), the Setup menu,parts/a4_navigation_part.tpland the logo. Test:eZTopMenuOperatorSetupTabsTest.- Without the Dashboard tab the logo leads to the dashboard for a user who may open it, instead of the content root.
- A page of a moved tab draws the Setup menu also when its view names a left menu of its own: the audit views
(
$module_result.left_menu,design:audit/menu.tpl) drew the audit menu beside the marked Setup tab. While the tab is in the top menu, the view's own menu stays (page_leftmenu.tpl, admin4l's left menu block). Test:AdminSetupTabsLeftMenuTest. parts/ini_menu_tiles.tplreads icon, description (with its context) and figure from each item, whichparts/ini_menu.tpltakes from the section the link comes from.- Test:
AdminSetupOverviewTest(the Design and Audit groups, their sections, pages, icons, descriptions and translations, no tab hidden by default, the navigation part drawn with the tabs shown and hidden, the logo);AdminLeftMenuGroupsTestallows a group whose links all come fromGroupSections[].
Upgrade notes
- To take Design, Audit or the Dashboard out of the top menu, list them in
[TopAdminMenu] HiddenTabs[]of the admin siteaccess and clear the INI and template caches. - A site that wants the Setup tab to open the cache page again sets
[Topmenu_setup] URL[default]=setup/cachein itsmenu.ini.append.php. A site that gaveLeftmenu_setupits ownLinks[]addsLinks[overview]=setup/menuandLeadLinks[]=overviewfor the overview's left menu entry. - Clear the INI, template and template-override caches after the update.
15 October 2026: the Healthy Eating landing page has its own subtitle
Fixed
- The Healthy Eating landing page showed the Fitness subtitle ("State of complete physical, mental, and social well-being"): the text block under its title was a copy of the Fitness one, as in the upstream reference. It now reads "Simple, nourishing food for an active life". The layout seed carries the new text in explayouts 1.4.25 and sevenx_themes_media 1.5.57 (one block parameter; no row renumbered), so a new installation shows it too.
Upgrading
- An existing installation keeps the old text until an editor changes the block in the layout editor (Landing Page / Healthy Eating, header zone).
15 October 2026: pausing cronjob parts
Added
- Setup > Cronjobs pauses a cronjob part, or every part, until someone resumes it.
runcronjobs.phpleaves a paused part out whoever starts it (the crontab,./console cron:<part>, the console), before it asks thecronjob/part/runfilter; a script run alone is left out while a part that lists it is paused. The cards show Paused, since when and by whom, a notice says so while every part is paused, the Pauses section lists who paused and resumed what (var/<vardir>/log/cronjob_pause.log), and a paused part cannot be run from the console. The state is a file per part (<cache>/cronjob_control/cronpart_deactivated_<part>.txt,1= paused), the layout extensions that paused parts already use. New:expCronjobPause,cronjob.ini [PauseSettings], Pausing cronjob parts. Test:expCronjobPauseTest.
Updated
- A part paused while it runs finishes the script that runs, and
runcronjobs.phpleaves out the scripts after it (it asks the pause again before each further script) instead of running the part to its end. - Every run a pause leaves out is written to
<log directory>/cronjob_skipped.logwith the part, the script and the siteaccess, besides the notice in the run's output. - The state file is written to a temporary file and renamed over the old one, and the state file and the history
get
StorageFilePermissions, so the command line or Velocity's workers (root) and the web server (the site user) can each replace what the other wrote. - Pauses and resumes are audited:
system.cronjob.pauseandsystem.cronjob.resume. - The feature page covers several servers (a shared
[PauseSettings] Directory) and what a pause does not hold back (the queue workers ofexpqueuethat run as a service).
Upgrading
- A part an extension paused through such a file (holding
1) is left out from now on; on Exponential those files had been read by nothing. Look at Setup > Cronjobs after the update and resume what should run, or setcronjob.ini [PauseSettings] Pause=disabled.
15 October 2026: a note with a cronjob pause, and the paused parts at a glance
Added
- Setup > Cronjobs has a field Note for a pause or resume (optional): the next Pause or Resume keeps it
with its line in
cronjob_pause.log(one line, at most 200 characters). The note shows on the paused part (compact row and card), in the notice of a pause of every part, in a Note column under Pauses, and in the audit record (after.noteofsystem.cronjob.pauseandsystem.cronjob.resume).expCronjobPause::pause()andresume()take it as a third argument; newexpCronjobPause::cleanNote()andNOTE_LENGTH. - The overview of Setup > Cronjobs has a Paused figure (every part while all are paused).
- The Cronjobs tile of the Setup overview says 1 part paused, n parts paused or All parts paused while a
pause holds:
menu.ini [Leftmenu_setup] LinkFigureTemplates[cronjobs]and the fetchfetch( 'setup', 'cronjob_pauses' )(enabled, all, count).
Updated
- The notice
runcronjobs.phpprints for a paused part says since when, by whom and why:... is paused (Setup > Cronjobs). Paused since 2026-10-15 13:05 CEST by admin: Index rebuild until noon.The same line goes tocronjob_skipped.log. NewexpCronjobPause::describe(). - Translations ger-DE and eng-US; Pausing cronjob parts. Tests:
expCronjobPauseTest,AdminSetupOverviewTest. - The history keeps who paused as the user id only; the console and the notice show the user's current login,
looked up by the id. The history is read again in each request, so a persistent Velocity worker shows a pause
another process made since. The Setup tile counts only the parts
cronjob.inidefines.
Upgrading
- Nothing to do. History lines written before have no note; a site that set its own
LinkFigureTemplates[]for the Setup menu addsLinkFigureTemplates[cronjobs]=design:setup/setupmenu_figure.tplfor the new figure.
15 October 2026: a documentation link for each cronjob script
Added
- Setup > Cronjobs shows Docs beside a script that names its documentation, in the compact list, on the part
cards and under the available scripts; it opens in a new window. A script names it with the
@linktag of its header (or of its class file, #207): a URL, or a path that is put aftercronjob.ini [ScriptDocumentation] DocumentationBase(empty: Exponential's documentation on GitHub). An installation gives or replaces it per script with[ScriptDocumentation] Link[<script>], by the file name;noneremoves it. Onlyhttpandhttpslinks are shown. NewexpCronjobRunner::scriptDocumentation()anddocumentationUrl();scriptDescription()reads the header as before. - The kernel's scripts
rssimport.php,session_gc.php,notification.php,workflow.php,mailpreferences.php,mailbounces.phpandquerycache_cleanup.phplink to their guide. Translations ger-DE and eng-US, the console's feature page and the cronjobs guide (section 11). Test:expCronjobScriptDocumentationTest.
Updated
- With the default
DocumentationBasea path is linked only when the installation ships that file, so a typo or an unpublished page never becomes a dead link; the relative path in the header of an extension's script is not put after Exponential's base (it names a file of that extension). A script that names nothing shows no Docs until[ScriptDocumentation] Link[]gives one.
15 October 2026: a compact view of the cronjob parts
Added
- Setup > Cronjobs lists the cronjob parts in a compact table by default: one row per part - the part with n
scripts, which opens them one per line with what each does in a row below it (several at once), its badges, the
schedule in words with the next run below it, the last run from here with its result, and Pause/Resume
and Run - on one page, wrapping to the window's width. Run and filter is folded, so the page opens on the
overview and the list. Detailed next to the heading shows the cards as before, paged. The choice is the user
preference
admin_cronjobs_view(compactorcards). The search and the Show filter work on the rows. Both administration designs; translations in ger-DE and eng-US. Test:expCronjobsCompactViewTest.
Updated
- Compact and Detailed are two toggle buttons (the shared segmented control) that write the preference by a
POST to
user/preferences/setwith the page's form token, as the other preference switches of the administration, instead of GET links; the current view is the pressed button.
15 October 2026: the node view's tab box keeps its height
The box of the node view's tabs (View, Details, ...) keeps the height the user drags it to, for every node. Feature ("Height of the node view's tab box").
Added
- admin4: a grip at the bottom right edge of the tab box (
parts/a4_tabs_height.tpl, innode/view/a4_full_body.tplandnode/top_node_full.tpl), a focusable separator for the mouse, touch and the keyboard (Arrow Up and Down 16px, Shift+Arrow, Page Up and Page Down 64px, Home and End, a double-click for the default size: the keys and steps of the left column's and the debug panel's grips), in place of the browser's resize grip, which neither the keyboard nor a touch screen can use. The height is held between 10em and 60em and at most 85% of the window; a phone (below 576px) keeps the default. A "Default size" button (iconarrow-counterclockwise, added toIconList[]) shows while the height is not the default and forgets it. - The eZPreference
admin_node_preview_height(pixels; one value for all nodes), written on<html>byparts/a4_preference_attributes.tploutside every cache-block and applied byadmin4.jsbefore the page is drawn. It is saved once at the end of a drag or 400ms after the last key, and only when it changed. Test:AdminNodeTabsHeightTest(its behaviour runs in node,tests/tests/design/js/tabs_height.js).
Updated
window.a4Pref(page_head_style.tpl): a preference that cannot be saved (nofetch, a network error, an error status, a redirect such as the login page of an expired session) is kept in the browser'slocalStorageinstead, which is read while the preference is empty;set( name, '' )also removes the browser's copy. The request is sent withkeepalive, so a change saved as the page is left still arrives.
15 October 2026: the Setup groups AI and Updates
Links that extensions add to the Setup menu no longer land unsorted under "More": the left menu and the Setup overview have two more groups, AI and Updates, and "More" stays the last group for links that name no group yet. Feature.
Added
[Leftmenu_setup]ofsettings/menu.ini:Groups[]=ai("AI") andGroups[]=updates("Updates") after System. AI holds the links of expai (expai_summary,expai_models,expai_budgets), Updates those of ezupdate (ezupdate) and git_manager (git_manager,git_manager_backup). The job queue (expqueue) and the database editor of sevenx_dse (dse) go to System. A key whose extension is not active shows nothing, and a group without a link the user may open is not drawn, so a site without these extensions sees the Setup menu as before.- Icons and descriptions for these eight links (
LinkIcons[],LinkDescriptions[]):stars,cpu,piggy-bank,list-task,database,cloud-arrow-down,git,archive, eight Bootstrap Icons 1.13.1 SVGs, unchanged (MIT), inshare/icons/bootstrap-icons/svg/, with their.a4-bi-*rules in both copies ofcomponents.cssand indesign.ini [AdminDesignSettings] IconList[]. - New strings in design/admin/parts/setup/menu: the group names "AI" and "Updates" and the eight descriptions,
ger-DE and eng-US (the group names also in
untranslated). - An extension sorts its own Setup link into a group with
GroupLinks_<group>[]=<key>in itsmenu.ini.append.php, e.g.GroupLinks_ai[]orGroupLinks_updates[]; documented in the feature page ("Extending it"). - Tests:
AdminLeftMenuGroupsTestchecks the group order (the five core groups, then AI and Updates, "More" built last), that AI and Updates hold only extension links that core does not ship itself, and draws the Setup menu with the template engine and the real groups for links of active extensions: only groups with a link appear, in their order, "More" last with the link of an extension that names no group, the overview before the groups; the same as tiles with the new icons; and with no extension link the AI, Updates and More groups are gone.AdminSetupOverviewTestrequires an icon and a description for every grouped link, an extension's too.
Upgrade notes
- A site that reset
Groups[]for its own grouping ofLeftmenu_setupkeeps it; to have the new groups, addGroups[]=aiandGroups[]=updatesthere. Clear the INI and template caches after the update.
15 October 2026: the user preferences of admin4 show on and off as tabs
Updated
- admin4: the User preferences block of the right column (Locations, Re-edit) draws "on" and "off" as the design's
tabs (
ul.tabs, like the node view's tabs) instead of a segmented button switch with a filled part: the current state in the accent tint with a bar under it, the other state quiet. Each choice is a group named after its preference, the tabs are at least 24px high, and under forced colors an outline marks the current tab of everyul.tabs(the tint and the bar are dropped there). Styling rules;AdminLegacyControlsTest. - admin4: the two states are the buttons of a POST form to
user/preferences/set, as the left column's width switch, instead of GET links: a preference is written only from the site itself (with the form token), and the current state is the pressed toggle button (aria-pressed="true"), which a screen reader announces as such. The template draws both preferences from one list.
Upgrade notes
- A site design that overrides
toolbar/full/admin_preferences.tplfor admin4 keeps its own markup; to get the tabs, copy the new markup. Clear the template cache after the update.
15 October 2026: "Open the site" among the header's tools
Changed
- admin4's "Open the site" link (
box-arrow-up-right) left the logo for the header's tools: it is the first of them, before light/dark, the user menu and logout, and a round 36 px icon button as they are (.a4-header-btn: border, hover, focus ring, light and dark). The header reads logo | menu | search | site | light/dark | user menu | logout from 1280 px; from 576 px the tools head the first row's end, and on a phone the row is logo | site | light/dark | user menu | logout | menu button. The header keeps its heights (56, 88 and 56 px). The tools stand in the markup in the order they are drawn, so the Tab key goes search, site, light/dark, user menu, logout (the main menu and the phone menu button keep their place before them in the markup, as before). Name, tooltip and address are unchanged ("Open the site",siteaccess_url()), and it opens in the same window; on edit pages it stays a link, as before. - The link is
#header-site > a.site-preview.a4-header-btninpage_topmenu.tpl, once instead of once per branch of the edit check. admin4l: it moved from theadmin_logoblock to theadmin_theme_switchblock, before the light/dark switch. The header's cache blocks keep their keys (siteaccess_url()was already one of them). - Removed: the
.site-previewrules of admin4'sadmin4.cssandpagelayout.css(its 24 px icon beside the logo). - Test:
AdminHeaderTest::testTheSiteLinkIsTheFirstOfTheTools(markup order in both designs, the logo without the link, the cache keys, the CSS order and the phone row);AdminLandmarksTestcounts the one link.
Upgrade notes
- A site design that overrides
page_topmenu.tplor admin4l'sadmin_logo.tpl/admin_theme_switch.tplkeeps the link where its own copy has it; to follow, move the#header-siteblock before#header-theme. CSS of your own for.header-logo .site-previewno longer matches anything. An admin4l layout without theadmin_theme_switchblock has no "Open the site" link any more; add the block to show it. Clear the template cache after the update.
15 October 2026: the online editor's engine switch is one control
Updated
- ezoe's engine switch below a rich text field (
EngineSwitch=enabled) is one control instead of one button per other engine: a group named "Editor" with every available engine in the order ofEngines[], the engine in use marked as the pressed one (see below), every other engine a button. The buttons post the same custom action as before (CustomActionButton[<attribute id>_switch_engine_<engine>]): the draft is stored with the text typed so far, then the choice is saved as the user preferenceezoe_engine; their title still says "Switch to the editor ...". admin4 draws the group as a segmented control at the control height (--a4-control-h), left-aligned under the field and apart from the editor's status bar, under its label on a narrow phone; other designs show the label, the engine in use and the buttons. New: the input handler's template attributeengine_switch_optionsandexpOEEditor::switchOptions()(every engine with acurrentflag);engine_switch_choicesstays. Tests:expOEEngineConfigTest,expOEEditorRegistryTest,AdminContentEditFormsTest. - The engine switch is a segmented control of toggle buttons, as admin4's width switch: the engine in use is the
pressed button (
aria-pressed="true",type="button", so pressing it again posts nothing) instead of a span, so it stays in the Tab order and a screen reader names the group, the engine and its state; the other engines are not pressed. Each button's name is the engine's visible name; the title says that it switches and that the text is kept. Hovering the engine in use no longer draws the hover edge.
Upgrade notes
- A design of your own that overrides
content/datatype/edit/ezxmltext_ezoe_engine_switch.tplkeeps working; to take the new control, drop the override or build it onengine_switch_options. A script or test that looked for the switch asinput[type=submit]finds abuttonwith the samenamenow.
15 October 2026: further findings of an end-to-end test of the administration
Fixed
- The sub-items table speaks the admin's language: the table options list the columns' groups and descriptions
translated like their names (
expSubitemsColumn::group()anddescription(); an attribute column's group is "Attributes: <class>" with the class's own name, its description stays the identifiers), and every column name, group and description ofsettings/subitemscolumns.iniand the built-in columns, the units of the relative times ("vor 2 Minuten", "gerade eben") and Yes/No are in German and American English (263 texts in the contextdesign/admin/node/view/full). The remove confirmation counts sub items in German ("3 Elemente"). Test:AdminSubitemsTranslationsTest. - The language choice of "Create new" has a page title (Content / Language selection, "Sprachauswahl") instead of
"- Exponential Admin", and the tab of the place the object is created in. Test:
AdminCreateLanguagesTitleTest. - admin4's phone menu (below 576px): Escape closes the open menu, sets
aria-expandedto false and gives the focus back to the menu button; an open search scope popup in the menu takes the first Escape. The open menu ends under its search field (admin3's 7rem under the list left about 80px of empty panel), and the field takes the panel's width. From 576px the header's search field is 166px wide at 576px (106px before) and 230px from 640px. Test:AdminPhoneMenuTest. - TinyMCE 8's toolbar, menus and dialogs follow admin4's dark mode like the editor's content: an editor made on a
dark page starts with the skin
oxide-dark, and a change of the mode swaps the loaded oxide stylesheets; a skin a site configured is kept, TinyMCE 3 is unchanged. Test:AdminEditorSkinTest.
Updated
- The top node of the content tree (node 1) is called "Content" ("Inhalte" in German) instead of "Top Level Nodes"
("Knoten der obersten Ebene"): in the content structure menu, the browse dialogs of the online editor (TinyMCE 3
and 8) and the top node's page, whose note now says that the content below it is listed under Sub items. The
source text stays
Top Level Nodes(contextkernel/content); a site without a translation (eng-GB) still shows it. Test:AdminTopNodeNameTest.
15 October 2026: publishing queues index_object again
Fixed
- Publishing a version no longer queues a reindex of the whole subtree. The step
register-search-objectof the publish operation had no parameters of its own, so it got all of the operation's parameters by position; since the publish operation takesnotify(6 October),notify(true by default) landed inregisterSearchObject()'s$isMoved. Withsite.ini [SearchSettings] DelayedIndexing=enabled(orclassbasedfor a listed class) every publication was queued asindex_moved_nodeinstead ofindex_object, and the indexing cronjob (indexcontent) reindexed the object's entire subtree each time. Moving an object to the trash removes only itsindex_objectentries, so these entries stayed until the cronjob had run them. The step now takesobject_idandversion, like its neighbours. Without delayed indexing nothing changed: the object is indexed at once, and$isMovedis read only to choose the pending action. Test:eZPublishRegisterSearchObjectTest(the step's parameters, the pending action through the operation's parameter mapping withnotifytrue, false and missing, no other publish step taking more than object and version without parameters of its own, a move still telling the search engine it is one).
Updated
- A publication that changes the object's parent (a location moved in the edit form) is a move for the search
engine, as content/move is: an engine that implements
addNodeAssignment()(ezfind) hears of it with$isMovedtrue, so with delayed indexing it queues oneindex_moved_nodeentry and the paths of the moved subtree are reindexed; before, only the object was. With the kernel'sezsearch, whose index holds no paths, nothing changes (oneindex_objectentry).publishNode()marks the version when it moves the node, and the stepregister-search-objectuses the mark once. - The entries
registerSearchObject()queues inezpending_actionscarry the time they were queued (created), with SQL and MongoDB, so the cronjobindexcontenttakes the oldest first and a row tells when it came. - An object that goes to the trash loses its queued
index_moved_nodeentries as well as itsindex_objectones. - What each operation queues with delayed indexing, for both kinds of engine, is a table in After installing, Search.
- Test:
eZPendingIndexActionsTest(without the database): the queued row and its time (SQL and MongoDB), the moved publication with both kinds of engine and its mark per version, a real move, a new location, the trash, the cronjob, and every method step of every kernel operation that inherits the operation's parameters filling only the optional method parameters it is meant to (delete, swap, translationavailable, sort, updatepriority, user activation; a loop's children take the loop'schild_parameters).
Upgrading
- An installation with delayed indexing that ran the code of 6 to 15 October 2026 may hold
index_moved_nodeentries made by publications. List them withSELECT id, param FROM ezpending_actions WHERE action = 'index_moved_node' ORDER BY id;. Those entries carry no time (createdstayed empty until this fix), so the row alone does not tell a publication from a move. Where the search engine does not implementaddNodeAssignment()with$isMoved(the kernel'sezsearchdoes not), no move queuesindex_moved_node: every such entry comes from a publication, andUPDATE ezpending_actions SET action = 'index_object' WHERE action = 'index_moved_node';turns them into the intended reindex of the object alone. Otherwise leave them: the cronjob processes them correctly, at the cost of the subtree.
15 October 2026: the segmented switch is one pill
Changed
- admin4's segmented switch (
.a4-tabs.is-segmented,components.css) is one pill instead of three square boxes with lines between them: one track (a 1px edge in--a4-line-strong,--a4-softinside,border-radius: 999px) around every choice, each choice a pill inside it. The current choice is tinted in the accent (--a4-accent-softover--a4-card), with an edge and text in--a4-accent-text(4.8:1 light, 6.1:1 dark) instead of the solid accent fill. The switch stays as high as the other controls (--a4-control-h, 32 px); each choice is 26 px high, round icon buttons (.is-icons) 34 px wide. Every choice has its own focus ring (--a4-ring, 3 px), drawn over its neighbours. Under forced colors the track is drawn inCanvasText, the current choice inHighlight, and the focus ring stays outside the choice inCanvasText. - The current choice is found by
aria-pressed="true"(buttons) orspan.current,a.current,.is-current(links, witharia-current), so a switch of links shows its current state the same way. - It is seen at the bottom of every left menu (the width of the left column: narrow, medium, wide,
parts/left_width_control.tpl), in the version preview's device switch and in the online editor's engine switch (below). The user preferences of the right column are the design's tabs (ul.tabs) since the same day. The markup of the width switch is unchanged. - Every segmented switch answers the arrow keys (
admin4.js): Left and Right move the focus to the previous and next choice (round, a disabled one skipped), Home and End to the first and last; each choice keeps its Tab stop and is pressed with Enter or Space, Up and Down keep scrolling the page. - ezoe's engine switch below a rich text field is the same pill: the engines' span carries
a4-tabs is-segmented, socomponents.cssdraws it in every administration design (track, the engine in use tinted with an accent edge, focus ring, forced colors) instead of the joined segments with the engine in use filled in--a4-accent-strong;modules/content.cssonly places it and keeps the pill's hover. Tests:AdminContentEditFormsTest,AdminLegacyControlsTest::testTheSegmentedSwitchesAnswerTheArrowKeys(a node harness,tests/tests/design/js/segmented_switch.js). design/admin/stylesheets/components.cssstays a copy of admin4's.- Test:
AdminLegacyControlsTest::testTheSegmentedSwitchIsOnePill(track, round choices, height, tint, focus ring, forced colors, both copies identical).
Upgrade notes
- CSS of your own that relied on the joined boxes (
border-radiusof:first-child/:last-child, the-1pxmargin) no longer has anything to adjust. A site design that wants the old filled current choice setsbackground-color: var(--a4-accent-strong); color: var(--a4-on-accent)on.a4-tabs.is-segmented > [aria-pressed="true"]in its own stylesheet. Clear the stylesheet cache after the update (var/<vardir>/cache/public/stylesheets).
15 October 2026: the version preview in the width of a device
Added
- admin4's version preview (content/versionview: "Preview" from the edit form, the node view and the version history)
shows the version in the width of a device: a segmented pill iPhone (390 × 844 px) | iPad (820 × 1180) | Desktop
(1280 × 800) | Large Desktop (1920 × 1080) above the preview, each a button with its icon (
phone,tablet,laptop,display), its name andaria-pressed, and a landscape switch (phone-flip) for phone and tablet, disabled for the desktops. The iframe is drawn at the device's size and scaled down when the content column is narrower (never up); beside the switches the size and the scale are shown ("820 × 1180 px, shown at 74 %"), and a change of the column's width redraws it. It still askscontent/versionview/.../site_access/<siteaccess>, so it shows the site's own rendering for the siteaccess chosen in the left column. - The choice is the signed-in user's: the eZPreferences
admin_preview_device(phone,tablet,desktop,large) andadmin_preview_landscape(1,0), saved throughwindow.a4Prefand written on<html>byparts/a4_preference_attributes.tpllike admin4's other remembered choices, so it follows the user to another browser; an anonymous page keeps it in the browser (exp-admin4-preview-device,exp-admin4-preview-landscape), and a choice stored there is taken along once. Without one the preview opens as Desktop, scaled to the column (before, the site was drawn in the column's own width). The script is part ofadmin4.js(packed with the other admin4 scripts); the template holds no script. The arrow keys move between the devices. A stored landscape choice is kept while a desktop is chosen. In a column narrower than 640 px the switches show their icons only, the names stay for screen readers and in the titles. Light and dark mode come from the tokens; under forced colors the frame has aCanvasTextedge. - Without JavaScript the switches stay hidden (
hidden) and the iframe fills the column at 800 px, as before. - New Bootstrap Icons 1.13.1 files
tablet.svg,laptop.svg,display.svgandphone-flip.svg(unchanged), with their.a4-bi-*mask rules incomponents.css(both copies) andIconList[]entries indesign.ini. The styles are inmodules/content.css(.a4-content-preview-*, both copies). New texts are translated to German. - Test:
AdminPreviewDevicesTest(the pill and its sizes, the iframe's address and fallback, the script run in node: the saved and restored preferences, landscape, the scale, the anonymous fallback; the stylesheet in both copies, the icons, the translations);AdminPreferencesTestknows the two preferences.
Upgrade notes
- A site design that overrides
content/view/versionview.tplkeeps its own preview. The classicdesign/admintemplate is not changed; only its copy ofmodules/content.csscarries the rules. Clear the template, stylesheet and translation caches after the update.
15 October 2026: the site links show a globe
Changed
- admin4's links to the site show the globe (Bootstrap Icons
globe2) instead of the arrow out of a box (box-arrow-up-right), which reads as "open somewhere else" and is the icon of external links: the header's "Open the site" (admin4'spage_topmenu.tpland admin4l'sadmin_theme_switchblock) and the node view's "View on site" (node/view/a4_full_body.tpl, beside Preview and the versions). Names, tooltips, addresses and targets are unchanged. - New mask rule
.a4-bi-globe2incomponents.css(both copies) andIconList[]=globe2indesign.ini[AdminDesignSettings];globe2.svgwas already part of the theme..a4-bi-box-arrow-up-rightstays for other uses. - Test:
AdminIconsTest::testEveryLinkToTheSiteShowsTheGlobe(the three templates, the mask rule in both copies, the icon list, the unchanged SVG).
Upgrade notes
- A site design that overrides
page_topmenu.tpl, admin4l'sadmin_theme_switch.tplornode/view/a4_full_body.tplkeeps the arrow until its copy takesa4-bi-globe2. Clear the template and stylesheet caches after the update.
15 October 2026: SVG images, sanitized on upload
Added
- The image datatype accepts SVG images (
.svgand gzip-compressed.svgz):image/svg+xmlis onimage.ini [ImageUploadSettings] AllowedMIMETypes[], and the new settingSanitizeSVG=enabled(the default) stores every SVG as the output of the kernel's SVG sanitizer,expSvgSanitizer(lib/ezimage/classes/). It is allow-list based and parses withDOMDocumentwithout network, entity substitution or DTD loading: only SVG elements and attributes known to be safe are kept;<script>,<foreignObject>, event attributes, references other than internal#fragments(excepthttp(s)/mailtolinks and embedded PNG, JPEG, GIF or WebP images),javascript:anddata:URLs, externalurl(),@importandexpression()in CSS, and animations ofhref,styleor event attributes are removed. A DOCTYPE with entities (XXE, billion laughs), UTF-16 input, a file over 5 MB (also after decompression), 50 000 nodes or 128 levels, and a file in which nothing drawable remains are refused.SanitizeSVG=disabledrefuses SVG, so an SVG is never stored unsanitized. The edit form, the upload module and multi-upload, the rich text editor's upload, the AJAX uploader, WebDAV and imports all go through it; the package thumbnails stay raster images. Page: SVG images. - Every alias of an SVG is the original file, with the width and height from its
width/heightattributes or itsviewBox; an SVG is never handed to GD or ImageMagick. - SVG files are served with
Content-Security-Policy: sandbox; default-src 'none'; style-src 'unsafe-inline'andX-Content-Type-Options: nosniff: by.htaccess_root,content/download, the cluster gateway and Exponential Velocity (0.0.4.53). A virtual host configuration needs the lines of the page above. - Tests:
expSvgSanitizerTest(86 harmful SVGs, 15 real icons and drawings that render the same) andexpImageSvgTest.
Fixed
expSvgSanitizerchecks every piece of a<style>element a browser can read as a declaration, not only the innermost rule blocks: a declaration next to a nested rule, in a last block left open or before the first rule could keep an externalurl(). CSS functions that load an image from a plain string (image-set(),-webkit-image-set(),image(),cross-fade(),element(),src()) andattr()are removed like an externalurl(), and aurl()is a fragment only when it starts with#as written, not after decoding character references. An<a>link must start withhttp(s)://ormailto:as written.expSvgSanitizer::isSVGFile()recognises an SVG behind a DOCTYPE with>in its system identifier, a processing instruction with>in its data, and a prolog longer than 64 kB; the prolog is matched without backtracking, and a prolog the pattern cannot finish counts as SVG, so the sanitizer decides.eZImageAliasHandlerdoes not store a file as SVG (by its name or the type the browser sent) that the content check does not recognise as SVG, so it is never stored unsanitized; this closes the package import's path, which has no check before storing. New:eZImageAliasHandler::isUnsanitizedSVG().- The serving headers match
.svgand.svgzin any case, in.htaccess_rootand the cluster gateway. - Labels and titles of an SVG (
aria-*,title,xlink:title) keep ordinary text such as "Image (2)" or "metadata:": only a script or data URL at the start of a word removes them, since such text is never loaded. The test corpus covers further style sheet nesting, escaped and upper case image functions and character references.
Upgrade notes
- SVG uploads are accepted after the upgrade. To keep refusing them, set
SanitizeSVG=disabled(or removeimage/svg+xmlfromAllowedMIMETypes[]) in an override. Behind a virtual host instead of.htaccess, add the serving headers of the page above.
15 October 2026: the order of the Dashboard menu
Added
- menu.ini
TrailLinks[]: links of a left menu drawn after all its other links, in the order named, also after the links extensions append and, in a grouped menu, after the groups and "More" in no group (.a4-navtrail). Read by every copy ofparts/ini_menu.tpl(admin4, admin, adminui); the access checks are unchanged. In the tiles they are the last tiles. Test:AdminLeftMenuGroupsTest.
Updated
- The Dashboard tab's left menu (
[Leftmenu_my]) starts with Dashboard and ends with Change password and Edit profile (TrailLinks[]=change_password,TrailLinks[]=edit_profile); Audit trail is the last link of the kernel, right before the links of extensions such as Exponential Intelligence. Order: Dashboard, Collaboration, My pending items, My drafts, My bookmarks, My e-mail preferences, My notification settings, Audit trail, the extensions' links, Change password, Edit profile. - admin4's tiles of a menu without groups showed no tile: the list of lead links had no value when the menu had no
LeadLinks[], and every link counted as one.
15 October 2026: the user menu links to the user's own object
Added
- The name at the top of admin4's user menu (display name and login) is a link to the full view of the signed-in
user's own object, its main node's URL alias (
content/view/full/<main node id>for a node without one), as the right column's "Current user" box links the name. It is drawn only when the user's object has a main node and the user may read it (main_node.can_read); without either, and on edit and browse pages, where the menu's links lead nowhere, the name stays text as before. The link is a plain link of the disclosure (no menu role) and the first stop of its arrow keys; it is named by the name and login it shows, with "Open your user object" as its tooltip (title) and description (aria-describedby). Hover and focus ring as the menu's other links, light and dark; in forced colors the menu's links are underlined on hover. No new icon. The markup is inparts/a4_header_user.tpl, so admin4l (admin_theme_switch.tpl) has it too. - The header's cache blocks keep their keys: they are keyed by the page context, the role hash (
$user_hash) and the user's object id, so one user's link is never served to another. - New string: "Open your user object" (design/admin/pagelayout), ger-DE "Eigenes Benutzerobjekt öffnen" and eng-US.
- Test:
AdminHeaderTest::testTheNameLinksToTheUsersOwnObjectrenders the template with the real template engine (no database): the link and its target, the text without read access, without a main node and on edit and browse pages, two users with their own nodes, the cache keys of both designs, the CSS and the translations.
Upgrade notes
- A site design that overrides
parts/a4_header_user.tplkeeps its own copy. Clear the template cache after the update. Moving a user's object to another main node, or a change of its section or state that changes who may read it, shows in the header once its cache block expires; a change of roles or their limitations shows at once.
15 October 2026: visibility and account status at a glance
Added
- admin4 marks a node's visibility with a small status mark whose shape tells the state as well as its color: a
green filled disc for visible, a red ring for hidden, a red ring with a bar for hidden by an item above it
(
is_invisiblewithoutis_hidden); a user account that is disabled gets a gray crossed ring and "Disabled", one locked after too many failed logins an amber square and "Locked". The marks are one component,.a4-markin both copies ofcomponents.css, at 3:1 or more in light and dark mode, with their shapes in forced colors. They show in the node view's header (a badge, replacing the "Hidden" and "Hidden by a parent" badges), before every name of the sub-items table (and beside the text of its Visibility column), after hidden nodes and disabled accounts in the content tree, and before hidden nodes of the browse list. Page: admin4, Visibility and account status. expUserAccountStatusreads the account status of a page of objects with at most two queries; the rows ofezjscnode::subtreeandexpsubitems::rowscarryis_hidden,is_invisibleand, for user accounts,user_status; the tree menu's answer carriesuser_statusfor a disabled account; templates havefetch( 'user', 'account_status', hash( 'user_id', ... ) ). The status is shown to users withrole/read, like the sub-items columns "Account enabled" and "Locked out".- Tests:
AdminVisibilityStatusTest,expUserAccountStatusTest.
Updated
- Enabling or disabling a user account (user/setting, activation) marks the user's locations as modified, so the
content tree, which the browser keeps by
modified_subnode, shows the change. - admin4's content tree keeps the "(Hidden)" and "(Hidden by superior)" text after a node's name for screen readers
(visually hidden instead of
display: none); the status mark before it is decorative. - German: every "hidden by a parent" text reads "Versteckt durch übergeordneten Eintrag", next to "Versteckt": the node view's badge ("Hidden by a parent", was "Durch übergeordneten Knoten verborgen"), the sub-items table's Visibility column and the tree ("Hidden by superior", was "Vom übergeordneten Element versteckt"), the tree's "(Hidden by parent)", the bookmarks and "Hidden by its parent". Only the translations changed, not the sources.
- The classic admin design shares the sub-items script,
children_detailed.tpl, the tree script and template andcomponents.csswith admin4, so it shows the marks too. - The mark is a class of its own,
.a4-mark: the pull request named it.a4-status, the class of the status with a dot and its text (Setup > Cronjobs, Preload, the workflow processes), which would have drawn those as 10 px rings.AdminVisibilityStatusTestkeeps the two apart.
Upgrade notes
- A design of your own that overrides
node/view/a4_full_body.tpl,children_detailed.tpl,contentstructuremenu/show_content_structure.tplorcontent/browse_mode_list.tplkeeps its markup; take the includedesign:parts/visibility_status.tplor theSTATUSlabels to follow. CSS of your own for.a4-badge-warn(admin4.css) no longer matches anything, and.a4-subitem-name > a:first-childis nowa:first-of-type. Clear the template cache after the update.
15 October 2026: the new Exponential logo in admin4
Updated
- admin4 and admin4l show the new logo artwork of the project in the header and on the sign-in page, drawn inline from
one template,
parts/a4_logo.tpl: the letters and outlines take the text color of the theme (dark on light, light on dark, CanvasText under forced colors), the orange square keeps the brand color#f15a28(the--a4-brandtoken, light and dark), and the inner square of the back mark takes the surface color. Until now the header paintedimages/admin4/logo-light.pngin light mode and admin3's white sprite in dark mode, and the sign-in page did the same with its own rules. - The header logo is 24px high and 152px wide in the 32px box the old image had; the header keeps its heights (56px,
88px from 576px, 56px from 1280px). Below 576px the header shows the mark alone, in a 36px link like the tools:
with the word the phone row left its tools 8px apart and narrowed the menu button. From 576px the search field gives the logo its 44px
(
clamp(6rem, calc(100vw - 454px), 230px), was 410px), so the tools keep one row at 576px. - The logo links, their targets and names are unchanged; the drawings are
aria-hidden. - New files:
design/admin4/images/admin4/exponential-logo.svg(word and mark, cut to the drawing, no background) andexponential-mark.svg(the mark, square), the same drawing as the template. The--a4-logotoken is gone.logo-light.pngstays: the old admin design and the kernel's error page still use it. - The word is drawn as paths, as high as the mark: the E and the two squares share top and baseline (viewBox
6 6 380 60). The raster logos (images/admin4/logo-light.png,images/setup/2/Exponential-logo-title.png,images/Exponential-logo.gifin the standard design) are re-rendered from the SVG, sizes adjusted to the word. Each is twice the size it is drawn at, so it stays sharp at 200% zoom and on high density screens:logo-light.pngand the setup wizard'sExponential-logo-title.png(the same file) 400 x 100 with the drawing at its own scale in the middle (7 KB, was 4 KB with the old, smaller word),Exponential-logo.gif420 x 120 for its 210 x 60 on the standard login page (was 210 x 60). AdminUI's white logo (exp_adminui 1.0.0.26) comes from the same drawing.AdminLogoTestLG-07 (the E and the l share the squares' top and baseline) and LG-08 (the sizes, half the size where they are drawn, the setup copy identical). - Required packages: exp_adminui ~1.0.0.26 (the white logo at 520 x 82 for its 260 x 41, and the colors of
xrowextract's pages in AdminUI from the
--a4-mutedtoken). - Tests:
AdminLogoTest(new: one source, the names, plain SVG without script, style or references, the files equal to the template, the colors, the header heights);AdminPhoneMenuTestPM-03 takes the new search field width;AdminIconsTestlets the logo be the one inline SVG of admin4's templates.
Upgrade notes
- A site design that overrides
page_topmenu.tpl,loginpagelayout.tplor admin4l'sadmin_logo.tplkeeps an empty logo link until its copy includesdesign:parts/a4_logo.tplinside the link. A stylesheet that set--a4-logohas no effect any more. Clear the template and stylesheet caches after the update.
15 October 2026: the environment badge of the administration
admin4 and admin4l can name the installation's environment (development, test, staging, production) in the header, on the sign-in page and before the page title, so a live site is not taken for a test copy. Off by default. Feature ("Environment badge").
Added
site.ini [AdminEnvironment]:Level(development,test,staging,production, orautofor the level ofEXP_ENV; empty, the default, shows nothing),Name,ShortName,Release,ShowVersion(disabled) andShowInTitle(enabled).- The template operator
exp_environment_badge()(expEnvironmentOperator, besideexp_environment()) and the includeparts/a4_environment_badge.tpl: a pill in the level's color beside the logo (admin4'spage_topmenu.tpl, admin4l's logo block,loginpagelayout.tpl). From 768px the name, and for signed-in users the release and version; up to 767px, phones included, the short code (never the color alone), the name read out. The header keeps its heights (56, 88, 56px). Color tokens--a4-env-<level>,-inkand-lineinadmin4.css, light and dark; forced colors keep the text. German and American English for the nine new strings. Test:AdminEnvironmentBadgeTest.
Updated
design/standard/templates/page_head.tpltakes an optionaltitle_prefix(empty by default); admin4, admin4l and admin4's sign-in page pass "[NAME] " whenShowInTitleis on.- The cache-blocks of admin4's and admin4l's head and header and of the sign-in page's head are keyed by the badge, so a changed setting shows on the next request. The key list changes, so these blocks are rendered anew once after the upgrade.
Upgrade notes
- Nothing changes without
Level. To use it, set[AdminEnvironment] Levelper environment, for example insettings/override/site.ini.prod.append.php(EXP_ENV=prod) or withLevel=auto. No template cache needs clearing; whereEZP_INI_FILEMTIME_CHECKis off, clear the INI cache. - A site design that overrides
page_head.tplgets no prefix unless it prints{$title_prefix|wash}itself.
15 October 2026: the environment badge shows on hover
The environment badge of admin4 and admin4l no longer stands in the header at all times: by default it shows only while a mouse or trackpad pointer rests on the logo, or the logo has the keyboard focus. Touch screens, which cannot hover, keep it beside the logo. The page title keeps its prefix. Feature ("Environment badge").
Added
site.ini [AdminEnvironment] BadgeDisplay:hover(the default) oralways, the badge beside the logo as before.exp_environment_badge()returns it asdisplay, and it is part of the badge's cache key.
Updated
parts/a4_environment_badge.tpl: the badge carriesid="a4-env-badge"and the classa4-env-hoverora4-env-always; the title attribute only withalways. The logo links of admin4'spage_topmenu.tpl, admin4l's logo block andloginpagelayout.tplname it inaria-describedby, so screen readers read the environment with the logo.admin4.css: withhoverthe badge is out of the flow and clipped to nothing; inside@media (hover: hover) and (pointer: fine)it appears on:hoverand:focus-visibleof the logo link, beside the logo from 576 to 1279px and on the sign-in page, under it from 1280px and below 576px, and stays while the pointer is on it. A 120ms fade, none with reduced motion. The rules that made room for the badge (the logo mark from 576 to 767px, the search field's width, the short code) apply toalwaysonly. The header keeps 56, 88 and 56px. Tests:AdminEnvironmentBadgeTest,AdminLogoTest.- The hover badge meets WCAG 1.4.13: Escape hides it until the pointer or the focus comes back (
admin4.js, the classa4-env-dismissed; an open menu's Escape comes first), and the pointer can move onto it. Everything that hides it sits inside@media (hover: hover) and (pointer: fine): on a touch screen, a stylus or any pointer that cannot hover finely, the badge stands beside the logo as withalways, with its room and the short code up to 767px, so the information never depends on hovering.AdminEnvironmentBadgeTestchecks that the touch rules equal those ofalwaysand that nothing outside the two pointer media queries names the hover badge.
Upgrade notes
- To keep the badge in view, set
[AdminEnvironment] BadgeDisplay=always. A site design that overridespage_topmenu.tpl,loginpagelayout.tplor admin4l'sadmin_logo.tplkeeps working; addaria-describedby="a4-env-badge"to its logo link so screen readers keep the environment.
15 October 2026: a mega menu for the Setup tab (prototype, opt-in)
Added
- admin4 (prototype, opt-in): a mega menu for the Setup tab. A small button beside the tab (the tab stays a link)
opens a card under the header with a short choice of the Setup menu's links in their groups, each with its icon,
name and description, up to three groups side by side, and "All Setup functions" for the rest. The choice is
menu.ini [Leftmenu_setup] MegaMenuLinks[](link keys, in order, at most 12 shown; the default names twelve frequent ones, among them the AI and Updates links, shown when their extensions are active), with the menu's groups and access checks. The links a moved tab brings (SetupTabs[],GroupSections[]) are chosen as<group>-<key>(audit-console); a chosen Setup link hidden because its group has the same address takes that link (audit→ the Audit group's Settings). The WAI-ARIA disclosure navigation pattern:aria-expanded/aria-controls, Enter, Space and ArrowDown open it, Tab goes through it, Escape closes it and gives the focus back, a click outside closes it. A mouse resting on the tab for 300 ms opens a preview that closes 400 ms after leaving; never on touch. From 1280px only; the two-row header and the phone menu are unchanged. Switched on withmenu.ini [TopAdminMenu] MegaMenuTabs[]=setup; empty by default, and then nothing changes. admin4 design, "Mega menu". - The view
setup/megamenu: the Setup menu's groups and links as an HTML fragment without page layout, drawn frommenu.ini [Leftmenu_setup]byparts/ini_menu.tpl(new parametertiles_layout='mega', templateparts/a4_mega_menu.tpl), with the access checks of the menu and the functions ofsetup/menu. The header loads it once, when the panel is first opened (design/admin4/javascript/a4_mega_menu.js, loaded only whileMegaMenuTabs[]names a tab), so the page grows by the button only. - Test:
AdminMegaMenuTest(with a node harness for the script,tests/tests/design/js/mega_menu.js).
Upgrade notes
- Nothing to do while
MegaMenuTabs[]stays empty. To try it, setMegaMenuTabs[]=setupand clear the INI and template caches. A site design that overridespage_topmenu.tplorpage_topmenuitem.tplfor admin4 draws no button until it takes the new markup. Opening the panel requests a setup view, which the audit trail records asaccess.view.sensitive.
15 October 2026: the mega menu for the Users tab, one view for every tab (prototype, opt-in)
Added
- admin4 (prototype, opt-in): a mega menu for the Users tab, switched on with
menu.ini [TopAdminMenu] MegaMenuTabs[]=users: Users and user groups, Sessions | Roles and policies, Unactivated users | Edit profile, Change password, My notification settings, My e-mail preferences, in three groups, and "Go to Users". Its links come frommenu.ini [Leftmenu_user_mega], a section that gathers the tab's functions without restating a link:GroupSections[]adds[Leftmenu_user](the tab's left menu, unchanged) and[Leftmenu_my](the user's own settings) with their addresses and policies; its own links are the user tree and the sessions. The choice is itsMegaMenuLinks[]. Iconsshield-lockandperson-slash(Bootstrap Icons 1.13.1) added toIconList[]and bothcomponents.css;LinkIcons[]/LinkDescriptions[]for the four links of[Leftmenu_my]and the two of[Leftmenu_user](read only where a menu is drawn as tiles or as a mega menu). - Any tab can have a mega menu:
[Topmenu_<tab>] MegaMenuTemplatenames the template that draws its menu (design:parts/setup/mega_menu.tpl,design:parts/user/mega_menu.tpl), andMegaMenuTabs[]switches it on. admin4 design, "Which tabs take part, and how to add one". - Tests:
AdminMegaMenuTestchecks the Users section (its added menus, existing links, icons, names and descriptions in ger-DE and eng-US), which tabs the view serves and that it serves only a tab the user has (hidden, disabled, not shown, not listed), and runs two tabs in the node harness (one panel open at a time).
Changed
- The fragment view is now
user/megamenu/<tab>for every tab, in place ofsetup/megamenu: policyuser/login, since the tabs belong to different modules. It answers only for a tab ofMegaMenuTabs[]with aMegaMenuTemplate(else not available) that the topmenu operator shows enabled and open to the user, never for the anonymous user (else denied); each link is checked as in the left menu. - The panel's strings name the tab: "Open the %tab menu", "Loading the %tab menu", "The %tab menu could not be loaded.", and "Go to %tab" (the tab's own page) for "All Setup functions". One panel is open at a time; a panel opened by a click or a key is not replaced by the hover preview of another tab.
- ger-DE: "My notification settings" reads "Meine Benachrichtigungseinstellungen" (was "Meine Benachrichtigungs- Einstellungen").
Changed after the review
- The mega menu shows from 576px, wherever the main menu is one line (the two-row header from 576 to 1279px too, which is what a browser at 200% zoom shows on a 1920px screen); it was drawn from 1280px only. Below 576px, in the phone menu, the tab stays a plain link.
- admin4l has the mega menus too: the tabs of both designs are drawn by one template,
parts/a4_topmenu_items.tpl, which admin4'spage_topmenu.tpland admin4l's tab menu block include; admin4l's block drew no button before. - Settings that cannot be used are named in the debug report (
parts/a4_mega_menu_check.tpl): aMegaMenuTabs[]value withoutMegaMenuTemplate, aMegaMenuLinks[]key that is no link of the section ([Topmenu_<tab>] MegaMenuSection, new:Leftmenu_setup,Leftmenu_user_mega) or of a section it adds, and more than 12 keys. They were left out silently. - The button's name is "%tab menu" ("Setup menu", ger-DE "Menü „Setup“") instead of "Open the %tab menu": a screen reader adds "collapsed" or "expanded", which contradicted "open" while the panel was shown.
- Tests:
AdminMegaMenuTestchecks the width, the one template of both designs, and the debug messages of the check.
Upgrade notes
- Nothing to do while
MegaMenuTabs[]stays empty. A site that namedsetupalready needs nothing either; to add the Users tab, setMegaMenuTabs[]=usersand clear the INI and template caches. A site that gives another tab a mega menu setsMegaMenuSectionbeside itsMegaMenuTemplateto have itsMegaMenuLinks[]checked.
15 October 2026: the upload page (ezmultiupload 6.1.0)
ezmultiupload/upload/<node> is rebuilt in admin4, admin4l and adminui, in the frame of the content action pages
(box header, box content, control bar with the main action first) with the shared controls; the classic
design/admin keeps its lighter page. Root composer.json (and composer.ci-phpunit.json) require
se7enxweb/ezmultiupload ~6.1.0 and se7enxweb/exp_adminui ~1.0.0.20. Guide: the extension's
doc/upload-page.md.
Added
- Added: The upload page's target location shows its path and whether it takes files; Change location... opens
a location picker (breadcrumbs, a filter, the containers with the reason where one cannot take files), a row of
the queue can have a location of its own, and the last location is the eZPreference
exp_multiupload_location. - Added: Folders (where the browser supports it), drag and drop and pasted images; per file a preview, the class it becomes (upload.ini), an editable title, the alternative text of an image (required where the class asks, L16), a progress bar and the status with the reason; cancel, retry and remove per row and for all; three uploads at a time; a summary toast.
- Added: Checks before anything is sent (
ezmultiupload/service, Action=check): the create permission for the class in the location, the type list, the size limit (MaxFileSize=64Mand PHP's limits) and the alternative text; a file with the same name and size already there offers Skip, Rename or Upload anyway. - Added: SVG images on the upload page: an SVG becomes an Image (sanitized by the kernel), galleries take
*.svgand*.svgz, and the preview is an<img>of the stored file after the upload. - Added:
expMultiuploadService::canUploadAt(), one check whether the current user can upload at a node, for links and buttons to the upload page. - Added: Keep as drafts (the kernel's upload without publishing).
- Added: After the upload, the list of what was made with View and Edit, and Move to..., Add location... and Remove... (to the trash, after a confirmation) through the kernel's move, addlocation and delete operations and their permission checks.
- Added: Import compressed zip file(s) contents, off by default (eZPreference
exp_multiupload_zip_import; off, a ZIP file is one File item): the archive is read on the server by the new ZIP import backend (expMultiuploadZipImporter) and its entries listed, also those of ZIP files inside it, with the folders kept as folder items or flattened (eZPreferenceexp_multiupload_zip_mode). Limits in[ZipSettings]: depth 3, 1000 entries, 500 MB uncompressed, a compression ratio of 100 (ZIP bombs); unsafe paths, links and devices are skipped, encrypted archives refused, and the archive is unpacked in a private directory of the request that is removed afterwards.
Updated
- Updated: The content tree's "Upload multiple files" works on every container: the page used to answer with the
kernel's "object is not available" error on any location whose class was not
folderorgallery(AvailableClasses[]), which on the media site's installations is nearly every node (Fit & Healthy is anng_frontpage). New[MultiUploadSettings] AnyContainer=enabled: every container the user may create content in takes uploads; a location that cannot shows the page with the reason and offers the nearest one above. Upgrade note:AnyContainer=disabledkeeps the 6.0 rule. - Updated: Every request of the page that changes something is a POST with the session's form token, checked again by the extension (constant-time) and never accepted from another site; node ids are positive integers only.
- Updated: The Admin UI header's search button and user menu have names (exp_adminui 1.0.0.20).
- Tests:
tests/tests/extension/ezmultiupload/(testsuiteezmultiupload, no database): the input checks, the token, the permission pre-check, the duplicate check (expMultiuploadServiceTest) and ZIP files made by the test: nesting and its limit, traversal, links, a ZIP bomb, the entry and size limits, encryption, names that are not UTF-8, mixed types and both folder modes (expMultiuploadZipTest).
15 October 2026: sub items reordered by drag and drop
Where a node sorts its children by priority and the user may change the priorities, admin4's sub items table can be reordered by dragging a row, with the keyboard as well, and the new order is saved as the children's priorities. Feature.
Added
- admin4: a column of grips (
grip-vertical) in the sub items table (javascript/subitems_priority_drag.js, loaded bychildren.tpl), only where the node is sorted by priority and$node.can_edit(the condition of the classic priority fields,confObj.priorityDraginchildren_detailed.tpl), while the table is sorted as the node sorts and not filtered. Pointer events for the mouse, touch and pen (no HTML5 drag and drop, no library): the row is its own placeholder, a ghost with the name follows the pointer, the window scrolls near its top and bottom edge, Escape cancels. Keyboard: Space or Enter picks a row up, the arrow keys (Home, End) move it, Space or Enter drops it, Escape or leaving the grip cancels; Alt+Up and Alt+Down move it by one place at once. Every step is announced ("Position 3 of 23", "Moved to position 4 of 23"), translated in ger-DE and eng-US. - Saving right after the drop through
ezjscnode::updatepriority(the inline priority editor's server function, with the page's form token): the page's own priorities are given out again in the new order, in the node's sort direction, so no row moves to another page; equal priorities on the page, or one shared with the neighbouring row of the page before or after, become distinct values between those neighbours (in steps of 10 where there is room); without room, all children are numbered again in steps of 10. A toast says "Order saved" with Undo (it stays 10 seconds), which puts the previous priorities back. A failed save puts the rows back and says why. ezjscnode::updateprioritytakes the compact listsPriorityListandExpectedPriorityList("node_id:priority,...", one field for a page of any size, far belowmax_input_vars) andRenumber=1withPageOrderandPageOffset. WithExpectedPriorityListnothing is written when one of those children has another priority now or is no longer a child (array( 'conflict' => true, 'nodes' => ... )); the table then loads again and says that someone else changed the order. Otherwise the answer lists the changed priorities ('changed' => array( node_id => array( old, new ) )).ExpectedPriorityListis required with the new fields and must name every node they write (a node ofPriorityListorPageOrderwithout its shown priority is refused); they are refused for a node that does not sort its children by priority. Permission as the classic form: the parent'scanEdit();ContentNodeIDmust be a positive integer. The check and the write run in one transaction, rolled back together on an error, the rows read withFOR UPDATEon MySQL, PostgreSQL and Oracle; the write is the kernel's (thecontent/updatepriorityoperation with its triggers, elseeZContentOperationCollection::updatePriority()), and a workflow that stops or defers it is answered as an error, not as saved. PostedPriority[]/PriorityID[]work as before.window.a4Toast( message, kind, opener, delay ): the time a toast stays, for a toast with an action button (.a4-toast-action).- Tests:
AdminSubitemsPriorityDragTest(its behaviour runs in node,tests/tests/design/js/priority_drag.js) andezjscServerFunctionsNodePriorityTest.
Updated
- The sub items lists sorted by priority (
ezjscnode::subtree,expsubitems::rowsthroughexpSubitemsColumnRegistry::sortFor()) order equal priorities by node id, in the same direction, so a page shows the same rows each time and the renumbering keeps the order the list showed. - The priority cells stay editable. A drag does not start while a priority is being edited with a changed value (a toast asks to save it with Enter or cancel it with Escape first, and the editor keeps the focus) or while such a change is being saved; no cell can be edited while a new order is being saved.
15 October 2026: language badges show the state of a translation
Added
- admin4's language badges (DE, EN) show the state of each language for the object by their shape, not by a color
per language and without the status colors: the main language filled, a translation outlined, a language of the
site the object does not have dashed with pale text, a translation last edited before the main language with a
small mark. The code stays the text; the state is in the title and the accessible name ("German – main language",
"French – missing", "Spanish – outdated, older than German"), in ger-DE and eng-US. Under forced colors the shapes
stay. Shown in the node view's meta line (every language of the site, the missing ones as a "Translate into ..."
link,
content/edit/<object>/f/<language>, for a user who may create that translation) and in the Translations tab (the state of each translation, the missing ones under the table). The sub-items table's Translations column shows main, present and, on a site with up to six languages, the missing ones (as text); the outdated mark stays on the node. Includedesign:parts/language_state_badge.tpl, the fetch functionfetch( 'content', 'language_states', hash( 'object', $object ) )and the classexpContentLanguageStates. - A translation is outdated when the newest published or archived version edited in it is older than the newest edited in the main language; both must exist, so an object whose languages came in one version (an import, a copy) or whose older versions were removed is never marked. One grouped query reads all objects asked for.
- The node JSON of the sub-items table (
ezjscAjaxContent::nodeEncode()) carriesinitial_language, the locale of the object's main language. - Tests:
AdminLanguageStatesTest(states, outdated, the list limit, one query for a page of objects, the texts, no color per language, forced colors, the sub-items formatter run in node), a contrast pair inAdminTokenContrastTest.
Updated
- A plain language badge is outlined (
--a4-field-line, 3:1) on no fill, so it reads as "this language exists" next to the filled main language; the badge of the language shown keeps its accent edge, tint and underline. - A badge in front of a language's name in a table cell (the Translations tab) keeps a gap to the name.
- The node view's copy buttons (Node, Object, Remote ID) are 24px targets at every pointer, not only on touch screens: the language badges of the meta line are 24px high and wrap beside them on a phone, where the shorter buttons failed axe's target-size check.
- A
fetch()in a compiled template checks that the function's class and method can be loaded before it calls them, as the interpretedfetch()(eZModuleFunctionInfo::executeClassMethod()) always did. A template compiled by a process that knew a new kernel class could run in a PHP-FPM or Velocity worker whose autoload did not know it yet, and the page ended in a fatal "Class not found" (it happened once on a development installation withexpContentLanguageStates, until the workers were reloaded). Such a worker now renders no result and writes an error to the debug output. A new kernel class is still deployed with its autoload (exp:velocity deploy --kernel) before a template uses it.
15 October 2026: an Upload button in the node view
Added
- The node view's actions have an Upload button after Edit (design/admin and admin4, so also admin4l; first in the
"..." menu of exp_adminui). It opens ezmultiupload's upload page for the node (
ezmultiupload/upload/<node id>) and shows only where the current user can upload there: theezmultiupload/uploadmodule is enabled and the new fetch functionfetch( 'ezmultiupload', 'can_upload', hash( 'node_id', ... ) )says yes (the module policy, read and create access at the node, a container or a configured upload class or node; ezmultiupload 6.1). Drafts and the edit pages never show it. An.a4-btnwith the Bootstrap Icon.a4-bi-upload(new incomponents.css), "Upload" / "Hochladen". Guide: doc/eng-us/bc/6.0/admin4-styling.md, "The node view's Upload button".
15 October 2026: unreferenced objects and broken relations
Added
- The kernel module
contentcheckwith two content checks for the administration. Page: The content checks. - Unreferenced objects (
contentcheck/unreferenced/<node id>): the published objects below a node that no other object refers to, filtered by class, sorted by name, class, date or id, and paged, with name, class, file size, last modified and location. A reference is a row ofezcontentobject_linkof any type (object relation, relation field, rich text embed and link) from a published, draft, pending or queued version of an object that is not in the trash; relations from trashed objects, old versions and the object itself do not count. OneCOUNTand one page query withNOT EXISTSand thecontent/readcondition in the SQL; 1 to 1.7 s for 100,000 hits on MariaDB. - Ticked unreferenced objects go to the trash after a confirmation that lists what happens to each (POST, the form
tokens and a confirmation token bound to the objects shown). Each object is checked again (still unreferenced,
content/removeon every location, no sub-items, the class allows the trash, no pending version, no content job) and moved by the kernel's removal, recorded ascontent.node.remove.trash; at mostMaxTrashBatch(50) at a time. - Broken relations (
contentcheck/brokenrelations/<node id>): visible, published content whose published version relates to, links to or embeds an object or node that is deleted, in the trash, hidden, never published or without location: the relations inezcontentobject_linkand theobject_idandnode_idof links and embeds in the text ofezxmltextfields (XmlDatatypes[]), which still name a deleted target. Source, field and language, target and its state, and an Edit button where the user may edit; filters for the subtree, excluded subtrees, the states and the kinds of reference. Read only. The sources are checked by object id in chunks, a page goes on after the last object checked (20 to 30 ms for a page of 50 findings). - Policies
contentcheck/unreferenced,contentcheck/trashandcontentcheck/brokenrelations(Administrator only in a new installation); the checks answer on administration siteaccesses only (contentcheck.ini). What a user may not read is left out of every list, count and page in the SQL; a target the user may not read is "not available", without id or state, and a deleted, trashed or hidden target the user may read is shown by its id and state only. - Setup menu and overview: Unreferenced objects (icon
file-earmark-break) and Broken relations (iconexclamation-diamond) in the group Content model, after the content finder. Context menu of the content tree and the sub-items: Advanced > Unreferenced objects in subtree and Broken relations in subtree, for users with the policy. Translations eng-US and ger-DE. - Tests:
expContentCheckQueryTest(no database) andexpContentCheckSandboxTest(a throwaway installation only), suitekernel-contentcheck.
Updated (review before the merge)
- Unreferenced objects lists assets only, judged against everything that can use them. A published page, a
folder, a user or a settings object is reached through the tree whether or not anything links to it; the first
version listed every published object without a relation row, which on a demo installation was 647 of about 800,
including landing pages, users and the site's settings object, all offered to the trash. The list now covers the
classes of
contentcheck.ini [UnreferencedSettings] AssetClassList[](image, file and the media classes), only when every location's parent is a storage folder (StorageParentClassList[],folder): below a gallery, an article or an event the parent's template shows the asset. - The reference index (
expContentCheckReferences) adds every source a relation row misses: relation field data (ezobjectrelation,ezobjectrelationlist,ngenhancedlink, which never writes a relation row); ids, schemes, storage paths and internal paths in the text of any field (54 images of the demo installation were embedded in rich text with no relation row, and the first version would have offered them for the trash); theezurltable, custom URL aliases and wildcards; the layouts (collection items by node or object, dynamic collections by parent node and class, block parameters includingexp-remote-location://links, rule targets); node ids in every settings file; literal ids in every design's templates; role and policy limitations by node or subtree; the shop, collected information, pending actions, bookmarks, subtree notifications, draft placements and the newsletter (configurable table columns, a missing table skipped) and the newsletter's article pools. The index is kept forIndexTTLseconds; the move to the trash always builds it again; a source that cannot be read lists nothing. Published objects without any location are counted. - Broken relations also read the data of relation fields (a target named where no relation row was written), show what to do about each finding, and list on the first page the layouts whose collection items, dynamic collection parents, block links or rule targets name a node or object that is deleted, in the trash or hidden.
- On the command line, report only:
./console exp:content-check --build-index | --unreferenced | --broken | --why=<ids> [--node=<id>] [--json](bin/php/contentcheck.php), and the cronjob partcontentcheckfor installations that setBuildOnRequest=false. - Ids of imported content are resolved as the site resolves them:
contentcheck.ini [ReferenceSettings] ImportedIdRemotePatterns[]andImportedIdObjectPatterns[](the media site demo resolves enhanced link and layout component ids as remote idmedia-o-<id+776>,media-o-<id>or object<id+776>; on the demo installation all 23 enhanced links reported as broken resolve that way). - A dynamic collection of the current location counts below the nodes its layout's rules target (children of a node target, everything below a subtree target, anywhere below a parent of a class condition's classes for other rules and for shared layouts), not anywhere: one gallery block had kept every image from ever being listed.
- Tests:
expContentCheckReferencesTest(no database): every reference source's extractor, the judging, the asset classes in the SQL, relation fields and hints.
Upgrade notes
- Nothing to do: no database change. Give the three policies to the roles of the people who tidy up content.
15 October 2026: removing and hiding check the reverse relations
Added
content.ini [RemoveSettings] ReverseRelationCheck=disabled|warn|block(defaultwarn): the confirmation page of content/removeobject lists, per selected item, the objects that point at what the removal takes away: the referring object with a link, its class, the kind of reference (object relation, embed or link in rich text, relation field with the field's name) and its target. It replaces the general hint to look at Reverse related for subtree. Withblockthe OK button is disabled while references exist, and the view refuses a posted confirmation,HideRemoveConfirmation=trueand a removal as a content job on its own as well.content.ini [HideSettings] ReverseRelationCheck=disabled|warn(defaultwarn): withwarn, hiding a node that other objects point at, or whose object has further locations, takes a confirmation page with the list first: from the context menu, the Locations tab, a link to content/hide and "Hide selected" of the sub items list.- Only objects the current user may read are named. References from other objects, and further locations the user
may not read, are counted per item and never named ("2 references you cannot see").
blockcounts them all the same. content.ini [ReverseRelationSettings] ProviderClassList[]: classes that report references kept outsideezcontentobject_link. explayouts addsexpLayoutsReverseRelationProvider: blocks of published layouts whose collection names a node inside the removed or hidden subtrees, listed as "Layouts block" with a link to the layout.- What counts: rows of
ezcontentobject_linkfrom the current version of a published object, of any kind, to an object whose every location lies inside the selected subtrees, from an object that stays. Page: Removing and hiding: the check of reverse relations. - Tests:
expReverseRelationCheckTest,AdminReverseRelationCheckTest. German and American English for the new strings.
Updated
- content/hide draws both of its confirmation pages with one method,
confirmationPage();expContentHideReturnTestcounts them there. - With
block, removing the last locations of an object on the Locations tab goes through content/removeobject (listed and refused there), and a WebDAV delete is refused. The standard design's removal page shows the list. - The selected nodes are fetched in one query; class and field names once per request.
Upgrade notes
- Hiding content that others point at, or an object with further locations, shows the list first from now on
(
[HideSettings] ReverseRelationCheck=warn); setdisabledfor the hide of earlier releases. - The removal page shows the list from now on (
warn); selecting every location of an object that others point at now shows the confirmation page instead of removing it at once. SetReverseRelationCheck=disabledunder[RemoveSettings]for the page of earlier releases, orblockto refuse removals while references exist.
15 October 2026: language badges show the language, not the region
Updated
- A language badge (
design:parts/language_badge.tpl, admin4 and the classic admin design) always shows the language part of the locale's language tag:eng-GBandeng-USare bothEN, also when the installation has both. Before, two content languages of the same language showed the region as well (EN-GB,EN-US), so a row of badges mixedDEwithEN-GB. The full name (English (United Kingdom), English (American)) stays the badge's title and accessible name, or is written next to it, so a tooltip and a screen reader still tell the two apart. The include no longer fetchescontent/translation_listto count the languages: one query less per badge. The sub-items table's Translations column gets its codes from the same include and follows. Guide: the admin4 design: languages. - Test:
AdminLanguageBadgeTest(the badge is the language part of the tag, the include fetches no translation list; every locale file ofshare/locale/gives a two or three letter code, everyesl-*localeES, so a locale added later such asesl-ARfollows). - Two regions of one language on one object keep
ENon both badges: the name tells them apart, written next to the badge in the Translations tab and the edit page's language choice, as each badge's tooltip elsewhere, and in the accessible name together with the state ("English (United Kingdom) – outdated, older than English (American)"). The admin4 design: languages.
15 October 2026: checking user accounts
Added
- Users > Check user accounts (
user/hygiene, also a tile of the Setup overview under Users and messages): every user account, or those below a user group, filtered by never signed in, not seen for N days, disabled, locked (more failed sign-ins thanMaxNumberOfFailedLogin), orphaned (no user object, never published, no location) or a shared e-mail address, each filter with its count; a search, the order of the columns and the page size. Each row shows the states, the last visit, the number of sign-ins, the groups and the roles (also those of the groups above). Disable, enable and unlock the ticked accounts after a confirmation with the counts; every account is decided again when it is changed, never the anonymous user, the user creator, the administrator account (id 14, loginadmin) or the user who acts, and only withcontent/editon the account's object. The changes go through the kernel's paths and their audit events (access.user.disable,access.user.enable,access.user.unlock). Export the filtered list as CSV (UTF-8 with a byte order mark, formula cells made text, recorded asdata.export.csv). Nothing here removes an account: a user object never goes to the trash. New: the functionuser/hygiene,expUserHygiene, menu.iniuser_hygienein[Leftmenu_user]and[Leftmenu_setup](iconperson-badge, new inIconList[]), offered in the Users tab's mega menu (MegaMenuLinks[]=access-user_hygiene), Check user accounts. Test:expUserHygieneTest.
Updated
- Check user accounts has the filter Shared login (any case, not counting accounts in the trash; the
duplicates LoveStack L04's
exp:user-login-checkreports, to resolve before--fix-index), a badge on a shared login, and one line under the tabs that says exactly what the current filter holds. The anonymous user is no longer counted as Never signed in (it cannot sign in). The page counts the published user objects that have no account row (the other side of Orphaned, which no action can touch) and says how many there are. Test:expUserHygieneTestUH-09.
Upgrading
- The page is only for roles with
user/hygiene(Administrator has it through*/*); e-mail addresses on it and the filter of shared addresses needrole/assign, as the E-mail column of the sub-items table.
15 October 2026: an Export menu in the content tree
Added
- The Advanced submenu of the content tree's menu and of the sub-items "…" menu has an Export submenu. Each entry opens a form filled in for the item; the export is started there, never from the menu: the item, or its subtree, as a content package (with or without class definitions, with all versions, or the package wizard itself), and, with xrowextract 2.5.11, the children or the subtree as CSV, the subtree of one class (a list of the classes below the item, read when it opens), every attribute or the migration columns. Export is shown only to a user with package/create and package/export (the content object wizard) or xrowextract/csv; an Editor sees no change. Guide: The Export menu of the content tree.
- Entry parameters of
package/create:(creator)/ezcontentobject/(node)/<id>[/(subtree)/1][/(classes)/0|1][/(versions)/current|all]start the content object wizard with the node or subtree selected and the second step preset; only a node the user can read and a user who may use the wizard and download its package. fetch( 'package', 'content_export_access' ), and admininterface.ini[AdditionalMenuSettings]ExportMenuTemplateArray[]andExportSubMenuTemplateArray[]for an extension's export entries.- ezpopupmenu.js:
menuArray[id]['onshow'], for a submenu whose items are filled in when it opens.
Updated
- ezpopupmenu.js places a nested submenu inside the window: beside its parent on the side with room, moved inside the window when neither side has room (960px at 200 % zoom, phones), up from the bottom edge, scrolling when taller than the window. adminui's copy (exp_adminui 1.0.0.22) is now admin4's, with the keyboard pattern of #165.
- Required packages: xrowextract ~2.5.11 (the CSV form's entry parameters and the class list of the Export menu); the context menu script of exp_adminui 1.0.0.22 is in the required ~1.0.0.23.
15 October 2026: deactivate and move user accounts instead of removing them
Added
- Deactivate and move and Reactivate and move back (opt-in): instead of removing a user account (users
never go to the trash, so removing one is final), it is disabled and all its locations are brought together in
one hidden user group without roles; the locations it had are remembered, and reactivating it enables it and
moves it back (a former location that is gone is left out and counted; a gone main location falls back to
[UserSettings] DefaultUserPlacement). On a user's page, in the context menu (Advanced) and as bulk actions on Users > Check user accounts, each after a confirmation that lists the accounts, their groups and where they go. The group is[UserSettings] DeactivatedUserGroupNodeIDor the user group with the remote idexp_deactivated_users_group, created on first use by a user who may, or beforehand bybin/php/ezcreatedeactivatedusergroup.php -s <siteaccess> [--commit]. Every step goes through the kernel's operations (user/setsettings,content/move,content/removelocation,content/addlocation) in one transaction; recorded asaccess.user.deactivate_move,access.user.reactivate_moveandaccess.user.deactivated_group.create(the audit catalogue has 148 names). The remembered locations are oneezsite_datarow per account (exp_user_deactivation_<id>), so no database update is needed. New: the function and viewuser/deactivate,fetch( 'user', 'deactivation_offers', ... ),expUserDeactivation, Deactivate and move. Tests:expUserDeactivationTest,expUserDeactivationLiveTest.
Updated
- The group of deactivated users keeps no roles: Assign on a role (role/view and role/assign) leaves it out of the chosen users and groups and says so on the role's page, and Deactivate and move refuses while a role is assigned to the group some other way. Check user accounts no longer depends on the new class being loaded (a process started before the deploy). Tests UD-07 and UL-08; texts in eng-US and ger-DE.
Upgrading
- Nothing changes until you switch it on:
site.ini [UserSettings] DeactivateAndMove=disabledis the default, and the installer neither switches it on nor creates the group. To use it:- optionally create the group beforehand:
php bin/php/ezcreatedeactivatedusergroup.php -s <admin siteaccess>(dry run), then the same with--commit; it prints the node id; - set
[UserSettings] DeactivateAndMove=enabled(and, only to use a group of your own,DeactivatedUserGroupNodeID=<node id>) insettings/override/site.ini.append.phpor the admin siteaccess, then clear the caches (php bin/php/ezcache.php --clear-all, andvar/cache/ini/); - give the function
user/deactivateto the roles that retire accounts (Administrator has it through*/*); they also needcontent/edit,content/moveandcontent/removeon the accounts, andcontent/createin the group (see the feature page).
- optionally create the group beforehand:
15 October 2026: the left column's size is a width only
Fixed
- admin4: the left column's sizes (the small, medium and large switch, and a width dragged by hand) change its width
only. The medium and large sizes also made its text larger (16px and 20.9px instead of 13.2px), a rule taken over
from admin3, where the sizes were font sizes. The column's text now keeps the body's size (
--a4-text-body, 13px up to 14px on very wide windows) at every width, as the right column; headings, the content tree and the group titles keep their sizes relative to it.--left-sidebar-font-sizeis gone from admin4'spagelayout.css;--left-sidebar-widthis unchanged. admin3 keeps its own rule. - Test:
AdminLeftColumnFontTest(testsuitedesign): nohtml[data-a4-left-size]rule of admin4 sets a font size, and.sidebar.leftinherits the body's.
Upgrade notes
- A stylesheet of your own that set
--left-sidebar-font-sizefor admin4 has no effect any more; setfont-sizeon.sidebar.leftinstead.
15 October 2026: an Import/Export button in the node view
Added
- The node view's actions have an Import/Export button after Upload and before Move (admin4, admin4l, admin;
exp_adminui 1.0.0.24: an entry of the toolbar's "…" menu). Its menu offers the export entries of the context
menus' Advanced > Export and, with xrowextract 2.5.12, Import into this item (only on a container below which the
user may create content), the export and import jobs, the export history and the schedules, each for its policy.
It is shown only for a published object with an entry for the user: an Editor sees no change, a user with
xrowextract/csv only sees the CSV entries. A menu button (Enter or Space open it, Escape returns to it) with the
Bootstrap icon arrow-down-up, which is now bundled (
.a4-bi-arrow-down-up, design.iniIconList[]). Guide: The Export menu of the content tree. popupmenu/popup_export_items.tpl, one definition of the export entries for both menus, and admininterface.iniImportExportMenuTemplateArray[]for an extension's import and job entries; ezpopupmenu.js's element optionshown_ifleaves out an entry that does not apply to the item.
Updated
- Required packages: xrowextract ~2.5.12 (
xrowextract/import/(parent)/<node id>, the import and job entries), exp_adminui ~1.0.0.24.
15 October 2026: the phone menu fits the screen and is a grid of tiles
Fixed
- admin4: the open phone menu (below 576px) was cut off at the bottom in Safari and Chrome on iOS. The panel was at
most
100vhhigh and started at the window's top, with the header's height as padding inside it. On iOS100vhis the height with the browser's toolbars hidden; with them shown (as when the menu is opened) the panel's end lay under the bottom toolbar, and scrolling the panel could not bring its last items up: with the search scope popup open at a visible height of 664px the panel ended 60px below the window (measured in an emulated 390 x 664 viewport with100vhresolved to 750px, as iOS does). The panel now starts under the header's row and is at most100dvh(the visible height, which follows the toolbars;100vhbefore it for browsers without dynamic viewport units) less the header. It is the only scroller (admin3 let the list inside scroll as well), its scrolling does not chain to the page (overscroll-behavior: contain), the page does not scroll while it is open, and the safe area insets keep its ends clear of the home indicator where a page covers it (viewport-fit=cover). - The closed phone menu was still in the Tab order (
max-height: 0hides, it does not remove): it isvisibility: hiddenafter its closing slide now. A menu left open while the window grows past 575px (a phone turned to landscape) is closed, which also gives the page its scrolling back.
Updated
- The phone menu is laid out for thumbs: the search first and on a row of its own, then the tabs as a grid of tiles (three to a row at 390px, two at 320px, one at 200% text) instead of a column of small centred links, the current tab filled with the accent; the user's actions (the user's name, leading to their object, Change information, Change password, My e-mail preferences, Logout) as a row of their own; a Close button at the end. Every target is at least 44 x 44px (Apple's Human Interface Guidelines; the search's scope button 40px inside the 44px field), with 16px at the panel's top and 24px at its sides and end. On a phone the header's row keeps the logo, the environment badge, "Open the site", light/dark and the menu button, which shows a cross while the menu is open; the user menu and logout move into the menu (admin4l's header, which has no such row, keeps them).
- Keyboard: while the phone menu is open, Tab and Shift+Tab stay in the header (its row, the menu button and the panel); Escape and the Close button close it and give the focus back to the button; a tap outside the header closes it. No slide with reduced motion; in forced colors the tiles keep an edge and the current tab is underlined with a heavier edge.
- New template
parts/a4_phone_user.tpl(the user's actions; the same links and conditions asparts/a4_header_user.tpl). Inpage_topmenu.tpland admin4l's tab menu block the phone search moved to the menu's start; admin4.js's one-line menu leaves the phone's own items (.a4-phone-only) out of its fitting. From 576px nothing changes. - Test
AdminPhoneMenuTest(testsuitedesign): the panel's height (100dvhafter a100vhfallback, no100vhleft in the phone rules), its single scroller andoverscroll-behavior, the grid, the 44px targets, the order of the menu's items, the user row's links against the header's, the hidden closed panel, and in node the focus trap, the Close button, the tap outside (pointerdown: iOS sends no click for a tap on the page's background) and the close on a wider window.AdminHeaderTestfollows the new order. - admin4l's tab menu block (
explayouts/block/admin_tab_menu.tpl) ends with the same Close button as admin4's top menu, wired by the same script;AdminPhoneMenuTestchecks its order and button too.
15 October 2026: the site menu of the administration
The globe in the admin4 and admin4l header, which opened the site, now also lists the installation's other sites that the signed-in user may log in to, with their addresses, so an administrator can see which end points there are without reading the INI settings. One button in the place of the link: the header takes no more room. On by default. Feature ("Site menu").
Added
site.ini [AdminSiteAccessMenu]:Show(enabled;disabledleaves the globe a plain link),List(related:RelatedSiteAccessList[]of the admin siteaccess, elseAvailableSiteAccessList[];available: all ofAvailableSiteAccessList[]; an installation with separate databases per site setsRelatedSiteAccessList[]for each administration),HiddenSiteAccessList[],AdminSiteAccessList[](which siteaccesses count as administrations; empty: those whoseSiteDesignbegins withadminor that setFrontSiteAccess) andListAdministrations(disabled: sites only;enabled: the administrations too, as a group of their own; part of the cache key).ezpSiteAccessMenuand the template operatorssiteaccess_menu( $current_user )andsiteaccess_menu_key()(ezpSiteAccessMenuOperator), andezpSiteAccessURL::frontSiteAccess(), the name of the siteaccessfront()links to. The addresses areezpSiteAccessURL::root()'s, assiteaccess_url()has them (MatchOrder, URIMatchType, HostMatchMapItems, SiteURL, the scheme, host and port of the request).parts/a4_header_site.tpl, the site link and the menu for admin4'spage_topmenu.tpland admin4l's tools block. For signed-in users, the globe is a disclosure button (aria-expanded, aria-controls) whose list opens the site first, highlighted, then the other sites (withListAdministrations=enabledthe administrations as a second group), each with its SiteName (the identifier without one), identifier, locale and address, in the same window; the siteaccess in use carriesaria-current. Keys as the user menu: ArrowDown opens it, ArrowUp/ArrowDown/Home/End move, Escape closes it and returns the focus, a click outside closes it. Without JavaScript, or with the menu off, or with no other siteaccess to list, the globe stays the link it was. On a phone the list opens under the header. Light, dark and forced colors. German and American English for the four new strings. Test:AdminSiteAccessMenuTest(with the node harnesstests/tests/design/js/site_menu.js).- Besides the site the globe opens, a siteaccess is listed only where the signed-in user may log in:
user/loginwith its SiteAccess limitation, checked byeZUser::canLoginToSiteAccess()as the login checks it. The template passes$current_usertositeaccess_menu(); the header's cache blocks are keyed by the user's role hash already. The entries take--a4-control-hand a spacing token.
Updated
- The header's cache blocks (admin4 and admin4l
pagelayout.tpl) are keyed bysiteaccess_menu_key()too: the list, its groups, the siteaccess in use and the scheme, host and port of the request. A changedSiteName,LocaleorSiteDesignof a listed siteaccess shows once the template caches of the administration siteaccess are cleared. - A mega menu does not open on hover while the site menu is open, as for the user menu.
15 October 2026: admin4's typeface is the Helvetica Neue stack
Updated
- admin4:
--a4-font(components.css, also the copy indesign/admin) is now"Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"instead of the operating system's interface font (system-ui, -apple-system, "Segoe UI", Roboto, ...). The text looks the same on macOS, Windows and Linux, and emoji in content names and text are drawn in color everywhere. System fonts only; no font file is added. - admin4: the stylesheets that still named a font list of their own use the tokens:
core.css(body, headings, form fields, buttons;preandcodetake--a4-monoinstead of "Courier New"), the pager oftheme/admin_datatable.css,content.css,packagecompare.css,ezinfo.css,visual-templateview.css.core.csskeeps Arial (andmonospace) as the fallback ofvar(), for the standard popup, print and redirect layouts that load it withoutcomponents.css. The text and the table cells of the rich text editor (editor_content.css) follow--a4-fontinstead of the editor skin's Arial, its literal blocks--a4-mono. - Test:
AdminFontStackTest(testsuitedesign):--a4-fontis exactly this stack in both copies ofcomponents.css, and everyfont-familyof admin4's stylesheets isvar(--a4-font)orvar(--a4-mono)(with an optional fallback) orinherit(icon fonts aside); the emoji faces follow the text faces (the generic family last of these, Windows' color emoji before its monochrome symbols); no font is downloaded (the one@font-faceis the IcoMoon icon font from the design's folder, no template or stylesheet of admin4 or admin4l loads a web font). admin4l has no stylesheets of its own and takes all of this from admin4.
Upgrade notes
- The administration's text changes from the system interface font (San Francisco on macOS, Segoe UI on Windows) to
Helvetica Neue, Noto Sans, Liberation Sans or Arial, whichever is installed first, with color emoji fallbacks. To
keep the old look, redefine
--a4-fontin a stylesheet of your own (design.ini [StylesheetSettings]BackendCSSFileList[]), e.g.:root { --a4-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }.
LoveStack features (from 2026-10-12)
Features of LoveStack, Mugo Web's distribution of the same kernel, rebuilt in Exponential with thanks to Mugo Web; each line names the LoveStack commit it comes from.
Overview, feature by feature, with who each one is for and how to switch it on: LoveStack features in Exponential 6.
- Moving a site from LoveStack:
./console exp:lovestack checklists what a LoveStack installation brings along and what to do with each item: the database version rowlovestack, LoveStack'sdb-schema-versionrow, a unique index onezuser.login, adata_text NOT NULL DEFAULT ''column, thelegacy_2018extension, LoveStack-only settings insettings/overrideand the siteaccesses (with the Exponential name where there is one) and themugoweb/ezpublish-legacypackage incomposer.json(expLoveStack). It reads only and ends with exit code 0 when nothing needs to be done, 1 otherwise;--jsonfor a machine-readable report. Tested on a LoveStack-shaped SQLite database built from Exponential's schema and LoveStack's schema steps 1 to 4 (mastere61e9fdf0c): the same content operations (publishing, new versions, adding, moving and removing locations, copying, a new class attribute on a class with objects, the trash) give the same content as on an Exponential database, and an empty string, never NULL, indata_text, so the column can stay as LoveStack made it. Guide: Coming from LoveStack. - Moving a site from LoveStack:
./console exp:lovestack migraterecords a LoveStack database as Exponential's. The version rowezpublish-version=lovestackbecomes this release's version (6.0.15stable), and LoveStack's schema version (db-schema-version, kept as it is) is recorded asezsite_datalovestack-schema-version. It then prints the settings and extensions to change by hand, taken fromcheck: LoveStack-only settings with their Exponential names,[Session] SameSite(read asCookieSameSitewith a deprecation notice until it is renamed), an activelegacy_2018extension and themugoweb/ezpublish-legacypackage (expLoveStack::migrationPlan(),migrate()). A dry run unless--applyis given; it never changes content or settings, a second run changes nothing, and a database that does not come from LoveStack is refused with a message saying so;--jsonfor a machine-readable report. It follows LoveStack's documented schema steps (mastere61e9fdf0c,update/run.phpandupdate/database/mysql/lovestack/1.sqlto4.sql), with thanks to Mugo Web. Guide: Coming from LoveStack. - L01 (LoveStack commit aa307a7ff3): the node data calls
ezjscnode::load,::subtreeand::updateprioritycan require the policy ezjscore / call with the function listezjscnode:ezjscore.ini [ezjscServer_ezjscnode] Functions[]=ezjscnodeand the new per server function settingRequirePolicy(enabled: checked, a request whose calls are all refused is answered with 403;disabled: not checked; not set: checked as before). New installations switch it on and give the Editor role the policy. Upgrade note: the shipped value isRequirePolicy=disabled, so nothing changes; to switch it on, give ezjscore / call withezjscnodeto the roles that use the administration without full access, then setRequirePolicy=enabledinsettings/override/ezjscore.ini.append.php. Guide: Roles and policies, section 8. - L02 (LoveStack commit 98854ab935): registering can require the
user/registerpolicy.site.ini [RoleSettings] PolicyOmitListExclude[]takes single entries out ofPolicyOmitList[](new:expPolicyOmitList), and the login pages of admin and admin4, the standard design and its login toolbar, the media and the starter designs show the register link or the Sign Up button only to a user who may openuser/register(fetch( 'user', 'can_open' )). New installations writePolicyOmitListExclude[]=user/register. Upgrade note:user/registerstays inPolicyOmitList[], so registration and its link stay open; to close it, addPolicyOmitListExclude[]=user/registertosettings/override/site.ini.append.php(and give the Anonymous role User / Register where registration should stay open). Guide: Roles and policies, section 8. - L03 (LoveStack commit 13ace6c9c8): logging out ends the session and its cookie.
eZUser::logoutCurrent()removes the session (eZSession::remove()) instead of giving it a new id with its contents kept. Thedestroy()of every session handler sends the session cookie expired with the path, domain,Secure,HttpOnlyandSameSitethateZSession::start()set it with (eZSession::expiredCookie(),eZSession::expireCookie());eZSession::remove()also keeps a handler that only deletes its stored copy (ezpSessionHandlerDB) from having the session written back, and gives a session started later in the same request a new id, under PHP-FPM and Velocity alike. The logout view readsLastAccessesURIbefore logging out and no longer opens a new session only to storeforce_logout. On for every installation. Upgrade: nothing to do; code of your own that reads session variables aftereZUser::logoutCurrent()finds the session empty. Guide: Security and audit. - L04 (LoveStack commit 826e1d4647): one account per login.
./console exp:user-login-checklists the logins more than one account has, also those that differ only in case, and changes nothing;--fix-indexadds the unique index onezuser.loginonce the list is empty and refuses otherwise (expUserLoginIntegrity). The kernel schema of every engine (kernel/sql/*,share/db_schema.dba) has the unique index, so new installations get it. A user account of a first version whose form did not validate is not stored, and a draft, a package import, the content API's account string and a first LDAP sign-in no longer give an account a login another account has; two requests creating the same account at once end with one account. Upgrade note: the database update does not add the index (it would fail on duplicates); run./console exp:user-login-check, resolve what it lists, then./console exp:user-login-check --fix-index. Guide: one account per login. - L05 (LoveStack commit c8e45fced7): uploaded images are checked by their content. PHP's
fileinfo reads the type, which must be on
image.ini [ImageUploadSettings] AllowedMIMETypes[](JPEG, PNG, GIF, WebP, AVIF, TIFF, BMP; SVG was added on 15 October 2026, stored sanitized); a refused file is not stored. It applies to the image datatype in the edit form,content/upload, the rich text editor's upload dialog, the AJAX uploader, WebDAV and imports, andeZContentUploadrefuses such a file before it creates an object. New:expImageUploadValidator::validate( $path ). On for every site, upgrades included: a file that is not a listed image by content (a renamed file, HEIC, SVG) is refused (SVG is accepted from 15 October 2026, see SVG images). To accept another type, add it toAllowedMIMETypes[]in an override;ValidateMIMEType=disabledkeeps only the getimagesize() check. Without fileinfo that check decides and one warning per process says so. See behaviour changes of October 2026. - L06 (LoveStack commit 04b3b6985a): the package wizard checks the thumbnail of a new package and the images of
a style package by their content, with the allow-list of L05 (
eZPackageCreationHandler::httpFileIsImage()), so a file that only looks like an image is refused. On for every installation, upgrades included; nothing to do. Guide: behaviour changes of October 2026. [Session] SameSite(LoveStack commit e56cb305f0): settings written with LoveStack's name for the SameSite attribute of the session cookie keep working.site.ini [Session] SameSite=is read whenCookieSameSiteis not set (CookieSameSiteis used when both are), and noted once per request at the deprecated level of the debug output (var/log/deprecated.log). Upgrade: nothing changes for visitors; renameSameSitetoCookieSameSiteinsettings/overrideand the siteaccesses to silence the note. Guide: Security and audit.- L07 (LoveStack commits fbbd333a85, e20570a7ad, b7832d110e): PHP deprecations have a debug level of their own,
eZDebug::LEVEL_DEPRECATEDwithSHOW_DEPRECATED(part ofSHOW_ALL) andeZDebug::writeDeprecated(). Withsite.ini [DebugSettings] DeprecatedLog=enabled(the default)E_DEPRECATEDandE_USER_DEPRECATEDare written tovar/log/deprecated.log, each distinct message once per request, and only counted: the debug bar shows their number (depr), they stay in the page's warning count, and none is kept in memory or listed with the messages.AlwaysLog[]=deprecatedwrites them while debug output is off as well. Upgrade note: on after the upgrade;DeprecatedLog=disabledkeeps them warnings as before. Guide: PHP deprecations and log file sizes. - L08 (LoveStack commit e39dde30ff): the size and rotation of the log files are settings,
site.ini [FileSettings] LogFileMaxSize=2097152(2 MB) andLogFileRotateCount=3, read by eZLog and eZDebug;setMaxLogSize(),setLogrotateFiles()and the constants inconfig.phpstill win over them. Upgrade note: log files are rotated at 2 MB instead of 200 KB after the upgrade;LogFileMaxSize=204800keeps the old size. Guide: PHP deprecations and log file sizes. - L09 (LoveStack commit 0bb0bac618): a template operator parameter that the template leaves out and the operator
gives no default value for is
null, with the warningOperator '<op>' has no default value for parameter '<name>', once per operator and parameter in a request; a compiled template is warned once when it is compiled and its code does not check again. New:eZTemplate::operatorParametersWithoutDefault(). Upgrade note: on for every site, only the warning is new. Guide: an operator parameter without a default value. - L10 (LoveStack commit fb20bee0d2):
site.ini [DebugSettings] UsedTemplateEditUrl=is thesprintf()format of the "Edit" links in the debug output's list of used templates (%1$seditor address with the file,%2$ssiteaccess,%3$stemplate file). Upgrade note: empty, the shipped value, keeps the link the list always had. Guide: edit links in the list of used templates. - L11 (LoveStack commit 3d18f47551):
eZDBException::getCode()is the driver's error number of the failed statement on every driver, aserrorNumber(): MySQL's number, PostgreSQL's SQLSTATE (a string), SQLite's result code, Oracle's ORA number; MongoDB keeps the server's error code inerrorNumber(). MySQL reads the error of the connection the statement ran on (the write server, when there is one), and an error is kept through the transaction's rollback. Upgrade note: PostgreSQL'serrorNumber()is the SQLSTATE instead of1; with the exception handling, MySQL on PHP 8.1 and later throwseZDBExceptioninstead ofmysqli_sql_exception. Guide: know your database driver. The "database unavailable" page (HTTP 503) also recognises aneZDBExceptionwhose error number says the connection went away or could not be made (MySQL 2002/2003/2006/2013 ..., PostgreSQL SQLSTATE class 08, Oracle ORA-03113/03114/12541 ..., SQLite 14/26):eZExecution::isDatabaseUnavailableCode(). - L12 (LoveStack commit f9691221a3): when a change would clear the view caches of
site.ini [ContentSettings] CacheThresholdnodes or more and all view caches are expired instead,eZContentCacheManagerwrites a warning with the number of nodes and the threshold (it was a debug message). Upgrade note: on for every site. Guide: caches, usual questions. - L13 (LoveStack commit 9c931192e0): location changes in the audit trail, channel
content, written fromeZContentOperationCollection:content.node.addonce per added location (the object; the new node and its parent as the target),content.node.removewith whether the removed location was the main one, andcontent.node.mainwhen an editor chooses another main location or the next location takes the place of a removed main one (causelocation-remove). The console shows them as Location add, Location remove and Main location change. The nameslocation-assign,location-removeandmain-node-updateare mapped inaudit.ini [AuditCompatSettings] Map[]andAuditFileNames[], with their attributes, soeZAudit::writeAudit()calls and imported files of those names land on the same events. Upgrade note:content.node.removeis on as before;content.node.addandcontent.node.mainstay off untilEnabled[]=content.node.add/Enabled[]=content.node.mainis set. Guide: audit reference. - L14 (LoveStack commits 55029bd473, a679a13176, 94b7b6c239):
fetch( 'content', 'site_data', hash( 'name', ... ) )reads one row ofezsite_data(the eZSiteData object, or false), read only and only for the names ofsite.ini [SiteDataSettings] TemplateReadable[](empty by default, so nothing is readable until a name is listed). Setup > Site data (setup/sitedata, policysetup/system_info) lists the rows with a filter and paging, in admin, admin3 and admin4. NeweZSiteData::createNew(),templateReadableNames(),isTemplateReadable(),fetchList()andfetchListCount(). Upgrade note: nothing is readable from templates until a name is listed. Guides: site data in templates, site data. - L15 (LoveStack commit 4d56593614): a Copy button on the full view of the administration interface (admin, admin3,
admin4), next to Move, active with create access at the item's parent as in the context menu; it opens
content/copy. Guide: copy from the full view. On for every installation; nothing to do on an upgrade. - L16 (LoveStack commit a28863d9c0): an image attribute can require an alternative text, the class attribute
option Alternative text is required (
eZImageType::ALT_TEXT_REQUIRED_FIELD,data_int2, off by default). The edit form, the online editor's upload dialog (also used by the Tiptap engine) and the AJAX uploader of relation fields refuse an image without one; the AJAX uploader has an alternative text field for classes with an image attribute and stores it on the new object. Packages carry the option. Upgrade note: off for every class; switch it on per attribute on the class edit page. Guide: required alternative text. - L17 (LoveStack commit 6a5c7dd23e): a text line attribute can trim the stored value, the class attribute option
Trim the stored value (
eZStringType::TRIM_FIELD,data_int2, off by default): spaces at the start and the end are removed in the edit form, information collection, imports and uploads. Upgrade note: off for every class. Guide: trim the stored value. - L18 (LoveStack commit 95d86f5990): a PHP class can make object names and URL alias names:
{ClassName}at the start of a class's name pattern, for classes listed incontent.ini [NamePatternSettings] ResolverClasses[](empty by default) that implementexpNamePatternResolverInterface; any other name is logged and the pattern is resolved from its tokens. NeweZNamePatternResolver::resolve(),customResolver()andlimitName(); the constructor andresolveNamePattern()are unchanged. Upgrade note: nothing changes until a class is listed. Guide: names made by a PHP class. - L19 (LoveStack commit 9c0ff40a78):
content.ini [FetchSettings] DefaultClassFilterType=is the class filter type the fetch functionslist,tree,list_count,tree_countandcalendaruse when a fetch givesclass_filter_arraywithoutclass_filter_type(eZContentFunctionCollection::classFilterType()). Upgrade note: empty, the shipped value, keeps today's results (the array is ignored);includefilters by the array. Guide: class filter default. - L20 (LoveStack commit f1b7f056b4, EZP-26501): in the edit form's Locations window an existing location starts at "Unchanged", not "Visible", so publishing no longer reveals a hidden location; a new location starts as it was given. Fix, for every site. Guide: the Locations window.
- L21 (LoveStack commits eee7f69fc3, 67eec8f972, 492a946963): the view templates of the selection and matrix
datatypes write no stray
<br />; several selected options are separated by one<br />each. Fix, for every site that does not override these templates. Guide: selection and matrix fields. - L22 (LoveStack commit e21ef46691):
content.ini [CustomTagSettings] IsHidden[]names custom tags the editors do not offer for new tags (TinyMCE 3'sezoe/tags, the TinyMCE 8 custom tag dialog, Tiptap). A hidden tag already in a text is still rendered, shown, editable and saved back unchanged; its own dialog keeps its name. New:eZOEXMLInput::hiddenCustomTags(),eZOEXMLInput::offeredCustomTags(), the keyhiddenofgetCustomTagDefinitions(). None is hidden by default, so nothing changes on an upgrade. Guide: Hidden custom tags. - L23 (LoveStack commit 63334f1681, the custom_tags_controls plugin, Copyright 2014, Mugo Web): custom tag controls
in the Online Editor with
ezoe.ini [EditorSettings] CustomTagControls=enabled. A focused custom tag gets a bar to move it up or down, edit it and remove it (its content stays), also with Alt+Shift+Up, Alt+Shift+Down, Alt+Shift+E and Alt+Shift+Delete; Backspace in an empty block custom tag removes it; every block custom tag shows its name as a label (ezoe_customtags.css); a tag's dialog showsimages/editor/custom_tags_thumbs/<name>.pngfrom a design as an example. TinyMCE 3 gets the new pluginexp_customtag_controls(TinyMCE 3's plugin API and ezjscore's jQuery 4, no YUI), TinyMCE 8 the buttons in its custom tag context toolbar, Tiptap the same in exp_oe_tiptap 0.1.2. New installations writeCustomTagControls=enabled. Upgrade note: the shipped value isdisabled, so the editor stays as it was; setCustomTagControls=enabledinsettings/override/ezoe.ini.append.phpand clear the INI cache and the ezjscore packer cache. Guide: Custom tag controls. - L24 (LoveStack commit 3a8c14bc0c): the sub/sup custom tag dialog of TinyMCE 3 closes on OK. Exponential's
tag_custom.tplalready had the fix (from the same author's later upstream commit); tested in the browser on TinyMCE 3 and Tiptap (TinyMCE 8 has its own sub and sup buttons). Nothing changes on an upgrade. - L25 (LoveStack commit f2284944b2):
eZURI::build( string $url, bool $full = false, bool $htmlEscape = true )builds a link within the site in one call, the same as theezurloperator gives (index file and siteaccess in front; with$fullscheme and host).$htmlEscape = falsefor a URL in a header or a JSON answer. Addition only. Guide: URLs, for developers. - L26 (LoveStack commit df07cb1ef5):
eZMailTransport::send( eZMail $mail, ?string $transportType = null )sends one message with another transport ofsite.ini [MailSettings] TransportAlias[](for examplefile) without changing the setting; the mail gate applies as before. Addition only. Guide: testing without sending mail. - L27 (LoveStack commit 627004e5c4): the template operators
rawurlencodeandrawurldecode(template.ini [PHP] PHPOperatorList[]), RFC 3986 encoding with%20for a space. Available in every installation, upgrades included;urlencodeandurldecodeare unchanged. Guide: encoding a value for a URL. - L28 (LoveStack commit f7c382bc2b):
[Include <pattern>]inside a settings file reads other files at that place: the pattern is relative to the including file, wildcards are allowed, the files are read in name order, includes nest, a loop is read once with a warning and a pattern without a match is a notice. The included files and the directories their patterns look in are part of the INI cache ('includes',eZINI::includedFiles()), soezcache.php --clear-tag=ini, the INI step ofexp:velocity deployandexpVelocity::refreshIni()pick up their changes; with the file time check on, a changed, added or removed included file rebuilds the cache by itself. The settings page, the classic settings editor andexp:inikeep the line as written and do not expand it. Only active where a file has such a line; parse time of files without one measured unchanged (an A/B in one process over 174 files, within the host's noise). Guide: splitting a large settings file. - L29 (LoveStack commit 97690e9c93):
error.ini [HTTPError-<code>] HeaderList[<name>]=<value>adds response headers to an error page after the status lines ofHTTPName; they are stored with the page in the content view cache and sent again from it (expErrorHeaderList). An invalid header name or a value with a line break is left out with a warning;HeaderList[Status]stands for a missingHTTPName. Empty by default, new installations and upgrades alike, so nothing changes until an entry is added. Guide: response headers of error pages. - L30 (LoveStack commits 3d2d337b0d, 3af1ec3aee): the ezjscore search (
ezjsc::search, used by the editor's embed and link dialogs and the relation search) sorts eZ Find results byezfind.ini [SearchSettings] SortBy[]; ezjscore shipsSortBy[published]=desc, the order it always used, so nothing changes on an upgrade. Without eZ Find or the setting the order stays newest first. Guide: the order of search results. - L31 (LoveStack commit 856a95e240): every node view sets the override key
layout(''outsidelayout/set, the layout oflayout/setkept), through the neweZTemplateDesignResource::setKeyValue( string $key, $value, bool $override = true ). Guide: the layout key. - L32 (LoveStack commit f435c2f91e):
site.ini [URLTranslator] DecodeOncedecodes the address of a request once instead of twice, so/(tag)/bar%2Bbargives the view parameterbar+barinstead ofbar bar.new eZURI( $uri, $decode )andsetURIString( $uri, $fullInitialize, $decode )take whether the string is encoded (null: follow the setting);eZURI::decodeOnce(). PHP-FPM and Velocity hand the kernel the same raw address, and the response cache keys are made from it, so they do not change. Upgrade note: off as shipped, on new installations and after an upgrade (DecodeOnce=disabled); to switch it on, setDecodeOnce=enabledinsettings/override/site.ini.append.phpafter checking links that put an encoded+or%in a view parameter. Guide: Decoding and cleaning addresses. - L33 (LoveStack commit 7517c0e925):
site.ini [URLTranslator] TrailingCleanupmakes the double quote a reserved character of the IRI alias cleanup (urlalias_iri), so it becomes a separator and is stripped at the ends like!, dots and separators (eZCharTransform::trailingCleanup()); the other transformations already strip them. Upgrade note: off as shipped (TrailingCleanup=disabled); withTrailingCleanup=enabledinsettings/override/site.ini.append.phpexisting aliases stay until their object is published again, and the old alias then redirects. Guide: Decoding and cleaning addresses. - L34 (LoveStack commit 3774a54ead): a number in a text beyond the range of the search index's integer column no
longer stops the built-in search engine from indexing the object; it is stored as the limit of the column. Each
driver states the range of its integer columns,
MAX_INTandMIN_INT(signed 32-bit for MySQL, PostgreSQL and any other driver, signed 64-bit for SQLite and MongoDB), andeZDBInterface::clampInteger( $value )keeps a value within it, also one written as a float (1e30), which a plain(int)turned into a wrong number. On for every installation; nothing to do on an upgrade. Guide: Numbers in the built-in search index. - L35 (LoveStack commit 5c089f6250): the search index leaves out the relation fields of related objects, and the
relation datatypes need no recursion guard. Exponential has done the same since 2018 (EZP-27101,
eZContentObjectAttribute::metaDataArray( $attributes, true ),eZDataType::isRelationType()), so there is nothing to switch and no setting;tests/tests/kernel/datatypes/eZObjectRelationMetaDataTest.phpnow documents the behaviour, a cycle of relations included. Upgrade note: none; a site coming from LoveStack gets the same search results. - L36 (LoveStack commits 3298b53584, 89a5a73877): the hide cronjob (
hide.php) tells the search engine about each node it hides (eZSearch::updateNodeVisibility(), in Exponential since 2018), and thecontent/cacheevent gives listeners( $nodeIDs, $objectIDs )from every sender in the kernel, now also from the node's URL aliases page; a listener that takes only the node ids keeps working. On for every installation; nothing to do on an upgrade. Guide: Cronjobs. - PHP 8 review (phase 7): LoveStack's PHP 7.2 to 8.3 fixes (2017 to 2026) reproduced one by one on PHP 8.5 and 8.1,
each as a regression test; most cases were already handled in Exponential and stay as tests that document them.
Four still failed and are fixed, on for every installation (plain bug fixes, nothing to do on an upgrade):
- LoveStack commit e828c54709: the
get_classandis_classtemplate operators accept a value that is not an object without template compilation too (''andfalse, as the compiled code gives); before, a string, number or null ended the request with a TypeError. - LoveStack commit 4065ed16b3:
wash(email)of an empty value gives no "Passing null" deprecation. - LoveStack commit 75cbb1daa2:
eZHTTPToolcopes with$_POSTset to null. - LoveStack commit 1bd250d4c6:
update/common/scripts/5.4/cleanupfieldvaluerelations.phpskips relation list attributes whosedata_textis NULL or empty instead of stopping with a ValueError.
- LoveStack commit e828c54709: the
- PHP 8 review, four more sites from the same LoveStack commits, each reproduced first on PHP 8.5 and 8.1 and
fixed; on for every installation (plain bug fixes, nothing to do on an upgrade):
- LoveStack commit 4065ed16b3: the classic HTML debug report (used when the Exp Debug bar is not there) shows a message whose label or text is null without a "Passing null" deprecation, as the debug bar already did.
- LoveStack commit 4065ed16b3: the Exp Debug bar shows a message whose text is null or a number without a
"Passing null to strncmp()" deprecation when xdebug is loaded (
expDebugBarReport::messageRows()). - LoveStack commit 4065ed16b3:
ezurl,ezrootandeZURI::transformURI()of a number (a node id) give no "Trying to access array offset" warning, andnew eZURI( null )(the command line before a request URI is known) is an empty address.eZURI::build()gives the same links as before. - LoveStack commit 1bd250d4c6:
eZPostgreSQLDB::escapeString( null )gives'', as the MySQLi, SQLite3 and MongoDB drivers do, instead of passing null tostr_replace()andpg_escape_string().
- LoveStack commit 5a17c08bfc: a customer's orders are found from the order lists, the archive, the customer list
and the shop dashboard when the e-mail address has a "+" (or "?", "#"). The links
rawurlencodethe address and shop/customerorderview no longer decodes it a third time; older links still work. On for every installation. Upgrade note: a site design of your own that links toshop/customerorderviewcan add|rawurlencodeto the address in the same way. - LoveStack commit cb8a3a1c58 (the spell checker part):
ezoe/spellcheck_rpcreads the request body fromphp://inputand never from the removed$HTTP_RAW_POST_DATA, GoogleSpell decodes character references without the/emodifier, and the spell checker and JSON classes have__construct(). Exponential already had all three; checked on PHP 8.5, a test keeps them (ezoeSpellcheckRpcRawInputTest). - The template operators
urlencode,urldecode,rawurlencodeandrawurldecodegive an empty text for a value that is not set, with and without template compilation, instead of handing null to PHP (a "Passing null" deprecation on PHP 8.1 and later). Found by the L27 tests; a plain fix for every installation, nothing to do on an upgrade (compiled templates take it when the template cache is cleared). - Tests (phase 8): every feature above is covered in the PHPUnit suite, each test file running on its own and,
apart from the tests marked live, without a database. New: the settings an upgraded site keeps
(
expShippedFeatureDefaultsTest), the Copy button of the full view (L15,eZAdminFullViewCopyButtonTest), the visibility the Locations window preselects (L20,eZEditLocationsVisibilityTest), the selection and matrix view templates (L21,eZSelectionMatrixViewTemplateTest) and the image check of the AJAX uploader (L05,ezjscServerFunctionsAjaxUploaderImageTest). LoveStack's own test cases for behaviour Exponential has are in the suite as well, with thanks to Mugo Web: the autolink operator (LoveStack commit d894b9e885), sorting by the trash date (e280ff5d64), the name length limit througheZNamePatternResolver::resolve()(95d86f5990), a bare&in a link address of the simplified XML input (5286068d5b), the REST request parser's recorded requests (30c1260b84, 09e49ce1ff), a password change that updates the account details of the user attribute (60a40cdc05, a live test that rolls back what it writes) andeZSiteData::create()andcreateNew()(55029bd473, 4a27ffecb4; the stored row in a live test that rolls back).
Required packages (root composer.json, 2026-10-10 to 2026-10-12)
The constraints of the root composer.json that changed between 2026-10-10 and the last commit of 2026-10-12.
| Package | Required at the end of 2026-10-12 | Why it was added or repinned |
|---|---|---|
| expai | ~0.0.1.0 | new: Exponential Intelligence, the writing assistant of the administration interface |
| expqueue | ~0.1.0 | new: the job queue for Exponential |
| exponential-velocity (suggested) | ~0.0.4.51 | the warm-up's request ends before the pool forks, and a request's $_SESSION ends with it (0.0.4.51); a HEAD over HTTP/2 is its headers only and a stop drains its connections (0.0.4.50) |
| exp_adminui | ~1.0.0.9 | the content finder box in the content structure menu (1.0.0.7), the Open Graph image (1.0.0.8) and the license block (1.0.0.9) of its metadata edit form |
| xrowmetadata | ~1.4.10 | an Open Graph image per object and per class; content licenses at installation, class and object level, from a catalogue of 1,332 licenses |
| sevenx-themes-media | ~1.5.54 | one og:image per page (1.5.51); the Sign Up button of the login page only for a visitor who may register (1.5.54) |
| cjw_themes_jumper | ~0.1.3 | the starter design prints Open Graph and Twitter tags (0.1.2); the register link of its login page only for a visitor who may register (0.1.3) |
| explayouts / explayouts-core / explayouts-ui / explayouts-ui-api | ~1.4.24 / ~1.0.7 / ~1.3.14 / ~1.3.15 | a block copy takes its collections, query and child blocks along; the layouts lists show the minutes of the modification time; the layouts pages belong to the Layouts tab and the template editor works again, on .tpl files in its allowed roots only (1.4.24) |
| ezoracle | ~2.3.6 | long multibyte texts load; the reserved column names comment and mode |
| xrowextract | ~2.5.9 | passes its CI check on PHP 8.1 to 8.5 |
| sevenx_authentication_2fa | ~1.0.6 | allows PHP 8.0 like the kernel |
15 October 2026: 89 new locales and one page for every language
Content can now be written in the major languages of the world: 89 locale files were added to share/locale
(138 in all), and every language Exponential supports, at which level, and how to add more is described in one
place, Languages.
Added
- 89 locales, generated at build time from Unicode CLDR 44 (ICU 74.2; Unicode License v3, named in each file):
South Asia
ben-BD,ben-IN,urd-PK,urd-IN,mar-IN,tel-IN,tam-IN,pan-IN,pnb-PK,snd-PK,guj-IN,kan-IN,mal-IN,ori-IN,asm-IN,bho-IN,nep-NP,sin-LK,pus-AF,eng-IN; Southeast and East Asiavie-VN,tha-TH,msa-MY,fil-PH,mya-MM,khm-KH,lao-LA,jav-ID,chi-SG,eng-SG,mon-MN,uig-CN; Middle East and Central Asiafas-IR,ara-EG,ara-AE,kur-TR,aze-AZ,kaz-KZ,uzb-UZ,kat-GE,hye-AM,tgk-TJ; Africaswa-KE,swa-TZ,amh-ET,hau-NG,yor-NG,ibo-NG,pcm-NG,zul-ZA,xho-ZA,afr-ZA,som-SO,orm-ET,tir-ET,kin-RW,mlg-MG,sna-ZW,eng-ZA,por-AO; Europeron-RO,bul-BG,slv-SI,lit-LT,lav-LV,est-EE,isl-IS,gle-IE,cym-GB,eus-ES,glg-ES,sqi-AL,mkd-MK,bos-BA,bel-BY,mlt-MT,ltz-LU,ger-AT,ger-CH,dut-BE,fre-CH,ita-CH,eng-IE; Americasesl-AR,esl-CO,esl-CL,esl-PE,esl-US,que-PE. Existing language codes are reused for regional variants (ger,esl,fre,dut,ita,ara,eng,chi,por); a new language gets its ISO 639-2/T code (ben,urd,vie,fas,ron...), Western Punjabi in Shahmukhi scriptpnband Nigerian Pidginpcm(ISO 639-3). Each file is UTF-8 with names in English and in the language, the BCP 47 tag, date and time formats, day and month names, the first day of the week, number and currency formats and country names. Where CLDR's 12-hour clock uses a marker other than am/pm, the file uses the 24-hour clock, sinceeZLocaleonly prints am/pm. The right-to-left languages (fas,urd,pus,pnb,snd,uig,ara) carry a comment;eZLocalehas no direction setting. - Languages: locales against administration translations, content languages and siteaccesses;
the support levels (full interface and content, partial interface, content only); every locale with its English
and native name, region, direction and release; every administration translation with its completeness measured
two ways (its own file, and the 13,620 strings of today: 100%
eng-US, 99%ger-DE, 20 to 48% for the others); the 38 languages with 50 million or more speakers and their locales (all 38 now, 18 before) and translations; the common languages still missing; how to add a locale (every key, the code convention, the generation, the test) and an administration translation; the right-to-left status; the limit of 62 content languages. Linked from Translations and languages, Content languages and the README. - Test
expLocaleFilesTest(no database, no PHP extension): everyshare/locale/*.iniis valid UTF-8, parses witheZINI, loads ineZLocalewith every key it reads, formats a date, time, number and amount and reads the number back; the generated files also follow the code, charset, HTTP tag, one-byte separator, license and right-to-left conventions.
Updated
- Setup > Languages > Add a language offers 132 locales instead of 43. An entry no longer repeats the country when the name already gives it:
German (Austria) · Deutsch (Österreich) · ger-ATinstead of... · Austria · ger-AT; the search still finds the country (Translations::localeChoices()). - Setup > Languages says what the limit counts: the add form reads "%used of at most %max content languages are
in use. Only the languages added here count, not the locales in the list below; removing a language no content
uses frees a place.", the overview "More can be added (at most %max content languages)", and the refusal at the
limit says to remove a language that no object or class uses (admin and admin4; eng-US and ger-DE). Why the limit
is 62 (one bit of the 64-bit
language_maskper language, bit 0 for "always available") and why it is not raised (a schema change for SQL that reads the masks) is in Languages. ser-SR.ini: Thursday was stored in ISO-8859-2 inside a UTF-8 file (an invalid byte instead ofČ) and misspeltČetvrak; it isČetandČetvrtaknow.
No PHP extension is needed at run time: locales are plain INI files, intl was used only to generate them. Nothing
changes on an installation until a language is added under Setup > Languages.
15 October 2026: references on every removal page, imported ids of the media theme
Updated
- The removal confirmation of the exp_adminui, ezwebin and ezdemo designs lists the objects, Layouts blocks and
further references that use the items, like the admin, admin4 and standard designs (the reverse relation check,
content.ini
[RemoveSettings] ReverseRelationCheck); withblocktheir Confirm/OK button stays disabled. - explayouts 1.4.27 brings the provider that lists Layouts blocks whose collections name a removed or hidden node.
- sevenx_themes_media 1.5.58 ships its
contentcheck.ini [ReferenceSettings] ImportedIdRemotePatterns[]andImportedIdObjectPatterns[], so the content checks of a fresh installation resolve the ids its enhanced links and layout components keep from the media site demo and report no false broken links. - Required packages: explayouts ~1.4.27, exp_adminui ~1.0.0.25, ezwebin ~6.0.18 (also the password change page of 6.0.15), ezdemo ~6.0.10 (likewise), sevenx-themes-media ~1.5.58.
- admin4 and admin4l: under forced colors the left menu's group titles (
#leftmenu-title-*, the groups' summaries) and the content tree's title nameCanvasText, so a contrast check measures what is painted (axe found 1.35:1 on them with a dark canvas:-webkit-text-fill-colorkept the light ink).
15 October 2026: each translation once in the German catalogue
Updated
share/translations/ger-DE/translation.ts: 88 messages that appeared twice (same context, source and comment, left by merges) are now there once, andeng-USloses its one duplicate. Where the copies differed (46 messages, almost all in the setup wizard and its system checks) the copy kept is the one the translator already showed, the later one, with the corrected spelling and German text instead of English; the earlier copy is kept for "Extensions" indesign/admin/setup/infoanddesign/standard/setup("Erweiterungen"), and the registration notice names 7x, as its English source does. Both catalogues still validate againstschemas/translation/ts.rng.- The German administration's header (search, user menu, site menu, color mode, side bar buttons, phone menu) is German again: an installation whose copy of the catalogue did not validate compiled its templates with the English texts, which stayed after the catalogue was corrected until the template cache was cleared.
Added
- Test
eZTSTranslatorTest::testMaintainedTranslationHasNoDuplicateMessage: the eng-US, ger-DE and untranslated catalogues hold no message twice.
15 October 2026: no "already defined" warnings from the admin4 menus and translations
Updated
- admin4's left menus (
parts/ini_menu.tpl) no longer write "Variable 'section' is already defined" and "Variable 'groups' is already defined" into the warning log on every page. The pagelayout shares its namespace with the variables a module's view sets for the page (section/view setssection, state/groupsgroups), so the menu's ownsection,groups,group_idandgroup_listnow carry the prefixini_menu_; its closing undef also no longer removes the view's variables. - The node view's Translations tab (
translations.tpl) keeps the main language's name in a variable of its own,a4_tr_main_language_name: the node view (node/view/a4_full_body.tpl) still hasa4_main_language_namewhen it draws the tabs, and each node view wrote "Variable 'a4_main_language_name' is already defined". - Require ezupdate 1.1.18, whose page header takes an empty error or message without the warnings "Variable 'error' is already defined" and "Variable 'message' is already defined" on each of its pages.
15 October 2026: the media design's registration completes again
Updated
- The registration form of the media design (
user/register) goes on to the success page again, with the activation mail where[UserSettings] VerifyUserType=email: the form disabled its Register and Discard buttons inside the submit event, and a submit button disabled while that event runs is left out of the posted data, soPublishButtonnever arrived and the view showed the empty form again without a message. sevenx_themes_media 1.5.60 disables them on the next tick, as its shop forms already do. - Required packages: sevenx-themes-media ~1.5.60.
Added
- Test
FormSubmitButtonDisableTest: no template of the kernel designs or of an installed extension disables its submit buttons inside the submit event (as the submit listener itself, in a submit listener or inonsubmit).
15 October 2026: the German administration in German below the header
Updated
lib/ezi18n/classes/eztstranslator.php: a message with an empty translation (the placeholder of a message nobody translated yet) no longer replaces a translation read before it. Extension catalogues are read after the kernel's, and some carry copies of kernel contexts with empty placeholders: ezstarrating's copy ofkernel/contentturned "Top Level Nodes" at the top of the content tree back into English. A later message with a translation still wins.- The German and English catalogues have the messages the administration asked for and did not find: the content job pages (section assignment, durations), the template view's override messages, the RAD page's content policy limitation and cronjob part filter, the workflow event statuses, the SVG upload error, the content finder and package messages, the navigation parts AdminAid and eZFind, and the audit console's labels for the content finder, sessions, deactivated users, API keys, paused cronjob parts and removed packages. The setup wizard's editor siteaccess texts and package download errors are German too.
- German translations that were English or unfinished: "Erweiterungen", "Sitzungen" and "PDF-Export" in the Setup menu, "Ablagen" on the package list, the user and status IDs, "Global (Überschreibung)", the RAD descriptions of datatypes and template operators, and the AJAX uploader, wishlist, OAuth admin and search messages that were marked unfinished. The active extensions cache is "Cache der aktiven Erweiterungen" (it read "Erweitungen").
- Setup > Cache management: the caches an extension declares in
site.ini([Cache_<name>] name=) are translated inkernel/cachelike the kernel's own; the REST and ezjscore packer caches have German names. - Setup > RAD: the labels of the survey counts are translated once, in PHP (
expRADSurvey::groupCounts()); the page translated them a second time, and each one wrote a missing translation to the debug output. - Additional node view tabs (
admininterface.ini [AdditionalTab_<tab>]) can name the context of their Title and Description withTranslationContext=; without it they are shown as written, as before. - The maintenance block of the dashboard reads "for more information" (it read "infomation"), in the template and in every catalogue's source.
- Require explayouts 1.4.28, whose Layouts and Authors tabs name their translation context ("Autoren" in German), and xrowextract 2.5.14, whose preset summaries read in the language of the page.
- Require explayouts 1.4.29: in 1.4.28 a comment in
settings/admininterface.ini.append.phpnamedtranslations/*/translation.ts, and its*/closed the PHP comment that wraps the file, so the settings parsers read a broken file (expIniRoundTripTest failed). 1.4.29 rewords the comment.
15 October 2026: the media design's header has a user menu, and its search fits a phone
Added
- The header of the media design has a user button beside the search button, on phones and desktops. Its menu
lists Sign in, Register, the basket, the sitemap and the tag cloud for visitors, and My profile, Edit profile,
Change password, the basket, the administration (for users who may log in there), the sitemap, the tag cloud and
Sign out for signed-in users, each only where the user's policies open that page. Escape closes it and returns
the focus to its button, the arrow keys move between the links, and only one header menu is open at a time.
Settings in
menu.ini [HeaderUserMenu]; the basket entry (ShowBasket=auto) appears only with theshop/buypolicy, so it stays off until a shop is installed.
Updated
- The media design's header search is a button, and on phones and tablets its open form spans the header below the bar instead of running off the left edge of the screen.
- Required packages: sevenx-themes-media ~1.5.61.
15 October 2026: the classic administration's Setup overview and pages back on its model
Fixed
- The classic administration design (design/admin) draws the Setup tab's first page (setup/menu) as the Setup
overview of admin4: every function of the Setup menu as a tile with its icon, name and description, in the groups
of the Setup menu, from the design's own menu template (
menu_layout='tiles'). It drew the setup toolbar of toolbar.ini instead, a bare list whose images the design does not have. The left menu names its first link Overview instead of the key "overview". - A check box with its text inside a field (
.a4-field > .a4-check) is no longer drawn small and bold as the field's label, in every administration design. - The trash and the URL alias page of the classic design close their frames once, so the footer stays on the dark page instead of below it on white.
- The classic design reaches 4.5:1 for text: the current left menu item and tree node, the version block of the dashboard, the disabled page links, the default button, the header row of expui's tables and the upload page's Select files button and hint (expui's colors mapped to the design's tokens).
- The classic design's language select of the node view, the search field, the template list's filter and the digest settings of notifications have accessible names.
15 October 2026: the registration result page names the account's state and offers the next steps
Updated
user/successknows the state of the account the registration made:user/registerkeeps its id in the session (RegisteredUserID, read for an hour) and the view passes$registration_state(verify_email, pending_approval, active, signed_in, error) and$registered_userto the template. Before, the page only knew whether e-mail verification was on and said the same thing to everyone.- The result page of the standard, ezwebin, ezdemo and media designs says what happened and what to do, as a live
region, and then offers the next steps in a navigation: Sign in, My profile, Edit profile, Register after an
error, Home, the sitemap and the tag cloud; the media design also Contact and, for users who may log in there,
the administration, and draws the activation page (
user/activate) the same way. - Required packages: sevenx-themes-media ~1.5.63, ezwebin ~6.0.19, ezdemo ~6.0.11.
15 October 2026: every shipped catalogue holds each message once
Updated
- The Chinese (chi-CN, chi-HK, chi-TW), Mexican Spanish (esl-MX) and Serbian (srp-RS) catalogues hold each message once (37, 81, 81, 85 and 68 duplicates removed). The copy the translator used is kept, so no page changes, except where the earlier copy was the better one: in chi-HK and chi-TW two setup texts lose a stray character and two keep their %1 placeholder, in esl-MX a setup hint reads correctly, and in srp-RS the upgrade warning names Exponential.
- The duplicate-message test covers every catalogue under
share/translations, and a change to a catalogue runs it.
15 October 2026: the newsletter pages log no warnings, and the layouts editor belongs to the Layouts tab
Updated
- Required packages: cjw_newsletter ~4.2.7, whose edition list, report and A/B test pages no longer log that
statusori18nis already defined; explayouts-ui-api ~1.3.16, whose layouts editor and JSON views belong to the Layouts tab instead of Setup.
15 October 2026: the interface language, and a UI switch framework for interface choices
Added
- Interface language (#506): a signed-in user of the administration chooses the language the interface is shown
in, in the right toolbar right after Bookmarks (box Interface, choice Interface language). It sets
site.ini [RegionalSettings] Localein memory for that user's requests: translations, date and number formats, the page'slangandContent-Language. Content languages are never changed. The choice is kept in the session for that siteaccess and that user and ends at logout;RememberPreference=enabledalso keeps it in the user's preferences. Keyboard: arrow keys, Home, End and Escape in the list; without JavaScript the list is shown open. Interface language. - The UI switch framework behind it, open to extensions:
expUISwitchProviderInterface, the base classexpUISwitchProvider, the registryexpUISwitch(called fromezpKernelWeb::requestInit()after the session start, before the locale and any template) and the first providerexpUISwitchLanguageProvider. The provider API. - The module
uiswitch:uiswitch/set(POST only, form token, refusesSec-Fetch-Site: cross-site, strict allow-list, redirects only to a local path) anduiswitch/state(JSON); one policy function per provider, built fromProviderList[], souiswitch/languageand any extension'suiswitch/<identifier>. Listed insettings/module.ini. fetch( 'uiswitch', 'toolbar' ), the toolbar tooladmin_ui_switch(design/admin4/templates/toolbar/full/admin_ui_switch.tpl,uiswitch/choices.tpl), the page of a refused change (design/standard/templates/uiswitch/refused.tpl).settings/uiswitch.ini:[UISwitchSettings] Enabled,SiteAccessList[](empty: every administration siteaccess),ProviderList[];[UISwitch_language] Class,Enabled,AllowedValues[](eng-GB, eng-US, ger-DE; a locale is offered only with its locale file and, except English, its translation),RememberPreference. UI switch settings.- AdminUI: the side bar plugin
uiswitch, the dropdown Interface after the bookmarks (exp_adminui.ini [MenuPlugins] Enabled[]=uiswitch). - admin4l: the block Admin: interface choices (language) (
admin_ui_switch) for admin layouts, and the block in the right zone after Bookmarks in the admin layouts fragment of explayouts. - The audit event
access.uiswitch.change(channelaccess, severity info), with the value and siteaccess before and after. - Translations of the new texts in eng-US and ger-DE (contexts
kernel/uiswitch,design/admin/toolbar/uiswitch,design/standard/uiswitch). - Tests:
tests/tests/kernel/classes/uiswitch/expUISwitchTest.php(18 tests, DB-less).
Updated
- The cache variant key of a choice (
$GLOBALS['expUISwitchVariantKey'], for examplelanguage-ger-DE) keeps caches apart:{cache-block}keys, the content view cache hash (keyuiswitch) and compiled template names; the role-aware HTTP page cache does not store a page rendered with a choice (ui switch). Without a choice the key is empty and no cache name changes. - New installations: the editor siteaccess's right toolbar is Bookmarks, the interface choices, the current user and
the preferences; the admin and adminui toolbars list
admin_ui_switchafter Bookmarks; the Editor role getsuiswitch/languagein the clean data (policy 332) and in the multi-site installers.
Upgrading
- The choice appears on an existing installation only after
AvailableToolArray[]=admin_ui_switchandTool[]=admin_ui_switch(afterTool[]=admin_bookmarksin[Toolbar_admin_right]) are added to the admin siteaccesses'toolbar.ini.append.php, or the block to the admin layout's right zone on admin4l, followed by a clear of the INI and template caches. Roles other than Administrator needuiswitch/language. The extension bcswitchadminlanguage is replaced and must not be installed alongside. See UI switch settings.
15 October 2026: every administration page keeps the footer on the page
Fixed
- About and Copyright (every administration design), the trash and the URL aliases (admin4 and admin4l) and the package uninstall error page closed one frame too many or too few, so the rest of the page and the footer fell outside the page's frame, onto white. A test now checks that every template of the administration designs closes the frames it opens.
- The muted text of About in the classic design reaches 4.5:1.
15 October 2026: the job queue's admin pages on the shared components
Updated
- Require expqueue ~0.1.1: Setup > Job queue no longer carries a
<style>block orstyle=""attributes. Its pages are drawn with the administration's shared components and--a4-*tokens, so they follow admin4's dark mode; what the components cannot do is in the extension'sexpqueue.css, loaded throughdesign.ini [StylesheetSettings] BackendCSSFileList. Bar heights and progress are.a4-pct-<n>classes. A DB-less test in the extension (tests/unit/StylingRulesTest.php) keeps inline CSS and colour literals out.
15 October 2026: signed links in the administrator's registration mail
Added
- The administrator's "New user registered" mail (
user/registrationfeedback.tpl) is an HTML mail with a plain-text part (multipart/alternative, with every mail transport). It lists the account's name, login, e-mail address, registration time and state (waiting for the e-mail confirmation, for an administrator or for an approval item, pending, active), and carries signed links into the administration: View account, Activate account (Approve when an approval item holds it), Reject and remove, the approval item and the list of unactivated users. A link is an HMAC-SHA-256 token under a key derived from the site secret, names one account and one action, expires after[UserSettings] RegistrationActionTTL(7 days); approve and reject links work once (expRegistrationAdminAction). Settings:RegistrationAdminLinks,RegistrationAdminBaseURL,RegistrationAdminSiteAccess,RegistrationActionTTL. Guide: Signed links in the "New user registered" mail. - The page
user/registration/<token>in the administration: needs a signed-in user withuser/activation(or the approver of the waiting approval item; a view link also withcontent/editon the account), shows the account and its state, and approves or rejects only after a confirmation posted with the form token; opening a link changes nothing. Approve activates the account as Users > Unactivated users does, or accepts the approval item and publishes the account at once; reject denies the approval item and removes the account; an active account is never changed. An invalid, changed, used or wrong link is refused with 403, an expired one with 410. Audited asaccess.registration.approve,access.registration.rejectandaccess.registration.refused. - One layout for HTML mails,
design:mail/layout.tpl, with its plain-text twindesign:mail/layout_text.tpl: content templates pass text and links, the layout holds the styles. A design overrides it for its own colors and logo; the media design does.
Updated
The registration mail no longer links to
http://<host>/content/view/full/<main node>: the account is not published when the mail goes out, so the node did not exist yet, and the link usedhttp.The mails a new user gets are HTML mails with a plain-text part through the same layout: the activation mail (
user/registrationinfo.tpl, with an Activate account button), the activation mail sent again from Users > Unactivated users (user/unactivated_resend_mail.tpl) and the "registration approved" mail (user/registrationapproved.tpl, with a Sign in button). Their links usehttps; the activation mail'shttp://<host>/content/view/full/link without a node is gone. The senders set the body throughexpMailTemplateBody::apply(), which makes the multipart mail when a template setscontent_typetext/html andbody_text.Required packages: sevenx-themes-media ~1.5.65 (the mail layout of the media design, with the site logo).
15 October 2026: the public media site fits phones, tablets and a 200% zoom
Added
- The media design draws the advanced search page itself: the form in the search page's markup, labels tied to their fields, 44px controls and 16px text on phones, results in the search view. It used ezwebin's unstyled form, whose inputs scrolled every phone sideways.
Updated
- A crawl of every public page group at 320, 375, 414, 768, 960 and 1440px in Chromium and WebKit, on both servers, found squeezed pictures, text on the screen edges of the generic full view, a narrow search input, slider arrows, links and buttons under the touch sizes, a card title covering its author link, low contrast in the search results, the basket steps and the 404 code, an empty picture link in the zigzag features block and an empty title link in the file view. All are fixed in the theme; the same crawl finds none of them now.
- The style blocks and style attributes of the media design's page templates moved to a stylesheet that packs with the theme; the sitemap and tag cloud links are 44px rows on phones; empty consent labels fall back to the checkbox's name.
- Required packages: sevenx-themes-media ~1.5.66.
15 October 2026: the country lists start with the most used countries, and autofill picks the right one
Added
- Grouped country list (#517): every country select shows Most used countries first, ranked (the United States, Canada and Mexico on top, then South America, Europe, the United Kingdom, Asia, New Zealand, Australia, Africa), then All countries A-Z by the translated name in the reader's collation (intl's Collator, or accents and case folded without it), as native option groups. Guide: The grouped country list.
settings/countrygroups.ini [CountrySettings]:GroupList[],GroupName_<group>,GroupCountries_<group>[](ISO alpha-2 codes, ranked),RepeatInAllCountries. 37 countries ship in the groupcommon.fetch( 'content', 'country_groups' )(classexpCountryGroups) and the shared templatedesign:country/options.tpl, which every country select includes; group labels in eng-US and ger-DE (contextkernel/country).
Updated
- Autofill with a United States address picks the United States, not the United States Minor Outlying Islands: the
United States is the first option, the selects carry
autocomplete="country"(the country attribute, which posts codes) orautocomplete="country-name"(the shop, which posts names), and names are no longer shortened to 20 characters. Checked in Chromium and WebKit. - The country attribute's edit, information collection and class edit templates and the shop's
shop/country/edit.tpl(account form of every design, VAT rules, user country toolbar) use the shared options. A single select repeats the ranked countries in the A-Z group and selects a stored value once; a multiple select lists each country once. The shop's select starts with an empty Choose a country option when no country is known, and takes an optionalselect_classandselect_autocomplete. - Stored values are unchanged (codes for the attribute, names in the orders), so no migration is needed.
- Required packages: sevenx-themes-media ~1.5.67 (the account form's grouped country list and autocomplete fields), exp_adminui ~1.0.0.28 (the country attribute's selects in AdminUI).
16 October 2026: the job queue pages are dense again and fit a narrow column
Updated
- Require expqueue ~0.1.2: the cards, facts and tables of Setup > Job queue are as dense as in 0.1.0 again (spacing tokens only), and each table hides its secondary columns when its own box is narrower than 760px, so the classic admin at 200% zoom shows the Priority classes and job tables without sideways scrolling.
15 October 2026: every language in the interface language list
Added
- The interface language list offers every language (#516): the Interface language choice lists the 33 most
needed locales in three groups (English, Major world languages, Regional variants, ranked by speakers and
share of web content), then every locale of
share/localeA–Z (about 130). Each entry gives the language's own name, its name in the current interface language and how complete the kernel's translation is ("Deutsch (Deutschland) — German (Germany) (99%)"); a language without a translation is offered with English texts and its own formats, marked "no translation yet". The A–Z list follows the collation of the current interface language (Intl), with a fallback without Intl. See The interface language list. - A list box with headings for long choices (
design/standard/templates/uiswitch/grouped.tpl): a native select with type-ahead and an Apply button, the same in admin4, admin4l, AdminUI and the classic admin design. expUISwitchLanguageCatalogue: the names, HTTP codes and translation coverage of every locale, cached in memory and under the cache directory (uiswitch/), built again by itself when a locale or translation file changes.- uiswitch.ini
[UISwitch_language]:GroupList[],GroupLabel[],GroupValues_<group>[],AllLanguages,ListUntranslated. See UI switch settings.
Updated
- The allow-list of the language choice is the siteaccess's Locale, the groups,
AllowedValues[]and, withAllLanguages=enabled, every locale file;AllowedValues[]is empty by default. Without the new settings the choice behaves as before (only translated languages, a list of buttons). uiswitch/stateandfetch( 'uiswitch', 'toolbar' )return the groups of a provider that has them.
15 October 2026: sorting and readable filter counts on the extensions page
Updated
- Setup > Extensions sorts by name both ways (#519): the Sort choice offers Loading order, Name (A to Z) and
Name (Z to A), with the arrow keys like the other chip groups. The sort and the filter are kept in the address
(
/setup/extensions/(sort)/name/(dir)/desc/(show)/active), so a reload or a bookmark shows the same list. AdminUI's Order and Name headings now sort descending too: the view used to ignore(dir). - The filter choices read All (87), Active (62), Inactive (25) and Problems (0): the count was a separate element in the chip, whose space was lost, so the chips showed and copied as "All87". German: Alle (87) and so on. Same in admin4, admin4l and the classic admin design. See The extensions page.
15 October 2026: accessible names in the classic admin page frame
Updated
- The classic admin design names its search field, siteaccess list and user link (#522): the search field has a
visually hidden label (
.a4-sr-only) besides its title, the Siteaccess: label of the quick settings is bound to its list, the search scope's close link has a name, and the icon beside the current user's name is no longer a second link without text. axe reports no serious finding in the page frame on 10 classic pages at 1440 px and at 960 px with 200 % zoom, on both web servers. The same label fix is in the standard design's quick settings, and admin3's disabled search field gets the name its enabled one has.
15 October 2026: the test language Lorem Ipsum
Added
- A test language for content translations (#523):
share/locale/qlo-ZZ.ini, Lorem Ipsum, a code from the ISO 639-2 range reserved for local use, English formats and lorem-style month and day names, marked[LanguageSettings] TestLanguage=true. It is shipped but added nowhere: Setup > Languages > Add a language offers it like any locale, for a test, to be removed again afterwards. The interface language choice leaves test languages out of its ranked groups and its A-Z list unlessAllowedValues[]names them. exp:loremipsum(bin/php/exploremipsumfill.php) fills the translation of objects or a subtree in a test language with lorem ipsum text per datatype (text line, text block, rich text, keywords), with--dry-run. It refuses a real language and never adds one. See The test language Lorem Ipsum and Languages, section 12.
15 October 2026: the package wizard is a step wizard
Updated
- The package creation wizard is drawn again (#520): Setup > Packages > Create new package and every step of the
content class, content object, extension and site style wizards, in admin4, admin4l and the classic administration.
Each step shows the wizard's steps (the current one marked with
aria-current="step"), "Step n of m", what it asks for, its fields in cards with labels and help, and one primary action (Continue, on the last step Create package) beside Back and Cancel, in a bar that stays in view. An error of the last Continue is listed at the top with a link to its field and shown under the field (aria-invalid,aria-describedby). The last step reviews the whole package (name, version, format, license, maintainer and what it carries) before it is written. The classes and extensions are tables of checkboxes with a filter and a count, the chosen nodes a table with their class and URL, the siteaccesses and languages of the content export chips; extensions that may not be exported are folded away with the reason once. The posted names, what the creators read and the packages they write are unchanged (the same packages, byte for byte apart from their dates). See The package wizard. - Remove selected on the content step with no row ticked removes nothing; it stopped with an error before.
16 October 2026: right-to-left languages, Hebrew, Latin, Klingon and 81 more locales
Added
- Text direction of locales: a locale file can say
[LanguageSettings] TextDirection=rtl;eZLocale::textDirection(),isRightToLeft()andtextDirectionOf()read it (template attributestext_direction,is_right_to_left), and$site.text_directioncarries the direction of the interface language. See Right-to-left languages. - Right-to-left administration: with a right-to-left interface language (UI switch) the pagelayouts of admin4,
admin4l, the classic admin design and AdminUI print
dir="rtl", and the layout skeleton (header, side columns, toolbar, tables, forms) is mirrored through logical CSS properties; left-to-right pages are unchanged. - Right-to-left content: text in a right-to-left content language gets its language tag and
dir="rtl"in the edit forms, the admin full view and the rich text editors (TinyMCE 3 and 8, Tiptap); the Add language list marks right-to-left languages. - 81 more locales (219 in all): every right-to-left language with CLDR data (28, among them Yiddish, Central
Kurdish, Syriac, Dari, Kashmiri, N'Ko and Dhivehi, and nine more Arabic countries), 62 world languages and regional
variants from CLDR 44, Latin (
lat-VA) and Klingon (tlh-QO, private-use region QO). See Languages. - Interface translations (first drafts, to be reviewed by native speakers): Hebrew, Latin and Klingon. Hebrew is in the Major world languages group of the interface language list.
- Complete administration translations for Spanish (Spain and Mexico), Hebrew, Latin and Klingon, machine-assisted, with a quality test for placeholders, markup and whitespace.
- Font fallbacks for CJK, Thai, Devanagari, Arabic and Hebrew script after the Latin families of the admin font stack, and line-breaking rules for Japanese, Korean and Thai.
Updated
- Hebrew (
heb-IL), Hindi (hin-IN) and Korean (kor-KR) are generated from CLDR again (double-encoded month names, English names, an invalid Korean date pattern); Chinese, Greek, Georgian and Ukrainian get small fixes. eZLocale::localeList()no longer returns whatever the first call of a request asked for, and sees a locale file added while a worker runs; the interface language list does the same (no restart needed for a new language).- The HTTP cache figures of Setup > System information are translatable.
- Right to left inside the pages: the module stylesheets of admin4 and the classic design use logical
properties, the data table pagers' arrows turn with the direction, and AdminUI 1.0.0.32 mirrors its pages too: its
own stylesheets use logical properties, a right-to-left copy of its reference stylesheet (
style-rtl.css, built fromstyle.css) is loaded in place of it, and the icons that point along the line and the content tree column's resize handle follow the direction. Left-to-right pages are unchanged. - Names keep their own direction: node names, the path, page headings, the user name, author lines, relation
lists, dashboard blocks and the content tree isolate their text (
<bdi>,dir="auto", and Unicode isolates in the page title of a right-to-left page), so an English name in a Hebrew page, or a Hebrew name in an English one, keeps its punctuation in place. Left-to-right content shown in a right-to-left interface carriesdir="ltr"and itslang(parts/exp_content_direction.tpl,exp_dir_ltr). AdminUI 1.0.0.33 and 1.0.0.35. - Accessible names in the edit form and AdminUI: the classic design's and AdminUI's attribute labels point at their fields, the online editor's frame has a title (TinyMCE 3 and 8), the relation search field, the tags field (eztags 2.4.13), the image upload field (ezie 6.0.11), the preview toggle (expui 1.0.0.7) and AdminUI's sort, section and class list controls have names; every page checked reaches 0 serious or critical accessibility findings, light and dark. AdminUI 1.0.0.34, expui 1.0.0.8.
- The accent text color (
--a4-accent-text, light) is darker, so an accent badge reaches 4.5:1 on a hovered or striped table row. - The package wizard's creator choices wrap inside their cards at narrow widths, with their descriptions in normal weight; the node view's action buttons wrap instead of running past the card with long words (Latin, German).
15 October 2026: the classic menus in the Design menu, Site and URLs reordered
Updated
- Menus (classic) (
visual/menuconfig) left the Setup menu and the Setup overview (#528): it is a link of the Design tab's left menu now, after Toolbar management, in admin4, admin4l, the classic administration and AdminUI, and its page marks the Design tab and draws the Design menu. Its address, its access (visual/*), its icon and its description are unchanged; with the Design tab left out it is a tile of the Design group of the overview. - Site and URLs in the Setup menu and the Setup overview opens with Preload Sites, RSS and Packages and ends
with RAD; the other links keep their order (
menu.ini [Leftmenu_setup] GroupLinks_site[]andLinks[]).
16 October 2026: the upgrade check names the tables of extensions that are not active
Updated
- The database part of the upgrade check (Setup > Upgrade check,
expSchemaConsistencyReport) also reads the schema files of the extensions that are installed and not active (share/db_schema.dba, and theCREATE TABLEstatements ofsql/<engine>/*.sql). A table in the database that only such an extension names, for exampleezfind_elevate_configurationof ezfind, is no longer a "Table not in the schema" with a DROP statement: it is listed as "Table of an extension that is not active", "belongs to ezfind (not active)", with the advice to activate the extension or leave the table, since dropping it removes its data. No SQL is written for it, on its card, in All statements or in the SQL download, and it does not fail the check. A table that no schema names keeps its DROP statement as before. Guide: upgrade check. - Tests: the upgrade check reads the tables of extensions that are not active and writes no DROP for them; a fresh
installation creates
ezuser.loginUNIQUE on SQLite (fromshare/db_schema.dba, read back and refusing a second account with the same login), MySQL and PostgreSQL. A database installed before the index became unique (12 October 2026) still reports it, with the two statements that change it;./console exp:user-login-check --fix-indexruns them once no two accounts share a login.
16 October 2026: admin4 review, account actions, the site menu, the badge and Broken relations
Fixed
- Check user accounts (
user/hygiene) carries out a bulk Disable, Enable or Unlock only from its own confirmation page: Confirm needs the token of that page, kept in the session (HygieneToken), as Deactivate and move does, so the actions no longer rest onezformtokenalone. - Check user accounts and Deactivate and move count an account as disabled or enabled only when the
user/setsettingsoperation continued. A workflow that cancelled or halted it left the account enabled, while the result said it was disabled and Deactivate and move moved it. - An administration without
RelatedSiteAccessList[]lists only itself beside the site in the site menu (site.ini [AdminSiteAccessMenu] List=related), not every siteaccess ofAvailableSiteAccessList[]. - The environment badge takes
BadgeDisplay,ShowInTitleandShowVersionin any case and with surrounding spaces, as it already tookLevel;BadgeDisplay=AlwaysorShowInTitle=Disabledwere ignored. - The links of Broken relations without a filter no longer end in a bare question mark.
Tests:
expUserHygieneTest,expUserDeactivationTest,AdminSiteAccessMenuTest,AdminEnvironmentBadgeTest,expContentCheckQueryTest.
Upgrade notes
- An installation that relied on the site menu listing every siteaccess without
RelatedSiteAccessList[]setsRelatedSiteAccessList[]for its administration, or[AdminSiteAccessMenu] List=available.
16 October 2026: admin4 review, template warnings and small template fixes
Fixed
- The admin4 left menu (
parts/ini_menu.tpl) and the relations window (relations.tpl) no longer log "Variable 'check'/'offset' is already defined": they are$access_checkand$relation_offset. admininterface.ini [AdditionalMenuSettings] ImportExportMenuTemplateArray[]is declared, so the Import/Export entry of the content menu no longer logs a missing variable where no extension adds an item.- Cancel on a browse page opened with the parameters of an old edit session no longer leads to a "not available"
error:
content/browsedropsBrowseParameterswhose return page is the edit page of a missing object or version (neweZContentBrowse::editReturnTarget()). - The admin4 dashboard says "Nothing was published in the last 14 days." instead of showing an empty chart box.
- The toolbar editor draws a preview image only for tools that have one (no broken images for the
admin_*tools). - Seven admin4 templates close their
{default}block; the class view of admin and admin4 shows the background process monitor link only where thescriptmonitormodule exists; the relation list editor passesfetch( 'content', 'object' )only parameters it knows.
Updated
- Seven German strings of the admin and ezoe's file table headings (Name, Size, Type) are no longer marked
unfinished. Tests:
AdminReviewTemplateWarningsTest,eZContentBrowseEditReturnTargetTest.
16 October 2026: admin4 review, the search page
Fixed
- A new search from the admin4 search page keeps the section (a choice on the page now), the locations and the page
size (
SearchPageLimit, 1 to 5 as in the advanced search, at mostsite.ini [SearchSettings] MaximumSearchLimit); the pages of the advanced search keep their locations. - An empty admin search shows "Type the words to search for." without running the search, also with an engine that allows an empty search.
- The header's section can be chosen on pages without a location; only an edit page has no search.
- The search forms are named landmarks ("Search", "Advanced search", "Quick search"), the header's scope button
announces a dialog, each result checkbox names its item, the location choice is a labelled group, and the class
attribute label of the advanced search points at a field only when there is one.
Tests:
AdminSearchTest.
16 October 2026: admin4 review, the stylesheets
Fixed
- The checkbox and radio rows of the settings, class, RSS, history and other admin pages no longer inherit the
sign-in page's extra top margin and larger text (
.a4-checkis scoped to the sign-in page). - The grey edge on neutral badges is limited to the node view's title row; badges elsewhere in the admin are one flat tint.
- The primary button of expui dialogs, the upload button and a picked date use an orange that carries white text (5.2:1, was 3.1:1, and 2.3:1 in dark mode); focus rings of these widgets and of the language badge are the 3 px admin ring (the badge's was 2.8:1 on a current badge in dark mode).
- The node view's action row (language choice, Edit, Move, Remove, icon buttons) wraps on tablet widths instead of being cut off at the card's edge.
- The tree's open/close arrows and "Actions for ..." buttons, and the sub-items row menu button, have a 24 x 24 px click target without changing how they look, left to right and right to left.
- The sub-items row menu button (three dots) and the image icon stay visible in forced colours.
- Long pop-up menus (user menu, mega menu, context menu) fit the visible screen on iOS and Android (
100dvhafter100vh). - The password rules' check mark is readable in dark mode and in high contrast.
- The in-place confirmation's summary (
.a4-confirm > summary) has one rule of its own incomponents.css(both copies).
Updated
- The IcoMoon font of the pager loads as woff only with
font-display: swap, and its glyphs are no longer read out; small helper text keeps at least 12 px. - Removed the dead legacy modal-window rule that could fix and shift any ancestor of such a window, and a
duplicated block of the sub-items table stylesheet.
Tests:
AdminCssReviewFixesTest,AdminResponsiveTest,AdminTokenContrastTest.
16 October 2026: a Content Security Policy, report-only on the administration
Added
site.ini [ContentSecurityPolicy]:Mode(disabled,report-only,enforce, orauto: report-only on administration siteaccesses, whoseSiteDesignbegins withadmin, disabled elsewhere; the default), the policy inDirectives[](shipped:default-src,script-srcandstyle-src'self',object-src 'none',base-uri,form-actionandframe-ancestors'self', images and media withdata:/blob:) andReportURI. A report-only policy is the headerContent-Security-Policy-Report-Only; an enforced one is sent as a second policy ofContent-Security-PolicyafterSecurityHeaders[Content-Security-Policy];frame-ancestorsandsandboxonly when enforced (browsers ignore them in report-only). The policy is sent also whenSecurityHeaders[]is not set. Values that would split the header are ignored.object-src 'none'blocks<object>/<embed>(the legacy plugin previews, inline PDF) once enforced. See Content Security Policy.
16 October 2026: the administration runs without inline scripts, handlers and styles
Updated
- admin4 and admin4l (and the admin3, admin and standard templates they render) carry no inline
<script>, noon...=attribute, nojavascript:link, nostyle=""attribute and no<style>element any more, so they run under the shipped Content Security Policy (script-src 'self',style-src 'self'). Page scripts are files underdesign/admin4/javascript/page/, loaded withezscriptwhere the inline script stood, and take their values fromdata-*attributes; the head script isa4_head.js. See Content Security Policy, "The administration without inline code". - Context menus are declared in
data-menu-*attributes (menu, items, openers) instead ofmenuArrayscripts andonmouseover/onclickattributes; menus and items of extensions written the old way keep working, also when they add items to a core menu (under report-only; once enforced their inline scripts and handlers do not run - convert them before enforcing). A script that assignsmenuArrayanew replaces the declared core menus: add to it instead. A menu item links and a redirect entry (data-menu-redirect,ezpopup_SubstituteAndRedirect()) goes only to an address of the site (http or https, the page's origin); ajavascript:,data:or foreign address is refused with an error in the console, and the values put into a redirect address are URL-encoded. - The content tree is drawn from
data-tree-*attributes (JSON) instead of an inline script withdocument.writeln(); its open/close buttons have noonclick/onkeydownattributes. - ezjscore: generated script and style content (server functions with packing off) is a file in the packer's
cache directory instead of an inline
<script>/<style>(ezjscPacker::generatedContentURL()). - The sub-items status badges use the admin's
a4-badge(token colours, dark mode) instead of fixed inline colours. - ezoe, TinyMCE 8: the edit template writes the editor settings as JSON data blocks
(
<script type="application/json" id="ezoe8-settings">,id="ezoe8-button-map") and each textarea's toolbar indata-ezoe8-*attributes;javascript/tinymce8_ez/ezoe8_start.jsstarts the editors, so a rich text edit page with TinyMCE 8 carries no inline script.eZOe8Init(),eZOe8Toolbar()andeZOe8StickyOffset()stay global; neweZOe8Start()starts editors added later. TinyMCE 3 and its dialogs are unchanged and still inline (the guide says what is left). The data blocks are printed with<and>as\u003cand\u003e:json_encodeleaves<!--as it is, and<!--<script>in a setting would have made the browser read past the block and skip the scripts after it, so the editor did not start. Withouttinymce.min.jsthe console says why the fields stay plain text. - The test of the administration's templates (
AdminContentSecurityPolicyTest) follows the chain the kernel builds: admin2 is part of it, and the design directories of the active extensions come before the core directory of each design. What extensions still have inline is listed per extension (EXTENSION_ALLOWLIST) and only shrinks.
Added
design/admin4/javascript/a4_actions.js: confirmations, select all, dismiss, submit or go on change and focus, declared withdata-a4-*attributes.
Fixed
- Package > Compare and import: All/None and the class cascade work (their inline script had a syntax error and never ran).
16 October 2026: oauth/authorize redirects only to a client's end point, the publishing status needs read access
Fixed
oauth/authorizesent an error for a missing client ID, an unknown client or a mismatchingredirect_urito theredirect_uriof the request, so any address could be reached through the view (an open redirect). Until theredirect_uriis the end point of a registered client the view now answers400 Bad Requestwithout aLocation(RFC 6749, section 4.1.2.1); aredirect_uriorclient_idgiven as an array is refused the same way. Errors after that check (a missingresponse_type,access_denied) still go to the client's end point.- The ezjscore server function
ezpublishingqueue::statustold any caller, also an anonymous one, whether a version of an object exists and its publishing status, and the address of the node once it was published. It answers now a user who can read the object, or the registered user who created the version and waits for it; the address of the node only a user who can read the object. For anyone else the answer is the one of a version that does not exist ("Object version not found"), and arguments that are not positive integers get the same answer. - The asynchronous publishing page (
content/queued) hides its link "View the published item" when the status comes without the node's address, instead of leaving it pointing at#.
16 October 2026: ezurl and ezroot escape an absolute link in quotes
Fixed
ezurlandezrootgave a link with a scheme (https:,mailto:,javascript:,data:) or a protocol-relative//hostone back unescaped, also when they put it in quotes for an HTML attribute; a stored link with a"closed the attribute (<a href={$url|ezurl}>). In quotes (no quote parameter,'double','single') such a link is now escaped as the links the operators build already are (an&already in it stays as it is). Relative links and quote'no'are unchanged;eZURI::transformURI()itself is unchanged. Compiled templates take the change after the template cache is cleared. In a<script>block, JSON or a CSSurl()such a link in quotes now carries&for&, as relative links already did; there use quote'no'with|wash( 'javascript' )or|json_encode.- The filter redirects of
package/list,package/compareandpackage/viewsend their address without HTML escaping, so an&in it no longer reaches theLocationheader as&.
16 October 2026: online editor hardening
Updated
- The custom tag dialog (
ezoe/tags/<id>/<version>/custom/<name>) opens only for a name of content.ini[CustomTagSettings] AvailableCustomTags[](or no name, for a new tag); any other name gets the "Invalid parameter" message.tag_custom.tplescapes the name in its script string and its class attribute as well: a link with a crafted name ran script on the administration for the editor who opened it. The "Invalid parameter" messages of the view escape the value they repeat. ezoe/embed_viewshows an embed whose EmbedID has no_, whose node or object no longer exists, as the unknown embed; it ended in a fatal error (HTTP 500) before, which broke the editor on an old draft. The EmbedID is read withRelations::parseEmbedId()likeezoe/relationsandezoe/load, theviewparameter must be the tag's own view (embed, orembed-inlinefor an inline embed) or one of content.iniAvailableViewModes[]of that tag's sections only ([embed]and[embed_<class>], or[embed-inline]and[embed-inline_<class>]), otherwise the tag's own view is used, andclass,size,alignandvieware read as strings.- The upload of the online editor refuses executable types for every engine: a file name with
php,phtml,phar,sh,cgi,.htaccessand the like anywhere in it is refused in every ezoe.ini[EditorSettings] UploadExtensionCheckmode,disabledincluded. Every dot-separated part of the name is checked, not only the last one, soreport.pl.pdforshell.php.jpgis refused too; rename such a file before uploading. TinyMCE 3 users skipped the check in the default modeengine, sox.phpwas accepted, which is dangerous on a web server without the.htaccessrules (nginx).UploadExtensionCheckstill decides about the listUploadFileExtensions[].
Removed
- The After the Deadline spell checker of the TinyMCE 3 editor: the view
ezoe/atd_rpc, which sent the editor's text over plain HTTP toservice.afterthedeadline.com(a service that no longer answers) and appended the debug report to the answer, the pluginjavascript/plugins/AtD, the commentedPlugins[]=AtDandButtons[]=AtDand the[AtD]block of ezoe.ini, and theatd_options of the editor template. A siteaccess that still listsPlugins[]=AtDorButtons[]=AtDin an override should drop them; TinyMCE 3 skips a plugin it cannot find. The spell checker of TinyMCE (ezoe/spellcheck_rpc) stays.
16 October 2026: exact link registration, formula text in the online editor, ForceEngine
Added
- ezoe.ini
[EditorSettings] ForceEngine=<engine>(per siteaccess, not set by default) gives every editor of a siteaccess one engine of the online editor, whatever their own choice onezoe/enginesays; the engine page then shows a note instead of its form. An engine that cannot be used (its files or its extension missing) is ignored, so editors keep the usual choice.
Fixed
eZURL::registerURL()andeZURL::urlByURL()match an address byte for byte. Theurlcolumn compares with its collation (MySQLutf8mb4_general_ci: case and accents ignored), andregisterURL()rewrote the row it found to the new spelling, so saving a text that links.../Present_perfectre-pointed every link stored as.../Present_Perfect(another page) to it. A different spelling gets a row of its own now (eZURL::fetchExactByUrl(),eZURL::pickExact()), andurlByURL()finds it, where it returned false for two collation-equal rows and the rich text datatype stored another row on every save.- Online editor: a custom tag whose text is
0keeps it (it became a space), and line breaks inside a custom tag (a LaTeX formula) become a space instead of gluing the lines together. eZContentObject::versionLanguageName()with version 0 or a negative version returns false without a PHP warning and without a database query.oauth/authorizerefuses an emptyredirect_urialso for a client stored without an end point (400 Bad Request).- The online editor's upload refuses an executable type followed by white space, NUL or a dot (
x.php,x.php.) and alsophtm,php6andshtml; the engine page escapes the engine names it shows.
Renamed
ezpContentSecurityPolicy(new in this release) isexpContentSecurityPolicy(kernel/private/classes/expcontentsecuritypolicy.php). The security headers are sent without the policy while a long-running process does not know the class yet.
Updated
AdminContentSecurityPolicyTestchecks the core designs and the extensions this repository ships (expservices, ezformtoken, ezjscore, ezoe). Extensions installed from their own repositories are checked withCSP_ALL_EXTENSIONS=1, an audit of an installation before the policy is enforced (see Content Security Policy, "Before enforcing").
16 October 2026: the SQLite driver counts its statements without SQL output
Fixed
- The SQLite driver counts every statement in the accumulator
sqlite3_query, as the MySQL and PostgreSQL drivers do, so the debug output and the debug bar's summary give the number of queries and their time also withsite.ini [DatabaseSettings] SQLOutput=disabled; they said "0 SQL / 0 ms" while the SQL query cache counted its misses. WithSQLOutputon, a statement faster thanSlowSQLTimeoutleft the accumulator running and was not counted either. Which statements are listed is unchanged. Test:eZSQLite3DBQueryCountTest.
16 October 2026: the debug bar's Velocity tab is called Server
Updated
- The Exp Debug bar's tab that says where the request entered and which process rendered the page is called
Server instead of Velocity: it shows the same for PHP-FPM, mod_php or the command line, where a tab named
Velocity read as if Velocity were running. Velocity, when it renders the page, is named in the tab itself. The tab's
ids (
data-tab="velocity",exp-debug-panel-velocity) are unchanged. German and American English. See the debug bar.
16 October 2026: panels of extensions in the debug bar
Added
- An extension can add a tab to the Exp Debug bar, after Memory, with a chip in the summary and data in the page's
JSON:
debugbar.ini [DebugBarSettings] Panels[]=<id>and[Panel_<id>] Class=<class>, the class implementing the newexpDebugBarPanelInterface(visible(),title(),chip(),html(),data()). The panel decides who may see it; an entry without a usable class and a panel that fails are left out with a warning.expDebugBarRegistry::panels(),expDebugBarReport::extensionPanels(). In the extension point catalogue (Setup > RAD). See Panels of extensions. Test:expDebugBarPanelsTest.
16 October 2026: OPcache, APCu and the versions in the debug bar's summary
Added
- The summary of the Exp Debug bar shows the memory of OPcache and APCu in use while they are on for the process
(
OPc 66 %,APCu 25 %, the details in the title), and after them the versions of PHP and Exponential (PHP 8.4.19 | Exp 6.0.15), all three at the right end; each opens the Server tab, which lists APCu below OPcache. Only for users withsetup/setuporsetup/managecache, since the bar also shows to anonymous visitors by IP. New thresholdsdebugbar.ini [DebugBarSettings] Thresholds[opcache_memory]=80;95andThresholds[apcu_memory]=75;90; the server summary (expDebugBarSummary) carriesopcacheandapcuwith their level. German and American English. See the debug bar. Test:expDebugBarReportRuntimeChipsTest.
16 October 2026: the debug bar names the PHP version only to administrators
Fixed
- The Exp Debug bar gave the version of PHP to everybody who sees it, which includes anonymous visitors when
site.ini [DebugSettings] DebugByIPmatches: in its server summary (expDebugBarSummary::collect(), the JSON of every page with the report) and in the PHP row of the Server tab. The summary has null now unless the user may usesetup/setup, the rule for its other lists; the Server tab shows the version to the users who see its privileged rows (setup/setuporsetup/managecache) and only the server API to the others. Tests:expDebugBarSummaryPhpVersionTest,expDebugBarReportEngineTest.
16 October 2026: the debug bar's Rendering tab
Added
- Rendering tab of the Exp Debug bar, for users with
setup/setup: the module views that ran with their view class and script, the siteaccess, the navigation part and the design order; what the content view cache did (read from the cache with the file's age and size, generated, generated without storing, or not used, each with its reason) and its key part by part, with the keys ofcontent/view/cachekeyslisteners marked; the template settings and the templates compiled in this request. Two chips, View: hit/new/off and Compiled N, open it. See the Exp Debug bar. eZNodeviewfunctions::generateViewCacheFile()also returnscache_hash,cache_keysandcache_keys_from_listeners; the three keys it returned before are unchanged.- Cache blocks in the Rendering tab: every
{cache-block}of the page with its template and line, hit or generated, the time to look it up and to generate it, its size, expiry and keys, and the chip CB hits/count. The compiled code of a cache-block passes its placement toeZTemplateCacheBlock::handle()and stores through the neweZTemplateCacheBlock::store(); clear the template cache once so compiled templates name their blocks. - Template tree in the Rendering tab: every template the page rendered, nested under the template that included
it, with the file the overrides chose and the time it took (at most 2000, every template counted).
debugbar.ini [DebugBarSettings] RenderTree=disabledleaves it out.
16 October 2026: admin4 review, admin4l
Fixed
- admin4l keys its cache blocks by the admin generation, so an edit of an admin layout or rule shows at once: the
page read the key with
.result.0, which the list the fetch operator returns does not have (a template error on every page, and a layout id without the generation); the key list is now imploded. - The GET parameter
SectionIDin admin4l's page cache keys is a number, so odd values no longer create cache files. - admin4l's tab menu block (
admin_tab_menu) draws only the list items, so the page has one menu panel instead of two nested ones.
Added
- Block templates
admin_toolbar_rightandadmin_toolbar_developer: the toolbarsadmin_rightandadmin_developeroftoolbar.ini, as admin4 draws them. Their block definitions belong toexplayouts.iniof the explayouts extension. Tests:Admin4lReviewTest.
16 October 2026: admin4 review, the design tests
Updated
- The design tests that run admin4 JavaScript in node look node up through
tests/tests/design/AdminNodeBinary.php: without node they are skipped on a developer machine and fail whereCIis set (GitHub Actions), so a runner without node no longer passes the suite with those checks gone. - The design tests that assert something is absent read their files through
tests/tests/design/AdminFiles.php: a missing file or an empty file list fails instead of passing without looking at anything. AdminMegaMenuTestruns the mega menu view as the anonymous user and checks that it is refused.
16 October 2026: the left column's tab stays on the window's edge on a phone
Fixed
- admin4 and admin4l: the open left column below 1024px (a drawer over the page) moved its tab to its far edge, by the column's width. With a width dragged or chosen on a wide window that put the tab and the width grip in the middle of a phone's screen, over the page, and right to left the tab left the screen. The tab now stays fixed on the window's edge, open or closed, in the 24 px the drawer's card leaves there, as the right column's tab does.
- On a phone (below 576px) the left column has no width grip, and below 1024px the drawer leaves 48 px of the page visible at any saved width (it could be wider than the screen).
- On a touch screen both sidebar tabs are 48 px high (40 px before; their width stays 24 px, the room the cards leave at the window's edge).
16 October 2026: the Tab order of admin4l's header is admin4's
Updated
- admin4l: the Tab key goes through the header as it does in admin4: logo, menu button, menu, search, site link,
light/dark switch, user menu, sidebar controls. The menu button and the sidebar controls were one block
(
admin_sidebar_toggles), drawn after the tools; they are two now,admin_menu_buttonandadmin_sidebar_controls, and the page draws the logo and the menu button before the menu panel and the rest of the header zone after it. A layout stored withadmin_sidebar_toggleskeeps working and keeps its old order (the block is made of the two new ones). To give such a layout admin4's order, replace that block in the layout editor withadmin_menu_buttonright after the logo andadmin_sidebar_controlsat the end of the header zone; nothing migrates stored layouts.
16 October 2026: the side columns are there on the first paint
Fixed
- No layout shift when an admin4 page loads: the side columns opened only once the document was parsed
(
ezadmin_menubar.jsadds.show), so a reload first drew the content across the whole window and then moved it aside for the columns; the dashboard at 1440 px with the left column open had a cumulative layout shift of 0.87 (0.73 with the default width and both columns). The page layouts of admin4 and admin4l now writehtml.a4-preload, and until the script has applied its stateadmin4.cssdraws the columns from the user's eZPreferences on<html>(admin_left_sidebar,admin_right_sidebar,admin_right_drawer) with the script's rules: open beside the content unless saved closed (left from 1024 px, right from 1280 px), a right drawer saved open is open on a tablet-sized window, and phones start closed. No transition runs before the script has run; it removes the class two frames after it has applied the state. Measured on the dashboard: 0.003 to 0.009 at 1440 px, light and dark, right-to-left, custom and default left width; no shift at 1180, 800 and 390 px. An anonymous page keeps its choice in the browser: it starts with both columns open, and a column it keeps closed closes when the script runs.
16 October 2026: the sub items say why they are missing without Exponential UI
Fixed
- The node view's sub-items table is drawn by Exponential UI (extension
expui). On a siteaccess where it is not active the table's place stayed empty below "Sub items (3)", without a word. It now says that the sub items cannot be listed because Exponential UI is not active or not loaded (admin4:a4-notice is-warn, admin:message-warning), as text and with role status. With Exponential UI nothing changes. German and American English. Test:AdminSubitemsWithoutExpUITest.
16 October 2026: explayouts 1.4.32 and 1.4.33: admin layouts with admin4's header and toolbars, the Exp Layouts debug bar panel
Updated
- Require explayouts 1.4.32. Its stock admin layouts (the admin layouts fragment of new installations) put the tab
menu in the
topmenuzone, order the header as admin4 does (logo, menu button, search, theme switch, sidebar controls: the new blocksadmin_menu_buttonandadmin_sidebar_controls), and draw the right column ofadmin_3colfrom the toolbarsadmin_rightandadmin_developer(the new blocksadmin_toolbar_rightandadmin_toolbar_developer; the interface choices come throughadmin_right). The single tool blocks, among themadmin_ui_switch, and the combinedadmin_sidebar_togglesstay available, so admin layouts already stored keep working unchanged.[AdminLayoutSettings] SiteAccessMatch[]also matches siteaccesses named<site>_admin. - The template editor of explayouts edits only the templates of the layout types and blocks:
explayouts.ini [TemplateEditorSettings] AllowedTemplateRoots[]ships the three folders belowextension/explayouts/design/standard/templates/explayouts/(it shippeddesignandextension, and an empty list fell back to them), and an empty list allows nothing. A site whose design overrides layout templates adds its own folder.AllowNewTemplates=disabled(new): a template that does not exist yet is created only withenabled. Symbolic links are never read or written, and the previous version of a saved template is kept in<var directory>/explayouts/template_backup/instead of a.tpl.bakbeside it. - The Layouts tab of the node view in admin4 has no inline script (the page to return to is in
data-explayouts-return-to). - Require explayouts 1.4.33, which adds the panel Exp Layouts to the Exp Debug bar through the kernel's panel API
(
debugbar.ini [DebugBarSettings] Panels[]=explayouts): the layout of the page, the rule or default layout that chose it, its zones and blocks with links to edit them, and the available blocks. It shows only on public siteaccesses and only to users withexplayouts_ui/edit, so visitors matched byDebugByIPdo not see layout internals.explayouts.ini [DebugBarSettings] AdminURLsets the address of an administration on its own host.
16 October 2026: the classic administration design on phones and narrow windows
Updated
- The right column of the classic administration design keeps its tools inside the column: they no longer ran about 5px past it and past the window's edge at 1440px and at 200% zoom. The interface choices (the design switch and the grouped language list) are one full-width button a row, a long name wrapping inside its button.
- Below 760px the classic design works on a phone (
design/admin/stylesheets/pagelayout.css): the page's content comes first, then the left menu, then the right column's tools; the header wraps (logo and logout, the search, the tabs); wide tables with links or fields scroll sideways inside their own box, tables of text wrap; fields and pictures keep to the width of the column; buttons, tabs and menu links are 44px high. Both page layouts carry the viewport meta, and the left menu's width control leaves the content's margin alone while the columns are stacked. Wider windows look as before, left to right and right to left. - Tests:
tests/tests/design/ClassicAdminNarrowWindowTest.php.
16 October 2026: admin4 review, the admin scripts
Fixed
- The admin frame saves a remembered choice only when it changes:
window.a4Pref.set()skips a value already on<html>, and a page view no longer posts one preference per left menu group that was stored closed. - Changing the color mode for a signed-in user also changes it in the other open admin tabs.
- A click on the left column's grip that does not move it saves nothing, and a width the server did not take is kept in the browser.
- The sign-in button shows its own label again after the browser's back button.
- admin4 always loads its rounded theme stylesheet, also when an old
admin_themepreference names another one. - A mega menu whose links do not load within 10 seconds says so and loads again on the next opening.
- A relation search result is added once and can be chosen with Tab, Enter and Space; a failed search shows "The search failed. Try again."
- Sub-items table options (presets, rows per page, columns) that could not be saved show "Not saved: ..." and are not shown as saved.
- The upload dialog shows an error for an empty server answer instead of stopping.
- The sub-items list without its scripts (Exponential UI) keeps the one note of the same day's fix ("The sub items cannot be listed ..."); the merge dropped a second note of its own.
- Context menus fill every placeholder, disable only what the opened menu asks for, and a click on a disabled item no longer jumps to the top of the page.
- In the content tree a node on the path to the current page is marked also when it shows no sub items.
- A broken tab cookie no longer breaks the node view's script.
Updated
- The content tree's open/close buttons have no inline handlers; the tree, tab and sub-items column cookies use
encodeURIComponentandSameSite=Lax(old values are still read). - The RSS export and RAD pages read
event.keyinstead ofkeyCode. Tests:AdminScriptsBehaviourTest(node harnesses intests/tests/design/js/),AdminScriptFailureTextsTest,AdminPreferencesTest,AdminMegaMenuTest,AdminKeyboardTest,AdminScriptsTest.
16 October 2026: admin4 templates for the copy step, the publishing queue and the locked class
Added
- admin4 has its own
content/copy.tplandcontent/copy_subtree.tpl(the choice of versions, creator and times after the target was chosen),content/queued.tpl(the page shown while asynchronous publishing works) andclass/edit_locked.tpl(a class whose changes wait for a cronjob), built from its components (.a4-radios,.a4-notice,.a4-facts,.a4-toolbar), so they follow the theme and dark mode instead of design/standard's unstyled pages. The form names, the variables and the ids the publishing scripts look for are unchanged; no inline code. - The image editor (ezie) in admin4: admin4's
content/datatype/edit/ezimage.tplhid the copy of the template that ezie ships in design/standard, so the editor's Edit button was missing. It now includescontent/datatype/edit/ezimage_editor.tplbeside a stored image, which draws the editor's scripts and its Edit button only whenezmodule( 'ezie/prepare' )allows it, and nothing otherwise.
16 October 2026: the content job page stops polling when it cannot go on; the relation search keeps its text
Fixed
- Content job progress (
content/job/<id>,page/content_job.js): the page stopped asking for the status only when the job finished. A job that was removed or that the user may not see (404, 403) and an ended session (the request redirected to the login page, or an HTML page in place of the JSON) made it ask every four seconds for as long as the tab stayed open. It now stops with a message (This job no longer exists or you may not see it ..., You are no longer signed in ...;data-text-gone,data-text-signed-outof the template), and so does a script error while it shows the status (data-text-error). After a network or server error the next request waits 4, 8, 16 ... up to 60 seconds and is back at two seconds after an answer, and a hidden tab makes no request until it is shown again. - "How to run it" on the confirmation pages of the content jobs (
page/content_job_mode_choice.js): the remembered choice was saved with a GET ofuser/preferences/set/..., which answered with a redirect that rendered a whole page, carried no form token and did not check the answer. It is saved throughwindow.a4Pref, as the other remembered choices of the frame: a POST touser/preferences/set_and_exitwith the form token. The fieldset'sdata-prefsattribute is gone. - Relation search of the edit form (object relation and object relations attributes,
ezajaxrelations_jquery.jsof admin4 and admin): the search text was part of the ezjscore call (ezjsc::search::<text>), wherestrip_tags()removed what looked like a tag and::cut it into arguments, soa<borc::dsearched for something else. It is sent as the POST parameterSearchStr, whichezjscServerFunctionsJs::search()reads.
Removed
data-a4-printanddata-a4-disable-on-submitina4_actions.js: no template used them, and disabling the buttons on a click would have left a form unusable after a cancelled submit or a page restored from the back/forward cache.
16 October 2026: content checks: the start location and the storage files
Page: The content checks, "Where a check starts", "Storage files" and "Quarantine".
Added
- The start location of a content check is any node: the presets of
contentcheck.ini [ScopeSettings] PresetList[](the whole tree, the content, Media, Media/Images, Users) or any node through the shared node picker (contentcheck/scope/<view>/<node id>,browse.ini [ContentCheckScope]). It is kept as the preferenceadmin_contentcheck_scopeof the signed-in user and in the address. Shared selectorcontentcheck/scope.tpl, classexpContentCheckScope; the views that take it are listed inViewList[]. - Storage files (
contentcheck/storage/<node id>, Setup menu "Storage files"): a batched scan ofvar/<site>/storage(images,images-versioned,original) with progress, kept as the last scan. Orphan files: noezimagefilerow, no image XML of the attribute their directory names, noezbinaryfileorezmediarow, and no path found by the reference index in fields, layouts, settings or templates; every version and object state counts; files younger thanMinAge(a day) never count. Size, age and totals. File problems of used files below the start location: another owner than the storage directory's, missing mode bits ofStorageFilePermissions, empty files, a name that differs only in case, an image url that is not its dirpath and filename, a file in another MIME type's directory. With a DFS or database cluster the files come from its table, report only. - Quarantine, never a delete in the storage: orphans are moved to
var/<site>/storage-quarantine/<date>/with their path, restored from there (never over an existing file), and deleted from there only after a confirmation with its own box. Fix permissions sets only the expected owner (as root) and mode. Every action is a dry run first, then a POST with the form tokens and a confirmation token bound to its files; each file is checked again when it is done. Policiescontentcheck/storage,contentcheck/quarantine,contentcheck/permissions. Audit eventscontent.file.orphan.quarantine,.restore,.delete,.refusedandcontent.file.permissions.fix. - On the command line:
./console exp:content-check --storage [--quarantine | --fix-permissions] [--dry-run] [--json]. - One layout for every check (unreferenced objects, broken relations, storage files): the start location, the
filters with Select all / Select none per group (buttons after Show, read on the server), the results header
("X to Y of N"), the table with a master box that ticks every row of the page (mixed state, "n of m selected"; without
JavaScript "Select all on this page" / "Select none"), the bulk actions with "all N results" (one batch, always the
plan first), and the pager. Shared parts
contentcheck/results_head.tpl,bulkbar.tpl,pager.tpl,filter_choice.tpl,select_all.tpl,select_count.tpl,scope.tplandcontentcheck_select.js. - Tests:
expContentCheckStorageTest(no database): the start location and the presets, the node picker view, paths, file problems, image XML and file paths, fixing owner and mode, the action view's decisions, restore and delete in a throwaway quarantine, the list parameters and the scan state.
Updated
- The filter form of Broken relations took its node with a redirect that the kernel answered with the same query again, endlessly: "Check" ended in "too many redirects". It reads the node of the form directly.
- The reference index (format 3) keeps every storage path it found, for the storage files.
Upgrade notes
- New settings with safe defaults:
contentcheck.ini [ScopeSettings],[StorageSettings],browse.ini [ContentCheckScope]. The new policies are held by Administrator only. Nothing is scanned or moved until someone asks for it. On hosts where two users write the storage (an application server as root, PHP-FPM as the site user), owner fixes need root: run the command line as root.
16 October 2026: admin4 review, values from the database and from packages are escaped
Fixed
- A package whose
package.xmlcarries markup or spaces in its version, release or type is no longer read (eZPackage::parseDOMTree()refuses it like a package without a name, with an eZDebug error), and the package pages of admin and admin4 wash every value taken from a package, including the error message of a JSON document. Before, a<number>with markup ran as script onpackage/view/filesand in the file browser ofpackage/view/full. - The admin and admin4 pages escape the hidden fields of the browse page, the template name of the template creator, workflow group names, product options of an order, wait-until class names, toolbar parameters, currencies and the preferred currency of the shop errors.
- A class name ending in a backslash no longer breaks the sub-items table's script.
- The content tree menu shows each user the classes they may create below the root node, not those of the user who drew it first: the root block's cache key carries the roles and limitations.
- The page cache of the administration no longer writes a new copy for every distinct
SectionIDin the address: the parameter enters the cache keys as a number. - White space around the version, release or type of a hand-written
package.xml(<number>\n 1.0\n</number>) is trimmed instead of getting the package refused by the new check. All 905 package definitions of the installed and downloadable packages (every content class, site and extension package) are still read. - design/standard, the fallback of every design without its own package pages, escapes the same package values, the action IDs of the install and uninstall errors and the package description, which it printed raw.
- The content tree menu keeps its data attributes (no inline script, Content Security Policy) with the roles and
limitations in its root block's key; the admin4 sub-items table keeps its JSON options, which escape the class
names for their context already.
Tests:
eZPackageDefinitionTest,AdminStoredValueEscapingTest.
16 October 2026: missing files and image aliases in the broken relations
Added
- Missing files, a target of Broken relations (
contentcheck/brokenrelations, chip Missing files in var/<site>/storage,Kind[]=missing_file): the file-backed fields of the content below the node whose file is not in the storage (through the cluster file handler, so a DFS counts), in the published version and in drafts, and the images whose aliases are missing, out of date (anotherimage.inikey, expired, or older than the original) or, when asked, never made. Each row: the object and its node, the field and language, the version, the expected path, the last modified time and the problem. Ticked alone, no relation is checked. With a total and pages ((file_offset), the findings keptListCacheTTLseconds). Datatypes throughcontentcheck.ini [MissingFileSettings] Handlers[](ezimage,ezbinaryfile,ezmedia; an extension adds its ownexpContentCheckFileHandler). Command line:./console exp:content-check --missing-files [--problems=...] [--versions=...]; opt-in count in the cronjob partcontentcheck(CronjobReport=enabled). Read only. Guide: Missing files and image aliases. Strings in eng-US and German; testsexpContentCheckFilesTest(no database). - Resolving missing files (
contentcheck/fixfiles, POST only, policycontentcheck/fixfileswithbrokenrelations, andcontent/editon each object): regenerate the aliases from the original, restore the file from another version that still has it, re-upload or replace it (same extension; an image must be an image), remove the dead reference (the database only), or mark for review; per row or for the ticked rows, with select all on the page (contentcheck_select.js, and links without JavaScript), select all and none in the filter groups, and a preview for all results. Always a preview first and a confirmation token bound to the rows shown; done in batches ofFixBatchSize(10) with progress; nothing is ever deleted or overwritten. Audited ascontent.file.alias.regenerate,content.file.restore,content.file.replace,content.file.reference.removeandcontent.file.review.mark. Command line:--fix=<action> [--items=...] [--replace-with=...] [--dry-run]. TestsexpContentCheckFileFixTest(no database).
17 October 2026: the log viewer
Page: The log viewer; guide: Finding an error by its reference.
Added
- Download all logs on Setup > Manage logs: one
.zip(or.tar.gz) of every log, its rotations optional, with aMANIFEST.txtof sizes, last writes and sha256; masked as the viewer shows them unless "Unredacted" is ticked (only withsetup/unmasklogs); POST with the form token, audited assystem.log.download(after.bundle), built as a stream undervar/tmp/logbundleand removed after it is sent; capped bylogviewer.ini [ActionSettings] MaxBundleBytes(default 256 MB). The row of buttons of the log viewer's pages has room above, below and between its lines. - Managing logs (Setup > Log files > Manage logs,
setup/logsmanage, policysetup/managelogs): every log with its size, estimated lines, last write, growth, archives and limits; rotate now, empty (archive first), compress the archives, remove archives older than N days, rotate all and prune all, each with a preview of every step before it is applied under a lock and audited. The limits come fromlogviewer.ini [RotationSettings]per log, elsesite.ini [FileSettings]; new switch[ActionSettings] Compress(defaultdisabled). The work is done by the log manager, or by a built-in fallback with the same API. - Log health (
setup/logshealth): disk use against[HealthSettings] DiskBudget(256 MB), the fastest growing logs, logs over their limit, files the web server cannot write (mixed ownership), the newest error per log linked to its line, and the rotation settings in effect with the file and section each comes from. - Error digest (
setup/logsdigest): the distinct errors and warnings of the last 24 hours per log, masked, normalized, grouped and counted, linked to their filtered view; "Mark as known" per user (preferenceadmin_logviewer_known). Settings[DigestSettings]. - Archives open read only under
/setup/logs/view/<key>/(archive)/N((rotation)/Nstill read). - Copy messages: copies the lines shown as plain text (following: every line kept; paused: the lines on screen), with a message saying how many.
- Tests without a database:
expLogArchiveViewTest,expLogHealthTest,expLogDigestTest,expLogManagerFallbackTest,LogViewerCopyMessagesTest. - Wrapping long lines is on by default and kept per viewer: the Wrap lines switch stores the choice as the user
preference
admin_logviewer_wrap, the same for every log, permalink and visit, drawn on first paint; without a choicelogviewer.ini [TailSettings] WrapLines(defaultenabled) decides. The Open and Copy permalink buttons are on a line of their own at the start, and Open, Copy permalink, Download and Rotate are primary buttons. - Setup > Log files (
setup/logs, Setup menu group System, admin4's mega menu): the log files of the installation (var/log, every site'svar/<site>/log, the logs of extensions) and their rotations.1...Nand.gz(read only), in a navigator grouped Kernel, Site, Extensions and Rotated archives with size, time and line rate. - Following a file live with short polls (
setup/logsfeed), never a held request: astat()first, only the bytes after the reader's offset, at mostMaxBytesPerPollper poll, whole lines only, a server-side cache of the last chunk per file, no file locks, an interval that doubles while nothing arrives; pause and resume; rotation and emptying detected by inode and size. - Search of a file, every kernel log or every file, with their rotations: literal or regular expression (with
pcre.backtrack_limitand a time limit), levels, time range (a binary search to its start), siteaccess, request, client address, error reference; bounded per request, continued with "More results". - Cross-references: the line format
[ time ] [client] [siteaccess] [request] messageread into entries; the lines of the same request across the kernel logs; the error reference of an error page (ERR-XXXXXXXXXX) found with its request; the audit console of the same minute; links to nodes, objects, users, classes, requests and templates. - Masking by default: e-mail addresses, IP addresses, passwords, tokens, keys, session ids and cookies; the values
of secret settings always. The per-viewer switch "Show unmasked" for
setup/unmasklogs. - Actions, POST with the form token and audited: download a range (masked, gzip; on), rotate now, empty a file, remove an archived rotation (each off until switched on, with a confirmation page).
- Policies
setup/logs,setup/managelogs,setup/unmasklogs(Administrator only in a new installation). Audit eventssystem.log.read,system.log.download,system.log.rotate,system.log.truncate,system.log.delete(channel system, on). - A rate limit per session for the feed (
[RateLimitSettings], 429 with Retry-After), kept outside the session. - On the command line:
./console exp:logs(bin/php/explogs.php):--list,--tail [--follow],--searchwith the same filters,--reference,--related,--json,--unmask, level colors. - Settings
settings/logviewer.ini:[LogViewerSettings],[TailSettings],[SearchSettings],[RedactionSettings],[ActionSettings],[RateLimitSettings],[ViewerAuditSettings], every one documented with its default on the page. Classes inkernel/classes/logviewer/; template fetchfetch( 'setup', 'log_files' ). - Tests (no database) in
tests/tests/kernel/logviewer/: the parser, offset tailing and the chunk cache, masking, path safety, the regular expression limit, the search, the rate limit and the actions. - Permalinks without a query string, by the log's key:
/setup/logs/view/error,/setup/logs/view/debug,/setup/logs/view/site-storage,/setup/logs/view/webserver-access, with the whole view as view parameters ((rotation),(level),(search),(regex),(case),(from),(to),(siteaccess),(uri),(ip),(reference),(scope),(rotations),(cursor),(offset),(follow),(wrap)), e.g./setup/logs/view/error/(level)/error/(search)/timeout/(follow)/0. A value is written as is when it is letters, digits and_ . , -, any other byte as~XX. The page keeps its address in step with the view, and Copy permalink copies it (never with the unmasked switch). The navigator, the menus and the actions link to permalinks. A key names only a listed log, never a path; an unknown key answers 404.expLogPermalinkwrites and reads them. - Old links
/setup/logs?file=var/log/error.log(with the search fields of that form) answer301with the permalink of the same file and search; the search form without JavaScript is answered303with its permalink. - The web server's logs, configured by key:
logviewer.ini [LogViewerSettings] LogList[],LogPath[<key>],LogLabel[<key>],LogGroup[<key>](webserver-accessandwebserver-errorare listed with no path, so they are off until a path is set: never guessed). Shown in the navigator group Web server with their rotations (<name>.N,<name>.N.gz), read and downloaded only: rotate, empty and remove are refused for a configured log. The parser reads access logs (common and combined; the status gives the level) and Apache 2.4 error logs. [LogViewerSettings] DefaultKey(error): the log/setup/logsopens.- Tests:
expLogPermalinkTest(encoding, view parameters, round trip),expLogKeyTest(keys, configured logs, the old links),expLogWebServerFormatsTest(no database).
Updated
- The Setup overview (
setup/menu) also opens withsetup/logsalone. - The audit catalogue has 167 names (five
system.log.*added).
Fixed
- A search finds only what the viewer is shown. The request and client address filters compared the values as
written while the results showed them masked, so a viewer without
setup/unmasklogscould find out a masked address one search at a time (203.0.1finds it,203.0.2does not, ...) and likewise a token or e-mail address in a request. With masking on, both filters now also compare the masked value (203.0.still finds203.0.113.7, shown as203.0.x.x;203.0.113finds nothing); the text, the request and the client address are checked against the redactor for every viewer, so the values of secret settings and the secrets in an address, hidden even with Show unmasked, are not found either. Unmasked, the address and the request are compared as written, as before.expLogSearch::run()takes the viewer'sexpLogRedactor(a callable still works, for the text only);expLogSearch::entryMatches()takes it as an optional third argument. Tests:expLogSearchTest(LV-48),expLogViewerTest(LV-65). - The filters compare only what is shown, so a value copied from the page finds its entries. With a viewer's
redactor the request and client address filters are compared with the shown value alone (no longer also with the
written one), and the client address filter accepts an
x:203.0.x.x,2001:db8:x:x,2001::x,::x,token=***. Before, the masked addresses of404.logandsecurity.log(written203.0.x.x) could not be filtered beyond their first two parts, and a masked value copied from the page was refused or found nothing. A filter with a mark of the masks also finds, for an unmasked viewer, what a masked viewer would find. The same holds for the archives, the page's script, the command line and the error digest. New:expLogSearch::shownContains(),shownStartsWith(),expLogRedactor::masked(),masksIps(),ipFilter(),ipFilterOf(). - A shared permalink never carries an unmasked address. Copy permalink gives the link with the request and
client address filters as the masks of the settings show them (
(ip)/203.0.113.7becomes(ip)/203.0.x.x, an e-mail addressj***@***.com), newexpLogPermalink::shareable()and the template variable$log_share_permalink(both designs; an override without it falls back to$log_permalink). The address bar keeps the filters as typed. WithMaskIPs=disabledthe address stays. - A compressed IPv6 address with one group before the
::is masked.2001::7334andfe80::1were shown in full by the log viewer; they are shown as2001::xandfe80::x, and404.logandsecurity.logwrite that form instead ofx. "Class::method" in a message is left alone (only the address field is affected). - The error digest links a log grouped by
uriwith the request filter (GET /blog/#links with(uri)/~2Fblog~2F), masked as the group is;expLogDigest::link()takes the grouping as an optional third argument,expLogDigest::uriFilterOf()is new. Tests:expLogMaskedFilterTest(LV-220 to LV-227: IPv4, IPv6 and its compressed forms, the request logs, the archives, the digest, the permalinks, without a redactor).
Upgrade notes
- Nothing changes for a site until someone opens the page: no new table, no cronjob. The new policies are held by
Administrator only; rotate, empty and remove are switched off (
[ActionSettings]). A site that keeps logs in another directory adds it to[LogViewerSettings] Directories[](it must be inside the installation root). - To show the web server's logs, set their paths, e.g. on Plesk
LogPath[webserver-access]=/var/www/vhosts/<domain>/logs/access_ssl_logandLogPath[webserver-error]=/var/www/vhosts/<domain>/logs/error_log; the user that runs PHP must be able to read them. - Bookmarks of the first form (
?file=) keep working: they are redirected to the permalink.
17 October 2026: content checks: quick scope buttons and a row of sites and sections
- Every content check (unreferenced objects, broken relations with the missing files, storage files) starts with the
quick scope buttons Whole tree (node 1), Websites (the content root,
content.ini [NodeSettings] RootNode) and Media (MediaRootNode), then Choose with the node picker for any other node, Users among them. - Beneath them a second row lists the sites and sections under the content root, read live from the content tree:
the visible children you may read, in the content root's sort order, names escaped. At most 12; with more,
More… opens the node picker at that level (
contentcheck/scope/<view>/<node id>/<start node id>; a cancel goes back to the check where it was). The site the start location lies in is outlined. - All of them are plain links to
contentcheck/<view>/<node id>(no JavaScript needed), wrap on phones and mark the current one witharia-current. The storage files keep the list shown (?Show=) when the scope changes. - One shared part,
design:contentcheck/scope.tpl(admin and admin4), fromexpContentCheckScope::forTemplate(). - Settings (
contentcheck.ini [ScopeSettings]):PresetList[]now defaults totree,content,media(was alsomedia/imagesandusers; add them back there if wanted); newSiteRow=enabled,SiteRowParent=content(a preset key, node id or URL alias path) andSiteRowLimit=12(1 to 50).SiteRow=disabledhides the second row. - Tests:
expContentCheckStorageTestCS-15 (no database). Translations: eng-US, ger-DE. - Page: The content checks.
17 October 2026: the extensions' left menus end with admin4's width switch
Updated
- The left menus of bccie, eztags, syndication and cjw_newsletter draw admin4's own width switch in admin4 and admin4l
(
parts/left_width_control.tpl: three icon buttons in a POST form, the current width pressed, stored per user), the same markup, script and behavior as every kernel module. bccie, eztags and syndication carried a copy of the classic Small, Medium and Large links; the newsletter pages had no switch in admin4. Each extension ships adesign/admin4/templates/parts/<name>/menu.tplthat includes the kernel's part; the classic admin designs keep their links. Required versions: bccie ~1.1.14, eztags ~2.4.15, syndication ~1.3.7, cjw_newsletter ~4.2.9. - eztags 2.4.15 names the fold links of its tag tree ("Fold/Unfold") for screen readers.
- The fold icons the classic tree menu script adds (
ezjslib_createHTMLChildImageNode()) carry an empty alt text: they are decorative, the fold link carries the name. - Tests:
AdminLegacyControlsTest::testExtensionLeftMenusUseTheKernelWidthSwitchInAdmin4(no database): an extension whose classic menu copies the width links ships an admin4 menu, and every extension's admin4 menu includes the kernel's switch instead of writing its own.
16 October 2026: admin4 review, templates
Fixed
- The admin4 sign-in page offers "Forgot your password?" only to a visitor who may open
user/forgotpassword(fetch( 'user', 'can_open' ), as the register link); on an administration where anonymous has nouser/passwordpolicy the link led to the access denied page. content/upload.tpl(admin4, admin, standard) prints the reasons an upload failed as text (|wash); they carry the file's name.- A page of the admin4 search past the last one says that it has no matches and links to the first page of the same search, instead of the number of matches above nothing.
- The Setup tab (
menu.ini [Topmenu_setup]) has noPolicyList[]=setup/managecacheany more: it is shown to every user who may open the Setup overview (setup/menu, any one of its six functions, checked byCheckViewAccess). A user withsetup/system_infoonly lost the tabs of[TopAdminMenu] SetupTabs[]and had no Setup tab to find them in. An installation with[MenuAccessSettings] CheckViewAccess=disabledthat wants the old rule sets the policy again in an overridemenu.ini.append.php. - "Active", "Activate", "Apply changes" (setup/extensions), "Sessions" (setup/session), "Repository" (package/list) and "Rejected" (content/versions) are current messages of the German catalogue, no longer marked obsolete.
Removed
setup/datatype_{basic,describe,code}.tplandsetup/templateoperator_{basic,describe,code}.tplof admin4 and the classic admin design: the step templates of the old wizards, fetched by nothing.
Tests: eZTopMenuOperatorSetupTabAccessTest, eZLoginTemplatesRegisterLinkTest (RL-05), AdminTreeMenuEscapingTest,
AdminSearchTest (AS-13), AdminSetupWizardTemplatesTest, AdminUsedTranslationsTest.
17 October 2026: the siteaccesses on Setup > System information
Added
- Setup > System information shows the siteaccesses (#552). The Exponential card has a short line, "Siteaccesses:
17 (default: site; match: uri, host)", and a folded part Siteaccesses lists every siteaccess of
AvailableSiteAccessListandRelatedSiteAccessListwith its kind, designs, locale, languages, access extensions, hidden nodes and login, the entries that reach it, an Open link (the code of "Open the site", so host-matched siteaccesses get their host and URI-matched ones their prefix, on the port of the page) and a link to itssite.inisettings. A resolution map shows the rules inMatchOrderorder, the default siteaccess and the rule that chose this request. Read from the settings without changing the current siteaccess; nothing secret is shown. ClassexpSiteAccessOverview, testsexpSiteAccessOverviewTest(no database). Guide:doc/eng-us/guides/system-information.md, section 5a.
Updated
- Fixed: the "Open the site" link of a URI-matched siteaccess (
ezpSiteAccessURL::root()) went to the host that the host map gives it whenever this page's host was mapped to another siteaccess, even withMatchOrder=uri;host, where the URI wins on any host (the mapped host may not even resolve). It keeps this host unless the host rule comes before the URI.
Upgrade notes
- New optional setting
site.ini [SiteAccessSettings] SiteAccessTypenames the kind of a siteaccess on that page; it is not set by default, and the kind is then worked out from the designs andRequireUserLogin.
17 October 2026: clean logs and one log rotation
A crawl of every site siteaccess, the administration and the default cronjobs now leaves nothing in error.log,
warning.log, deprecated.log and strict.log but the requests for pages that do not exist (#555).
Updated
- One log rotation for the kernel (
expLogRotator,lib/ezfile/classes/explogrotator.php), used by eZDebug and eZLog: an exclusive lock on the log while it is rotated, the size checked again under the lock (two requests that both found a log too big rotated it twice, and the archives raced: "rename(storage.log.N): No such file"), and the new empty log created at once with the owner, group and mode of the old one, so a log written by Velocity (root) and PHP-FPM (the site's user) stays writable for both. - The last timing point is written before the first message of a request into
debug.logonly; it was an entry of its own ahead of every first error, warning and notice. - The error page's log line names the error and its HTTP status ("(kernel error 2, HTTP 404)"). A login request (401), a refused request (403) and a moved page (301) are notices, not errors; the policies a refused request did not match are debug output.
- On an eng-US siteaccess a text the eng-US translation does not reword is shown as written and not logged as a missing translation (one debug line per text per page before).
- Routine traces are debug output: the online editor's fallback editor, the layout resolver's cache hits and matched
rules, a version without a name in a language, the upload location match, a skipped preference write, the
two-factor sign-in copy of
auth.log. runcronjobs.php -qno longer prints "No scripts found for execution" for a part without scripts.- A translation file that does not validate raises one warning naming the file and its first errors (one PHP warning per schema complaint before).
Fixed
- PHP 8.5 deprecations: null passed to
trim(),rawurldecode(),explode(),strlen()andcreateTextNode(), and null as an array offset, in content views, the data map, the module dispatch, advanced search, keywords, the class package export and the string template operators (which now take null as an empty string). - A missing file row of the content checks redefined
cc_action_names(a warning per row); the classic administration's left menu readLinkNames[]of sections that have none; a refused form raised anhttp_response_code()warning;content/translationslogged its optional fields as missing; the parent of a draft's virtual node logged an empty fetch; a cache callback that returns nothing was reported as an error. - The translations carry the upgrade check's texts on inactive extensions, the image editor's button, the sub-items
table's Editorial preset, and six administration texts the untranslated catalogue lacked. A new test
(
TranslationFilesValidTest) validates the three kernel translation files against their schema and checks every template text is in them. - Extensions released with their log fixes: exp_oe_tiptap 0.1.9, expai 0.0.1.7, explayouts 1.4.35, eztags 2.4.16,
git_manager 2.0.19, sevenx_authentication_2fa 1.0.7, ezwebin 6.0.21, exp_adminui 1.0.0.37 (
composer.jsonrequires them).
Upgrade notes
- New settings with safe defaults in
site.ini [FileSettings]:LogFileCompress=disabled(compress the archives from.2on) andLogFileMaxAge=0(the age in days after which a prune removes an archive; 0: none). eZDebug::rotateLog()andeZLog::rotateLog()take an optional second parameter, the size a log must still exceed under the lock; without it they rotate as before.- A log line of the error page ends with "(<type> error <number>, HTTP <status>)"; a filter that matched the whole line "Error ocurred using URI: <uri>" must allow the suffix.
17 October 2026: rich text keeps characters above U+FFFF, TeX comments and line breaks in custom attributes
Fixed
eZXMLInputHandlerandeZXMLInputParser::process()allow the characters XML 1.0 allows above U+FFFF (#x10000-#x10FFFF). Before, opening a stored text in an editor removed each of them without a replacement (<mi>𝑥</mi>in a MathML attribute became<mi></mi>,𝔸 = \mathbb{A}in a formula= \mathbb{A}), and saving made each of them a space (a 😀 bbecamea b). Control characters, U+FFFE and U+FFFF are still removed.- Online Editor (
eZOEXMLInput::inputXML()): a line break in a custom attribute value becomes a space instead of vanishing; multi-line MathML in an attribute (</mi> <mo>) lost the break and glued the tags together.
Added
content.ini [CustomTagSettings] TeXSource[<tag>]=truenames custom tags whose text is TeX source. When the Online Editor opens such a text it reads it as TeX does: an unescaped%comment is dropped with the rest of its line, the line break and the indentation of the next line (a run of comment lines counts as one); after a control word a space stays (\alpha%⏎betagives\alpha beta). Before, the line break became a space anda % comment⏎+ bturned intoa % comment + b, with the rest of the formula commented out. Leading white space of such a tag is dropped instead of being stored as a no-break space, andAllowMultipleSpacesadds no there. None is set by default; other custom tags keep the line-break-to-space rule.ezxml.ini [InputSettings] SupplementaryCharacters=auto(enabled,disabled) decides where those characters are kept.autokeeps them unless the database is MySQL or MariaDB on a connection charset other thanutf8mb4:Charset=utf-8connects asutf8, three bytes at most, where they would be stored as????or fail the save in strict SQL mode. There they are replaced as before.Rich text that is not valid UTF-8 no longer empties the whole text in the input handler and parser (
preg_replace()returned null); each invalid byte sequence becomes U+FFFD.eZOEXMLInput::texWithoutLineComments()finds a control word before a comment by walking back over the letters, not with a regular expression over the whole text before it for every comment, and takes ASCII letters only.
Tests: eZXMLInputSupplementaryCharactersTest (kernel-datatypes), expOEFormulaTextTest (ezoe), both without a
database. Documentation: Rich text: characters above U+FFFF and TeX source.
17 October 2026: the side columns stay where they are while an admin4 page loads
Fixed
- The content column no longer narrows after the first paint: the first paint's rules for the side columns
(
html.a4-preload) found the columns withbody:has(.sidebar.right), and the right column stands after the content in the source, so a paint of the half-parsed page drew the content 232 px wider and then wrapped it (node views: 0.12 to 0.14 of layout shift at 1440 px). The page layouts of admin4 and admin4l now name the page's columns on<body data-a4-columns="left right">(left: the frame's column or the one an edit-type page draws itself);:has()stays for a page layout without the attribute. - The right column's tools no longer move up: the Interface tool's design list was drawn open and closed by
admin4.jsonce the document was parsed, and every tool below it moved up 96 px.a4_head.jssetshtml.a4-jsbefore the first paint, andadmin4.cssdraws the disclosure button and the closed list from the start. - Trash, the content finder and the width buttons no longer move down while the content tree is built: the left
card with the content tree fills the column, the tree scrolls in it (at least 8 rem high), and the parts after it
stand at the card's foot (they moved 20 to 600 px on every page with the tree). Test:
AdminFirstPaintColumnsTest(FP-05 to FP-07). - The newsletter menu's tree no longer covers its Administer and Settings lists (
newsletter/*, admin4 and admin4l): the rules for the tree card above also reached a tree inside a box of its own (#newsletter-menu > #content-tree), where the inner boxes withflex: 1 1 0had no height, the card ended under its heading and the tree was drawn over the lists after it. The rules now start at the column's own card (#leftmenu-design > #content-tree), so a wrapped tree keeps the ordinary flow. Test:AdminFirstPaintColumnsTest(FP-08).
17 October 2026: the admin4 header's menu row stays in place while the page loads
Fixed
- The main menu row no longer moves down 4 px from 576 to 1279 px: the header's tools (search, site, light/dark,
user menu) stand after the main menu in the markup, so a paint of the half-parsed header drew the first row with
the logo alone, 32 px high, and the menu row moved down when the tools arrived. The logo's box is now as high as the
tools (36 px). Applies to admin4 and admin4l, which share the header. Test:
AdminHeaderTest.
17 October 2026: the version preview and the edit form stay where they are while they load
Fixed
- The version preview no longer jumps when its device switch appears (
content/versionview, admin4 and admin4l): the bar with the devices was shown and the frame drawn at the device's size byadmin4.jsonce the page was parsed, so the preview moved down 44 px and shrank (0.07 of layout shift at 1440 px). With the frame's scripts (html.a4-js)modules/content.cssnow draws the bar and the frame from the first paint, in the saved device's size and proportions (the preferencesadmin_preview_deviceandadmin_preview_landscapeon<html>), narrowed to the column as the script draws it. Without JavaScript nothing changes. Test:AdminPreviewDevicesTest. - The fields of the edit form no longer move down when an extension adds its tools to them: expai drew its bar of
field actions into every field's block with JavaScript after the page was shown, and every field below moved down
26 px per bar (0.047 of layout shift at 1440 px). New setting
content.ini [EditSettings] AttributeTemplates[](empty by default):content/edit_attribute.tplof the admin designs includes these templates in every field, after its label (first in the block for a field with its own fieldset or being translated), so an extension draws its tools with the page; expai uses it for its bar from its next release. Test:AdminEditAttributeTemplatesTest.
Upgrade notes
- New optional setting
content.ini [EditSettings] AttributeTemplates[], empty by default; nothing changes until an extension adds a template to it. A design with its own copy ofcontent/edit_attribute.tplincludes the list the same way to get the tools drawn with the page (otherwise the extension's script adds them as before).
17 October 2026: the admin4 content edit form, striped role lists, Bootstrap Icons 1.13.2
Added
- admin4
content/edit(also while translating), multi-edit anduser/registerdraw each attribute as a card with an edge, and its corner names the kind of input with an icon and a word (.a4-content-attr-kind, hidden from screen readers, which hear the control's own kind; on a phone above the name). Two short inputs next to each other share a row from 64em on (not while translating). From six attributes on a row of links above the form names every field with its kind's icon, the required mark and an error state; a link to a field in a collapsed attribute group opens the group (content_edit_attribute.js, also for the error summary). An attribute group is a heading line over its cards. The mapping isadmininterface.ini [ContentEditAttributeSettings]:DatatypeKind[],KindName[],KindIcon[],DefaultKind,CompactDatatypes[],FieldNavigationFrom; the kernel's datatypes and those of the extensionscomposer.jsonrequires are mapped, an extension adds its own inadmininterface.ini.append.php(The content edit form). - Bootstrap Icons
123,input-cursor-text,tag,tags,text-paragraph,ui-radios(1.13.2, unchanged);.a4-bi-*for them and forfile-earmark,film,geo-alt,starin both copies ofcomponents.css, all ten indesign.ini IconList[].
Fixed
- Every second policy of a role (
role/view,role/edit) and every second role card ofrole/listis striped (--a4-zebra), counting visible entries only; the tints of a moved or selected policy still win. - A one-line select without a class in an
.a4-fieldleaves room at its end for the chevron admin4 draws there ("Subtree" beside "Assign" inrole/viewran under it). - A field of the admin4 edit form that its datatype template draws without a name of its own (the name and email of an
author, the selects of an enhanced selection) is named by its attribute's name and its column header
(
content_edit_attribute.js). - The kinds of input and the field list of the edit form are translated in the completed catalogues (Spanish of Spain and Mexico, Hebrew, Latin, Klingon).
Updated
- admin4 names Bootstrap Icons 1.13.2; every shipped SVG is byte-identical to the one in the 1.13.2 package.
Tests: AdminContentEditAttributeKindsTest, AdminRoleListZebraTest (no database).
17 October 2026: map addresses escaped for HTML before JavaScript
Updated
- A map location's address is stored as typed and escaped where it is printed, in the same order everywhere:
|washin HTML,|wash|wash( 'javascript' )where a map script hands it tosetContent(), which reads HTML. Markup in an address (or in the title of cjw_themes_jumper's popup) ran in the map popup before. Required versions: ezgmaplocation ~6.0.9, ezflow ~6.1.6, cjw_themes_jumper ~0.1.4. - ezgmaplocation 6.0.8 stores the address without HTML entities. Upgrade step, once per database, before editors
work with it:
php extension/ezgmaplocation/bin/php/decodeaddressentities.php -s <siteaccess>(dry run), then again with--apply; rows saved by 6.0.7 or earlier would show their entities otherwise. ItsfromString()keeps an address with|#whole and stores only numeric coordinates, and its settings file no longer prints itself. - ezgmaplocation 6.0.9: the edit form opens on a database that returns the coordinates as numbers (SQLite, PDO), and its address, latitude and longitude fields have accessible names.
- ezflow 6.1.6: the map block leaves out a location without a map value instead of stopping the whole map.
- Tests:
expMapAddressEscapingTest(suitemaps, no database): the escaping order in every map template, the rendered pipeline, the coordinates made strings beforeexplode(), andfromString().
17 October 2026: job queue, autosave and star rating without inline script or GET writes
Updated
- expqueue 0.1.3: a job queue control that asks before it is sent declares its question in
data-a4-confirminstead of an inlineonsubmithandler. admin4 and admin4l ask it witha4_actions.js, the other administration designs with the extension'sexpqueue_confirm.js(design.ini [JavaScriptSettings] BackendJavaScriptList); it is asked once in every design. - ezautosave 6.0.10: the admin edit form reads the autosave settings from a JSON block (
<and>escaped) and starts the autosave from the deferredjavascript/ezautosave/autosave_admin.js, so the edit page carries no inline script.
Fixed
- ezstarrating 6.0.11:
ezstarrating::rateanswers POST requests only, so an image or link to its GET address on another page can no longer rate in the visitor's name. The shipped script already posts with the form token. A theme of its own that still calls the GET address can be allowed with the newsite.ini [eZStarRating] AllowGetRating(disabledby default), at the cost of that protection. - Required versions: expqueue ~0.1.3, ezautosave ~6.0.10, ezstarrating ~6.0.11. Tests: each extension's DB-less
tests/unit(runphpunitin the extension).
17 October 2026: the icon theme follows the design
Updated
- The class, file type, class group and other icons of admin4 and admin4l (
class_icon,mimetype_icon,classgroup_icon,icon, the content tree'sicon_info) are Bootstrap Icons without a setting of the siteaccess. Newicon.ini [IconSettings] DesignTheme[<design>], set tobootstrap-iconsforadmin4andadmin4l: while that design is theSiteDesign, its theme replacesTheme, andThemeis searched right after it. Before, the theme was opt-in per admin siteaccess, and an admin siteaccess whoseicon.ini.append.phpsaysTheme=crystal-admin(as the setup writes it) drew the crystal PNGs in the content tree, the node header and the sub items. A user who switches to admin3 gets the siteaccess'sThemeagain; a type's ownThemestill wins. - The Settings extension wizard and the RAD health check know the themes of
DesignTheme[]as well.
Fixed
class_iconand the other icon operators no longer pass null tostrlen()wheneZSys::wwwDir()is not set (command line).icon_infokeeps its answer per type and theme list instead of per type only, so a persistent worker or a switch of the design in the same session never hands one design the icons of another.icon_infoleaves out aClassMap[](or other map) entry whose file the theme does not have: an extension that maps its classes to crystal paths (cjw_newsletter) no longer gives the admin4 content tree addresses underbootstrap-icons/svg/that answer 404; the theme's own entry or itsDefaultis used.- The two tests of
eZSiteAccess::getIni()andeZSiteAccess::findPathToSiteAccess()live once, ineZSiteAccessGetIniAndFindPathTest(kernel-classes, no database), instead of twice under file names that differ only in case.
Upgrade notes
- Nothing to do. To keep the crystal PNGs in admin4, set
DesignTheme[admin4]=(empty) in the admin siteaccess'sicon.ini.append.php. A design of your own built on admin4 needs its ownDesignTheme[]entry. The opt-in setting (Theme=bootstrap-icons) still works and can stay.
Tests: eZWordToImageOperatorDesignThemeTest (kernel-classes), without a database.
17 October 2026: safer defaults in owsimpleoperator, no inline tracker in bcwebsitestatistics, full sitemap URLs in bcgooglesitemaps
Updated
- owsimpleoperator 1.2.7 no longer permits
file_get_contentsandereg_replaceas template operators by default: the first let every template read any file the web server can read, the second no longer exists since PHP 7. ItsDebugOutputis disabled by default. A site whose templates use one of them lists it again in its ownowsimpleoperator.ini.append.php([PHPFunctions] PermittedFunctionList[]); no template shipped with or installed on this installation uses either. - bcwebsitestatistics 1.0.11 drops the inline
urchinTracker()onClick handler from the download links of its binary file view: the retired Urchin tracker no longer exists, and the handler was blocked under a Content Security Policy without 'unsafe-inline'. - bcgooglesitemaps 1.1.6.5 builds each
<loc>of the dynamic sitemap withezurl( 'no', 'full' ): the addresses carry the scheme (https too), host, port and siteaccess path the site is served on, also on servers that do not passHTTP_HOSTin the environment (they werehttp:///there). - Required versions: owsimpleoperator ~1.2.7, bcgooglesitemaps ~1.1.6.5, bcwebsitestatistics ~1.0.11.
17 October 2026: formulas, tables and embeds survive the assistant and the Tiptap editor
Fixed
- expai 0.0.1.8: formulas, tables and embeds are no longer lost when Vector or a tool writes a rich text field. What
the model cannot express in an ezxmltext field is sent as a placeholder (
⟦C1⟧and the like) and restored from the stored value; a write that drops, repeats or invents one is refused, or staged for review in a Vector thread. Newexpai.ini [RichTextSettings](Placeholders=enabled). The bars of field actions are drawn with the edit form, so its fields no longer move down; above a rich text field whose editor has an assistant of its own (Tiptap with its AI on) the field actions are hidden ([EditViewSettings] HideActionsWithEditorAI=falseshows them), and the length of a field the model reads is[EditViewSettings] MaxFieldLength(default 20000). - exp_oe_tiptap 0.1.10 (not a requirement of this repository; sites that install it get it through their own
constraint): formulas (
exp_oe_tiptap.ini [EditorSettings] AtomicInlineCustomTags[], defaultmath_latexandmath_mathml) are inline atoms, so typing, deleting or formatting around a formula no longer changes it; their text is edited in the custom tag dialog's new field Content. Unedited content is stored byte for byte as before.
17 October 2026: configuration managed by the deployment
Added
site.ini [ConfigurationSettings] ManagedByDeployment(defaultdisabled). Enabled for a siteaccess, the admin interface does not change its configuration on the server: the views that write settings, templates, generated code or packages (settings/edit, the toolbar and menu editors, the template editor, the setup wizard, extension activation,setup/radand the code generators,package/upload|install|uninstall|create|export) answer like a disabled view and are hidden from the menus, and neithereZINI::save()norexpIniEditor::save()writes a settings file in a web request, under Velocity too. Read-only views and command line scripts are not affected; generated audit and mail preference keys are still written. Seedoc/eng-us/features/6.0/managed-by-deployment.md.
17 October 2026: log management on the command line and as a cronjob part
Page: Log rotation and pruning.
Added
expLogManager(kernel/classes/logviewer/), the kernel's implementation ofexpLogManagerInterface, which the log viewer's management pages take throughexpLogManagerFactory:- every log with its archives, sizes, growth, limits (with where they come from) and owner;
- plans (dry runs) for rotate, prune, compress and empty, carried out step by step, each step resolved through the allow-list again under the rotation lock.
- On the command line:
./console exp:logs --archives,--rotate=<keys>,--rotate-all [--force],--prune[=<keys>] [--days=N],--compress[=<keys>],--truncate=<keys> [--in-place], each with--dry-runand--json; changes are audited assystem.log.rotate,system.log.deleteandsystem.log.truncate. - The cronjob part
logs(cronjobs/explogrotate.php,[CronjobPart-logs], in no default part): rotates the logs over theirMaxSize, prunes and compresses. logviewer.ini [RotationSettings]:MaxSize[],Keep[],Compress[]andMaxAgeDays[]per log key (empty by default; a log without an entry takessite.ini [FileSettings]).- Tests (no database):
expLogManagerTest.
17 October 2026: the expai test suite runs on any site
Updated
- expai 0.0.1.9 (tests only, no change to what the extension does): its unit suite no longer depends on the language,
region rule and operating system of the site it runs on. A German siteaccess with
[DataPolicy] DefaultRegions[]=eu;localgives the same result as an English one, and a run without a database skips the tests that need it instead of ending in errors.
17 October 2026: authorizing an OAuth client needs the policy oauth/authorize
Page: Upgrade note: authorizing an OAuth client needs the policy oauth/authorize.
Fixed
oauth/authorizenever denied a signed in user: its check!$access['accessWord'] == 'yes'reads as(!$access['accessWord']) == 'yes', which is always false, and theoauthmodule had no policy function to grant. The check is nowexpOAuthAuthorizePolicy::userMayAuthorize(); a user without the policy is sent to the client's end point witherror=access_denied(RFC 6749, section 4.1.2.1). A search of the kernel and every extension for the same!$x == ...pattern found no other access check with it.
Added
- The policy function oauth/authorize, with an optional SiteAccess limitation.
rest.ini [OAuthSettings] AuthorizeRequiresPolicy(defaultenabled;disabledgives back the old behavior and is meant only for the time of an upgrade).bin/php/addoauthauthorizepolicy.php(--dry-run,--role=<id or name>): grantsoauth/authorizeto every role withuser/login, for the same siteaccesses; roles that have it already and roles of only the anonymous user are skipped.- New installations give the Editor role
oauth/authorize. - Tests (no database):
expOAuthAuthorizePolicyTest,expOAuthAuthorizePolicyGrantTest, and the view'saccess_deniedredirect inezpOauthAuthorizeRedirectTest.
17 October 2026: a toolbar theme for the debug bar
Added
debugbar.ini [DebugBarSettings] Theme=toolbardraws the pinned bar of the Exp Debug bar as a dark, compact toolbar in the manner of the Symfony web debug toolbar: an icon in each block, the request's HTTP status and module/view in the first block, amber and red blocks filled with their colour, and the details of a block (page and query time, queries, memory and its limit, the message counts, the request) in a popup above it on hover or focus. The panel follows the Symfony profiler: the tabs a dark menu down the left side with icons, a large title, and metrics at the head of Messages, Timing, SQL, Templates, Memory and Server. New stylesheetdesign/standard/stylesheets/expdebugbar-toolbar.css, linked only in this theme. The setting is also in the bar's Settings tab ("Bar theme").
Upgrade notes
- Nothing to do:
Theme=defaultkeeps the bar as it was. Guide:doc/eng-us/bc/6.0/debug-bar.md, "Themes".
Tests: expDebugBarReportToolbarThemeTest (lib), without a database.
Fixed
- The debug bar no longer grows or moves after the page has loaded (#571): the minimise symbol, "Keep open on reload"
and the minimised tab are in the report's markup instead of being added by its scripts, the bar keeps at least the
height of one line, and its sides are logical, so its link stays at the end of the line in a right-to-left
interface. Both themes. The report has no inline script any more: what must run before the first paint is
design/standard/javascript/expdebugbar-head.js, linked withoutdefer, so a Content-Security-Policy withscript-src 'self'holds.
17 October 2026: Tiptap in the layout editor, and Tiptap's admin4 look without a setting
Updated
- explayouts-ui-api ~1.3.19: the layout editor's rich text and code editors are Tiptap instead of CKEditor 4 and Ace.
CKEditor 4 is end of life without free security updates; the two editors were 23 MB of
design/standard/vendor/, which is removed. The editor bundle is unchanged: an adapter provides the calls it makes. Rich text keeps CKEditor's toolbar and writes its HTML as CKEditor did, and unchanged content comes back byte for byte. HTML snippet and Markdown blocks keep their text byte for byte. No longer available: anchors, bidi buttons and paste-from-Word cleanup; syntax checking, themes, search and code folding. Nothing has to change in an installation. - exp_oe_tiptap 0.1.11 (not required by this package):
exp_oe_tiptap.ini [EditorSettings] DesignSkin[]names the Tiptap skin per admin design, used whenSkinis not set. admin4 and admin4l get admin4's look (a4) without a setting of their own;Skin=o2k7keeps ezoe's look. The edit template no longer writes "Operator 'has_attribute' is not registered" to warning.log.
17 October 2026: the admin4 mega menu loads reliably
Page: admin4 design, "Mega menu".
Fixed
- The mega menu of a top tab no longer says "The <tab> menu could not be loaded." when nothing failed: a load the browser stops because another page is being opened (Firefox reports it as a network error) shows no error, and the next opening loads again.
- A passing failure (no connection, 5xx, 408, 429, no answer after 10 s, an answer without the menu) is tried once more after 600 ms. A failure for good offers "Try again" and keeps the link to the tab's page; a signed-out user is offered a link to sign in instead of the error.
- One request per tab and page, shared by every opening while it runs; the script binds each tab once.
user/megamenuanswers its script with a bare status and anX-Exp-Megaheader (ok,unknown,signed-out,denied), never the sign-in page or an error page in the page layout; it releases the session as soon as the user is known, so the page's other requests do not wait for it, and sends the fragment without the templates' indentation (Setup about 5 KB instead of 75 KB).
Updated
site.ini [SiteAccessSettings] AnonymousAccessList[]and[RoleSettings] PolicyOmitList[]nameuser/megamenu: the view checks the signed-in user,user/loginfor the siteaccess and the tab itself. A site that removes either entry gets the sign-in or error page back, which the script also handles.- The fragment's root carries
data-a4-mega-fragment="<tab>"; the script takes the element with that marker (or a design's owndata-a4-mega-groups). - New strings (eng-US, ger-DE): "Try again", "You are signed out. Sign in again to see the %tab menu.", "Sign in".
- Tests (no database):
AdminMegaMenuTestMM-09, the load scenarios run in node againstmini_dom.js.
17 October 2026: git_manager, ezupdate and the database editor follow the admin design
Updated
- The pages of git_manager (dashboard, commit details, Backup), ezupdate (
update/dashboardand the header, footer, status and method parts every update view shares) and sevenx_dse (dse/dashboard, its gate page and the embedded database editor) are drawn with the shared admin components (.a4-card,.a4-table,.a4-btn,.a4-toolbar,.a4-tag/.a4-badge, the notices and empty states) and the--a4-*tokens, so they follow admin4's light and dark modes and admin4l, and the classic admin gets the shared classes it has. No template carries a<style>block, astyle=""attribute or an inline script any more: each extension's own rules are in a stylesheet loaded through itsdesign.ini[StylesheetSettings](bundled by the packer), and the scripts are files. Every button, form, confirmation and status poll works as before. Required versions: git_manager ~2.0.20, ezupdate ~1.1.20, sevenx_dse ~1.1.10. - git_manager's dashboard and commit details get their own left menu (Git Manager, Backup) that ends with admin4's
width switch; the Backup page and the update and database editor pages keep the Setup menu, which has it already.
Sites that override git_manager's templates with the old
.gm-*classes lose that styling. - The database editor's stylesheets are scoped to the editor, so they no longer restyle the admin frame and its left column, and its login fields, logo, menu button and language select carry accessible names.
- New DB-less tests under
tests/tests/extension/{git_manager,ezupdate,sevenx_dse}check that these extensions' admin templates carry no inline CSS or scripts, that their stylesheets use only the tokens, and that their admin4 menus include the width switch. Each skips when its extension is not installed.
17 October 2026: copying a layout makes each zone once
Fixed
- explayouts-core ~1.0.8: copying a layout gave every zone of the copy two rows, the layout type's and the source's.
The layout editor added new blocks to one row and showed the other, so a block added to a copy disappeared from the
editor and the page. A copy now has each zone once, with the source zone's position, its link to a shared layout
zone and its blocks, collections and parameters. Draft and publish are unchanged. Layouts copied before can carry
duplicate zone rows; an installation that copied layouts can list them by grouping
explayouts_zoneonlayout_id, status, identifier.
17 October 2026: 404.log and security.log, error.log keeps the real faults
Page: Request logs: 404.log and security.log.
Added
var/log/404.log(expRequestLog,kernel/classes/requestlog/): one line per 404 or 410 answer with its reason (module:,view:,disabled:,node:,object:,rule:,file:,kernel:), the siteaccess, the node when known, the referrer and a short user agent; client addresses masked, every query value***.site.ini [RequestLogSettings]:NotFoundLog=enabled,NotFoundLogFile=404.log,MaskIP=enabled,MaxURILength=300.- The request classifier and
var/log/security.log(expRequestClassifier,kernel/classes/requestclassifier/,settings/requestclassifier.ini): a request that is a probe, a path traversal, an injection, comes from a known attack tool or is one of too many not-found answers from one client is written as one line<category>/<rule> confidence=... <status> ..., whatever its status, instead of its 404, notice or error line. Classification only, nothing is blocked. Rule sets by INI (RuleSets[],[RuleSet-<id>]withPatterns[],Disabled[],Excludes[]), extensions add their own (expRequestRuleSetInterface,expRequestPatternRuleSet), andListeners[](expRequestVerdictListenerInterface) is the hook for a later ban list or rate limit. - The log viewer:
404.logandsecurity.logare kernel logs (/setup/logs/view/404,/setup/logs/view/security) with the labels "Not found requests (404)" and "Security (request classifier)", the levels notice and warning, and rotation defaults ([RotationSettings]: 10 MB, 5 archives, compressed, pruned after 30 and 90 days). - The error digest groups per log (
logviewer.ini [DigestSettings] GroupBy[<key>]:message,category-rule,reason,reason-kind,uri) and lists other levels per log (LogLevels[<key>]): security.log by<category>/<rule>, 404.log by the reason of the answer, with its notices. LogLabel[<key>]names any log of the log directories by its key, translated in the contextkernel/logviewer.- Tests (no database):
expRequestLogTest, the classifier's tests intests/tests/kernel/requestclassifier/andexpLogRequestLogsTest.
Updated
error.logkeeps the real faults: "Undefined module", "Module/View ... is disabled", "Undefined view", "Error ocurred using URI ... HTTP 404" and node not found go to404.log(or tosecurity.logfor a classified probe) whileNotFoundLog=enabled. WithNotFoundLog=disabledthey are written toerror.logas before. 401 and 403 stay notices innotice.log.- The log parser reads the lines of both logs: the request, the siteaccess and the status become fields, the source
is the status or
<category>/<rule>. logviewer.ini [DigestSettings]and[HealthSettings]are read from the settings: before, the defaults were used whatever the file said.
Upgrade notes
- Nothing to do. To keep the old behavior, set
site.ini [RequestLogSettings] NotFoundLog=disabledandrequestclassifier.ini [ClassifierSettings] Classification=disabled.
17 October 2026: the file manifest check is a warning on pull requests
Every Quality failure of a pull request since 15 October stopped at "File manifest (share/filelist.md5)". The check
runs on the merge of the branch into the current main, so when main changed the same files a contributor could
not produce the merged checksums, and the person merging refreshes the manifest anyway.
- On a
pull_requestevent a mismatch is a::warning::annotation plus the diff and a note in the job summary; the job does not fail. On a push tomainthe check stays blocking, exactly as before. - Test:
CiWorkflowPullRequestTest::testManifestWarnsOnPullRequestsAndBlocksOnPush(suitequality). - Guide: Pull requests, CI.
17 October 2026: a merged contributor commit no longer fails the commit message check
Every pull request is merged and its commits are kept as written, so a contributor's subject such as Changed: ...
reached main and the Quality step "Commit messages" failed the push (and the pull request) for a commit nobody
could reword any more.
- The check is
.githooks/check-commit-range <base> <head>, used by CI and by the local gates alike. A commit by a project author, an address listed in.githooks/commit-msg-authors, must still start withAdded:,Updated:,Fixed:,Removed:orRenamed:and fails otherwise. A commit by anyone else gets a warning annotation and never fails. Merge commits are not checked.EXP_COMMIT_MSG_STRICT_ALL=1makes every author strict. - The
commit-msghook on a developer machine is unchanged. - Tests:
CheckCommitRangeTest(suitequality) builds throwaway repositories and covers project and contributor commits, a merged contributor branch, the strict switch and the annotations. - Guide: The commit messages.
17 October 2026: publishing under Velocity after a registration
Fixed
- content/edit and user/register each give the edit form an action check of their own name
(
expContentEditCheckContentActions,expUserRegisterCheckContentActions) instead of sharingcheckContentActions. Velocity's persistent workers keep every function a worker defined, so after one user/register in a worker, Publish in content/edit there ran the registration operation on the edited object: the publish answered 500 and the draft stayed unpublished. Apache and PHP-FPM were not affected. A custom content/edit or user/register view that hookscheckContentActionskeeps working, but under Velocity it should give its function a name of its own too. - The text part of the mail layout (
design:mail/layout_text.tpl) writes a row value that is not a string (a number, or nothing) as text instead of stopping the mail with an error.
17 October 2026: module views define global functions of their own name
Fixed
- Under Velocity's persistent workers a worker keeps every global function it ever defined, so two views that
defined the same name behind
function_exists()served each other's function. shop/productcategories and shop/vattype both definedapplyChanges(with different bodies: saving the categories of one view could run the VAT type code of the other), content/copy and content/copysubtree both definedbrowse, content/search and content/advancedsearch both definedpageLimit. Apache and PHP-FPM were not affected. - Every global function of a kernel module view now has a name of its own,
expModuleView<Module><View><Name>(for exampleexpModuleViewShopVatTypeApplyChanges,expModuleViewContentCopyBrowse). oauth/authorize no longer defines the genericerror(),response()andgetHTTPVariable()(nowexpModuleViewOauthAuthorizeError,expModuleViewOauthAuthorizeResponse,expModuleViewOauthAuthorizeGetHTTPVariable), which an extension defining the same name would have replaced.kernel/settings/validation.phpdefinesexpSettingsValidate()instead ofvalidate(), and the unusedtestPHPIni()of the setup tests isexpSetupTestPHPIni(). - The bundled ezjscore (1.8.3) and ezoe views do the same:
expModuleViewEzjscoreRunExitWithInternalError,expModuleViewEzjscoreRunHasAccessToBySetting,expModuleViewEzjscoreCallMultipleServerCalls,expModuleViewEzoeSpellcheckRpcFormatParam,expModuleViewEzoeSpellcheckRpcGetRequestParam. - The helpers of
kernel/content/node_edit.php,relation_edit.php,section_edit.phpandstate_edit.php(initializeNodeEdit()and the rest) keep their names: custom content/edit views call them.
Tests
expGlobalFunctionCollisionTest(no database) scans kernel/, lib/, design/ and the bundled extensions by token and fails when two request-time files define the same global function, or when a module view defines a generic or unprefixed one.
Upgrade notes
- Code that called one of the old names of a view's function (only possible after that view had run in the same request) calls the new name. A custom view should give its own global functions a prefixed name too.
17 October 2026: extensions without inline scripts in their templates
Updated
- The installed extensions move their inline scripts, event handler attributes and
javascript:links into script files withdata-*attributes, so the administration's Content Security Policy (script-src 'self') no longer reports them, and they keep working once it is enforced. composer.json requires these releases: cjw_newsletter 4.2.12, eztags 2.4.17, xrowextract 2.5.17, ezownerchange 1.4.2, ezgmaplocation 6.0.10, ezwt 6.0.11, ezie 6.0.13, recaptcha 1.4.8, hcaptcha 1.4, syndication 1.3.8, bccie 1.1.16, ezautosave 6.0.12, ezmbpaex 6.0.7, explayouts 1.4.37, explayouts-ui 1.3.20, explayouts-ui-api 1.3.24 (the layout editor), explayouts-content-browser-ui 1.0.8, xrowmetadata 1.4.13, ezstarrating 6.0.12, bcwebsitestatistics 1.0.12, enhancedezbinaryfile 4.4.8, ezmultiupload 6.1.7, expui 1.0.0.10, expai 0.0.1.12, expchangeclass 1.0.9 and sevenx_authentication_2fa 1.0.9 (and AdminAid 1.1.4, which is installed from its repository). - Their context menu entries are added to
menuArrayby the extension's script from data attributes, and the entries call the sameezpopmenu_*functions as before, so they work with this release'sdata-menu-*menus and with the menus of earlier releases. Every administration page had 9 policy reports from the menu entries of eztags, xrowextract and ezownerchange; it has none now. - Exponential UI (exp_adminui 1.0.0.40) runs its own administration design without inline scripts, except the TinyMCE 3 start script it shares with ezoe; the site designs follow: ezwebin 6.0.22 (except the footer script an editor enters as content), ezdemo 6.0.12, ezflow 6.1.9, cjw_themes_jumper 0.1.6, sevenx_themes_simple 1.0.24 and sevenx-themes-media 1.5.70 (its service worker registration included), as do ezfind 3.0.6, ezpm 0.10.1 and ezauthorize 1.4.3 where they are installed.
- The expservices portal designs (expportal_jquery, expportal_reactive) set their saved color mode from
javascript/portal/theme_head.jsinstead of an inline script.
Tests
ExtensionInlineScriptsTest(no database) fails when a template of one of these extensions, at its listed version or later, carries an inline script, an event handler attribute or ajavascript:link. What an extension still has is listed per file with the reason; the list only shrinks.
17 October 2026: the design switcher marks the older administration designs
Changed
- The administration design switch (
uiswitch.ini [UISwitch_design]) lists the current designs first (Admin 4 (layouts), Admin 4, Admin 3) and the older ones after them, labelled as such: "AdminUI (older)", "Classic (older)" and "Classic, server-rendered (older)". The labels are translated in eng-US, ger-DE, esl-ES, esl-MX, heb-IL, lat-VA and tlh-QO (contextkernel/uiswitch); the former labels stay in the catalogues for an installation that sets them in its own settings.
Tests
expUISwitchDesignProviderTest::testShippedOrderPutsTheCurrentDesignsFirstAndMarksTheOlderOnes(no database) checks the shipped order, the labels and their eng-US and ger-DE translations.
17 October 2026: the update Overview no longer starts Composer on every visit
Fixed
- ezupdate's Overview (
update/dashboard) rancomposer --versioneach time it was opened, and each run wrote a notice tovar/log/notice.log. ezupdate 1.1.21 keeps the answer in<cache dir>/ezupdate/composer-version.jsonforezupdate.ini [ComposerSettings] VersionCacheTimeseconds (default86400,0asks every time), keyed on the binary's path, size and time, so a newcomposer.pharis asked again at once; a failure is never kept. The cache is a cache item of its own (ezupdate, "Update extension cache"), cleared by "Clear all" andbin/php/ezcache.php --clear-id=ezupdate. A Composer run that succeeds is logged at debug level, one that fails still writes a notice. Required version: ezupdate ~1.1.21. Tests:ezUpdateComposerVersionCacheTest(DB-less, skips when ezupdate 1.1.21 is not installed).
17 October 2026: sitemap XML, inline CSS and accessibility follow-ups in five extensions
Updated
- bcgooglesitemaps 1.1.6.6: the dynamic sitemap (
googlesitemapdynamic/sitemap) is the whole response, sent asapplication/xml; charset=utf-8and ended witheZExecution::cleanExit()outside any try/catch. WithDebugOutput=enabledthe debug output no longer follows</urlset>, so the XML stays valid, under PHP-FPM and the Velocity engine. - explayouts 1.4.36: the admin templates carry no inline CSS. The Authors tab's dates and names use the shared
a4-nowrapclass instead ofstyle="white-space: nowrap", which a Content-Security-Policy without'unsafe-inline'forstyle-src-attrreports and drops; the Layouts tab and the legacy layout and rule lists take their styles fromexplayouts_admin.css(design.iniBackendCSSFileList), on the admin's colour tokens. - cjw_newsletter 4.2.11: the check-all boxes of the blacklist and the editorial article pool have an aria-label,
the edition filter's status icons an alt, the pager's current page 5.2:1 contrast, and the admin2 templates no
style=""attributes. - bccie 1.1.15: the overview's check-all box has an aria-label.
- cjw_themes_jumper 0.1.5: the starter design passes the contrast, target size and link name checks: the logo link
has a name, the v2 colours below 4.5:1 are darkened in the same hue in
starter.css, the footer menu links are 24px high, and linked images fall back to the object's name for their alt text. - Required versions: bcgooglesitemaps ~1.1.6.6, explayouts ~1.4.36, cjw_newsletter ~4.2.11, bccie ~1.1.15, cjw_themes_jumper ~0.1.5. Each extension carries a test without a database for its change.
17 October 2026: the layout editor page meets WCAG 2 AA
Fixed
- explayouts-ui-api ~1.3.22: the layout editor page (
explayouts_ui_api/app) had serious accessibility issues outside the editors, older than Tiptap. The sidebar's Content and Design tabs are WAI-ARIA tabs (a named tablist,aria-selected, a rovingtabindex, tab panels labelled by their tab) and move with the arrow keys, Home and End; only one tab shows as selected. The block palette's button and the left toolbar's icon buttons have accessible names, the palette's button says whether the palette is open, and its list can be reached by keyboard. Text colors reach at least 4.5:1 through the editor's own tokens (--exp-le-accent,--exp-le-muted,--exp-le-on-dark-muted). The editor's CSS moved from an inline style block inapp.tplintostylesheets/explayouts-editor.css, and the app and mobile scripts are linked without a version query. The new texts are translated in every language of the extension. Nothing has to change in an installation. Tests:EditorAccessibilityTest(no database).
17 October 2026: every request is classified once, also one that ends through a clean exit
Page: Request logs: 404.log and security.log.
Fixed
- A request that ended through
eZExecution::cleanExit()(a file download, an ezjscore call, a redirect, a view that sends its own output) was not classified, so a probe such as/content/download/../../etc/passwdor an ezjscore call with an injection in its query had no line insecurity.logunless it failed through the error module.ezpKernelWeb::run()now starts the request log withexpRequestLog::begin(), which registersexpRequestLog::finish()as a clean-up handler ofeZExecution; the clean-up runs on a clean exit, at the normal end and after a fatal error.finish()runs once per request, however many of these reach it, so a request has at most one line, insecurity.logor404.log. - Under Velocity's persistent workers the handler belongs to its request (
eZExecutionkeys its handlers onREQUEST_TIME_FLOAT) and is added once: nothing is carried into the next request and nothing accumulates. expRequestClassifier::setInstance(): a test hook for the classifierinstance()returns.
Tests
expRequestLogRequestEndTest(no database): a download probe and an ezjscore injection that end through the clean-up have one line each insecurity.log, a normal download and ezjscore call none; the normal end, the error module and a clean exit together still write one line; a realcleanExit()in a process of its own; eight requests in a row in one process as a persistent worker serves them, each probe once, one handler per request.phpunit.xmlhas the testsuitekernel-requestlog(tests/tests/kernel/requestlog,tests/tests/kernel/requestclassifier): the request log and classifier tests were in no suite, so CI did not run them.
17 October 2026: the layout editor page can be zoomed
Fixed
- explayouts-ui-api ~1.3.23: the layout editor page's meta viewport carried
user-scalable=no, which stopped phones from zooming the editor (WCAG 1.4.4, axemeta-viewport). It now readswidth=device-width, initial-scale=1, like the other editor pages. The "No content" placeholder of an empty block takes its color from the editor's muted token (--exp-le-muted) instead of an inline#888, which was 3.3:1 on a selected block. Nothing has to change in an installation. Tests:EditorAccessibilityTest(no database).
17 October 2026: the relation list's rows are labelled in the classic design
Fixed
- The classic design's relation list edit template (
design/standard,ezobjectrelationlist.tpl) gave the controls of a relation row no accessible name: axe counted two seriouslabelfindings per relation list on the edit form, from the empty row the browse script copies when a relation is added. As admin4 already did, the remove checkbox now carries the related object's name (else "Remove selected") as itsaria-label, and the order field and the translation base select are labelled by their column heading (aria-labelledby). The strings were already translated in every catalogue. Nothing has to change in an installation. Tests:AdminRelationListLabelsTest(no database).
17 October 2026: a repeated probe reaches PHP under Velocity too
Page: Request logs: 404.log and security.log.
Fixed
- Under Velocity the response cache answered a repeated identical anonymous request itself (
X-Cache: HIT), also when the request sentCache-Control: no-cache, so a probe sent five times reached the request classifier once andsecurity.loghad one line where Apache writes five. A request the classifier flags is now answeredCache-Control: private, no-store(expRequestLog), whatever its status, so no shared cache keeps it. [CacheSettings] NotFoundSecondsships as0: every 404 reaches PHP, as under Apache, so404.logandsecurity.logsee each one. A 404 withoutCache-Control(a REST answer) was answered from the cache for a minute.[CacheSettings] HonorRequestNoCache=enabled: a request withCache-Control: no-cache(orPragma: no-cache) reaches PHP and its answer is stored again; a browser's ordinary reload (max-age=0) is still answered from the cache. Needs exponential-velocity withQ.web.cache.honorRequestNoCache(main 8a1321e); older versions ignore it.- Normal anonymous pages are cached as before: measured on the :8080, 12 of 15 requests for three cacheable
pages answered from the cache before and after. A probe sent five times now writes five lines in
security.log(one before, four answered from the cache).
Tests
expRequestLogTest::testVerdictKeepsTheAnswerOutOfSharedCaches(no database).
17 October 2026: no inline CSS in the extensions' admin designs
Updated
- Eleven extensions' admin templates carry no
<style>block and nostyle=""attribute any more, which a Content-Security-Policy without'unsafe-inline'forstyle-srcreports and drops. Their styles are in each extension's admin stylesheet, loaded through design.ini[StylesheetSettings]and packed by ezjscore, on the shared components (a4-flag,a4-badge,a4-pct-*,a4-sr-only...) and the admin's--a4-*tokens, so they follow the dark mode. Values that are data (bar widths, tag cloud sizes, the details panel width) became classes in steps ordata-*attributes. Each extension carries a test without a database that fails on inline CSS in its admin templates.- exp_adminui 1.0.0.39: 120 uses in design/adminui, now in
adminui-pages.css(design.iniAdminUICSSFileList, loaded afterstyle.css); the workflow process list takes the Admin UI accent. - explayouts_ui 1.3.19 (39, design/admin Layouts pages,
explayouts_ui_admin.css), eztags 2.4.18 (10 in admin2 and 3 in the standard design; the tag cloud's sizes are classes), ezflow 6.1.8 (5), sevenx_authentication_2fa 1.0.8 (5), expai 0.0.1.11 (3), expchangeclass 1.0.8 (3; the result page also escapes the parameter file it prints), ezautosave 6.0.11 (2; the preview panel follows the dark mode), AdminAid 1.1.3 (2), ezauthorize 1.4.2 (1), ezmbpaex 6.0.6 (1).
- exp_adminui 1.0.0.39: 120 uses in design/adminui, now in
- Required versions: exp_adminui ~1.0.0.39, eztags ~2.4.18, ezflow ~6.1.8, sevenx_authentication_2fa ~1.0.8, expai ~0.0.1.11, expchangeclass ~1.0.8, ezautosave ~6.0.11, ezmbpaex ~6.0.6. Nothing has to change in an installation.
17 October 2026: a restart or deploy that is cut off no longer leaves Velocity down
Fixed
exp:velocity restart, and the restart step ofexp:velocity deploy, run the stop and the start as one detached unit (own session, and a transient systemd scopevc-restart-<date>-<token>as root). On 17 October a deploy's caller was killed between the stop and the start and :8080 refused connections for 28 minutes; now the restart finishes on its own. The caller still waits and prints the outcome with the same exit code.[ControlSettings] DetachedRestart=disabledrestores the old behavior.- The server runs at
[ServerSettings] Nice(default 5) whatever the caller's priority; a deploy run undernice -n 19left every worker at 19.exp:velocity statusreports a parent at another value.Nice=inheritkeeps the caller's.
Added
restart-in-progress.jsonandstopped.jsonnext to the pid file (expVelocityMarkers): an explicit stop or kill marks the server as stopped on purpose, start, restart and graceful lift it.deploystarts a server that is down without that mark and leaves one that was stopped on purpose alone.exp:velocity watchdog install|status|run|remove: an optional systemd timer (vc-watchdog, every 45 s) that starts the default engine when no process of it runs, unlessstopped.jsonsays it was stopped on purpose or a restart is in progress (restart-in-progress.jsonyounger than[WatchdogSettings] RestartTimeoutwith its process alive). It never touches a running server, backs off after three failed starts, logs tovar/log/velocity-watchdog.logand is paused byexp:velocity conf disable watchdog. Off by default.
Tests
expVelocityMarkersTest,expVelocityNiceTestandexpVelocityWatchdogTest(no database): markers, the start decision, the detached command, the priority prefix and its status check, the hand-over of the result.
17 October 2026: git_manager's commit log has pages and filters
Added
- The commit log of
git_manager/dashboardbrowses the whole history one page at a time: first, previous, the shared admin pager, next and last, "Showing x–y of z commits", and 10, 25, 50 or 100 commits per page (kept as the user preferenceadmin_git_manager_log_limit, 50 until one is chosen). A page isgit log --skip --max-countand the totalgit rev-list --count, so a long history is never read as a whole. - The log filters by branch or tag, author and message text (fixed text, any case) besides the dates. The page, its
size and the filters are view parameters (
/(offset)/50/(limit)/25/(ref)/main/(author)/jane/(grep)/fix), so a link reproduces the view. Every value reaches git as an argument of its own, without a shell, and a branch or tag must be one of the repository's refs. The filter no longer lives in the session. Required version: git_manager ~2.0.21. - Tests (no database):
tests/tests/extension/git_manager/GitManagerLogPagingTest.phpin theadmin-extensionssuite (the paging maths, the view parameters and the git arguments, hostile values included).
17 October 2026: the remaining serious accessibility findings of the admin pages
Updated
- The classic design's Layouts pages: the primary buttons and the table headings have white text (4.8:1 and 5.1:1 instead of 4.3:1 and 4.4:1), the links of the Layouts menu are at least 24px high, and the dates, counts, row links and the import hint that the bundle drew in light grey or its own blue take the admin tokens (4.5:1 or more). The mapping list's export checkboxes and the layout list's sort direction have labels. In admin4 a disabled mapping is set apart by its dashed border on the soft surface instead of being dimmed, which took its priority badge to 2.5:1 (explayouts-ui 1.3.21).
- The fold links of the tags tree are 24 x 24px targets in every admin design and are named with their tag ("Fold/Unfold Sports"); the tree's lines are at least 24px high (eztags 2.4.19).
- The Admin UI's package list and cache page label every field; the sign-in footer is readable on its dark frame (exp_adminui 1.0.0.41).
- The two-factor pages (second step at sign-in, setup, social login answers, the 2FA field of a user object) have
no
<style>block any more: their rules arestylesheets/sevenx_authentication_2fa.css, loaded through design.ini[StylesheetSettings]for the public and the administration designs. In a dark admin4 their card, fields and status colors follow the tokens, so the setup page keeps its text readable (sevenx_authentication_2fa 1.0.10). - Required versions: explayouts-ui ~1.3.21, eztags ~2.4.19, exp_adminui ~1.0.0.41, sevenx_authentication_2fa ~1.0.10. Nothing has to change in an installation.
17 October 2026: the last inline styles of the edit forms and the Admin UI's sign-in button
Updated
- The classic design's relation list edit template (
design/standard,ezobjectrelationlist.tpl) hides its "Create new object" field with thehiddenattribute instead ofstyle="display:none", and labels the name field. No datatype edit template of the standard design carries a style attribute any more (StandardDatatypeEditNoStyleAttributeTest). - The profile's link to the two-step sign-in page and the 2FA field of the public edit form take their layout from the extension's stylesheet instead of style attributes (sevenx_authentication_2fa 1.0.11).
- The Admin UI's sign-in button has its filled accent with white text above 5.5:1 instead of 2.6:1 (exp_adminui 1.0.0.42).
- Required versions: exp_adminui ~1.0.0.42, sevenx_authentication_2fa ~1.0.11. Nothing has to change in an installation.
17 October 2026: every Updates and packages view follows the admin design
Updated
- ezupdate's other views (Installed packages, Find packages, a package's page, Package servers, a server's packages,
Funding and a run's page) are drawn with the shared admin components and tokens, like the Overview since
ezupdate 1.1.20, so they follow admin4's light and dark modes and admin4l (no serious accessibility findings, no
light cards on the dark page). Each page draws its title, intro and actions in the shared page header. The older
stylesheet
ezupdate.cssis removed; the module's own rules are.a4-update-*in itsmodules/update.css. Composer's coloured output uses classes instead ofstyle=""attributes and the script sets no inline styles, so the pages work understyle-src 'self'. Every form field and button name is unchanged; sites that override an ezupdate template with the old.ezx-*or.ezupdate-*classes lose that look. Required version: ezupdate ~1.1.23. - Tests (no database):
tests/tests/extension/ezupdate/ezUpdateAdminStylingTest.phpnow checks every view and part: no inline CSS or script, no stylesheet of a template's own, the title through the shared header, the form names, ANSI colours as classes, and every string translated in the seven completed catalogues. It skips when ezupdate is not installed.
17 October 2026: eZ Find's search suggestions without YUI
Fixed
- eZ Find's search suggestions work again (ezfind 3.0.7). They needed YUI 3, which ezjscore no longer ships, so no
search field showed any. The suggestions are now plain JavaScript against the same server function
(
ezjscore/call/ezfind::autocomplete), as an accessible combobox: a named listbox of options under the field, arrow keys, Enter and Escape, a click, and a status region that announces the number of suggestions. Lookups wait for a pause in typing, and only the answer to the latest one is shown. The ezflow search block's AJAX search runs without YUI as well and escapes the values the server returns. - The list takes the administration tokens (with their dark values) in the admin designs and the site design's look
on the site. A site that styled the old
yui3-aclist-*classes needs the newezfind-ac-*names. - The composer.json suggestion names ezfind ~3.0.7. Nothing has to change in an installation without eZ Find.
17 October 2026: the public edit form names every control, and the last style attributes of the extensions
Updated
- The standard design's content edit form (the public edit form of starter and every design without its own) names
every control: the sort field, the sort order and main location radios, the translation radios, the related
object checkboxes and the class list have an aria-label, the move, remove and find image buttons an alt text, all
translated in the
design/standard/content/editcontext, and an attribute's label points to its field. axe found 13 serious findings (missing labels and alt text) on that form and finds none now (StandardContentEditAccessibleNamesTest). - The media design's two-factor pages take their rules from
sevenx_authentication_2fa_media.cssinstead of a<style>block and style attributes (sevenx_authentication_2fa 1.0.12). The metadata edit forms and the license picker take theirs fromxrowmetadata.css(xrowmetadata 1.4.14), and the location map its size fromezgmaplocation.css(ezgmaplocation 6.0.11), both loaded through design.ini[StylesheetSettings]. The language flags of the tags field have an empty alt text (eztags 2.4.20). - Required versions: sevenx_authentication_2fa ~1.0.12, xrowmetadata ~1.4.14, ezgmaplocation ~6.0.11, eztags ~2.4.20. Nothing has to change in an installation.
17 October 2026: the REST administration in light and dark
Updated
- The REST administration (Setup > oAuth admin: the application list, one application, its edit form, the removal
confirmation, the API keys and the revocation confirmation) is drawn with the shared admin components
(
.a4-card,.a4-figures,.a4-tabs,.a4-table,.a4-filterbar,.a4-toolbar,.a4-notice) and the--a4-*tokens, indesign/adminanddesign/admin4, instead of its own stylesheet with fixed light colors. In a dark admin4 its cards, tabs, figures, fields and confirmations follow the theme with readable contrast. The few rules the components do not have (the framed client identifier and secret, the key list as one block per key on a narrow screen) are.a4-setup-oauth-*rules instylesheets/modules/setup.css. Every form field, button and action is the same (AdminOauthadminPagesTest);design/standardkeeps its templates for other designs. Nothing has to change in an installation.
17 October 2026: export and import of layouts
Added
- Layouts can be exported and imported between installations (explayouts_core 1.0.9, explayouts_ui 1.3.22). The
admin page Layouts > Export chooses layouts (one, several, all), shared layouts, the mappings and drafts, or
everything, and downloads a versioned ZIP file (JSON documents and a
manifest.jsonwith the format version, the source site, the counts and a sha256 per file). The shared layouts a chosen layout links a zone to always come along. - Layouts > Import validates such a file and shows a dry run first: each layout and mapping is new, identical, changed
or conflicting, with the differences. A strategy can be chosen for all or per item: skip, overwrite (a new draft,
published only when asked) or create a copy. Node and object ids are mapped by remote id, and references without a
match are listed. The import writes in one transaction (rolled back on any error), makes shared-layout links by
identifier, writes audit events (
data.explayouts_core.import) and removes only the resolver cache entries of the layouts and mappings it touched. The format is described inextension/explayouts_core/doc/TRANSFER.md. ./console exp:layouts list|export|importdoes the same from the console, with--dry-run,--json,--strategy,--item,--only/--all,--publishand--unmapped.
Updated
- The console resolves the
@aliastags of extension commands as well (exp:layoutsrunsext:explayouts_ui:layouts). A full name is used as written; a bare alias stays in the extension's namespace (ext:<extension>:<alias>). The aliases the extensions already declared now work too (ConsoleExtensionAliasTest). - Export and import have policy functions of their own,
explayouts_ui/transfer_exportandexplayouts_ui/transfer_import. The import page asked forexplayouts_ui/editbefore: a role that should keep importing needsexplayouts_ui/transfer_import(roles withexplayouts_ui/*have it). - Required versions: explayouts_core ~1.0.9, explayouts_ui ~1.3.23 (1.3.22 sent no valid ZIP file: under the persistent server no file arrived, under Apache a stray newline came first; 1.3.23 fixes the download).
17 October 2026: the Velocity watchdog records an audit event only when it acts
Updated
exp:velocity watchdog run(thevc-watchdogtimer, every 45 s) no longer recordssystem.command.runfor every run, about 1,900 events a day. It records one specific event when it does something:system.velocity.watchdog.start(it started the engine, or a start failed),system.velocity.watchdog.backoff(it waited after three failed starts) orsystem.velocity.watchdog.skip(it left a down engine alone: stopped on purpose, a restart in progress, the lock held, an overlapping run, an error). A run that finds the engine running, or a paused watchdog, records nothing. Every other command still recordssystem.command.run.
Added
eZScript::setAuditCommandRun( false ): a command that records its own, more specific events can leave out the genericsystem.command.runof its run (only that run; there is no setting).eZScript::auditCommandRun()records it, andshutdown()calls it as before. The three new names are on by default (settings/audit.inican switch them off like any other name).
Tests
expVelocityWatchdogTest(no database): a run that finds the engine running writes no audit event, a start, a failed start, a backoff and a skip write exactly one specific event each, and another command still writessystem.command.run.expAuditRecordTest: the catalogue has 170 names.
17 October 2026: the Velocity watchdog records a skip only when its reason changes
Fixed
- While the server stayed down on purpose (or a restart was in progress, or the lock was held), every 45-second check
still wrote
system.velocity.watchdog.skip, some 1,900 events a day. A skip is now recorded only for the first skip of a period or a new reason. The last reason is kept inwatchdog-last.jsonbeside the markers and removed when a check finds the engine running or starts it.startandbackoffevents are unchanged;velocity-watchdog.logstill has one line per skip.
Tests
expVelocityWatchdogTest(no database): three skips with the same reason write one event, a new reason writes a new one, the engine running again resets it, and a start ends the period.
17 October 2026: enforcing the Content Security Policy
Added
site.ini [ContentSecurityPolicy]gains what an enforced policy needs; the shipped policy andMode=autostay as they were:AdditionalSources[]=<directive> <sources>adds hosts to a directive, as a list that extensions and siteaccesses can each extend.ReportOnlyDirectives[]sends a stricterContent-Security-Policy-Report-Onlybeside an enforced policy, for examplestyle-src 'self'while'unsafe-inline'styles are still allowed.ReportOnlyDesigns[](shipped:admin,admin2,admin_classic) keeps requests in the classic administration report-only, including when a user picks it with the UI switch.ReportOnlyViews[](module/view, empty by default) keeps a view that sends a policy of its own report-only under the kernel's policy, for a server that does not let the view's header replace the kernel's (Velocity).Nonce=enabledandNonceDirectives[]=script-srcadd a nonce per request. The template operatorcsp_nonce()prints it. It is never added next to'unsafe-inline', and it is renewed per request in Velocity's persistent workers.ReportEndpointsends the reports to the siteaccess's owncsp/report. An absolute report address is also announced withReporting-Endpointsandreport-to.
expContentSecurityPolicy::allowsInlineScripts()tells code whether inline scripts run under the current policy. The online editor uses it: it does not offer TinyMCE 3 where the policy is enforced without'unsafe-inline'and falls back to TinyMCE 8.- The shipped policy names
worker-src 'self'(the same as the fallback before), soAdditionalSources[]can extend it.
Updated
Mode=autoalso counts a siteaccess whoseAdditionalSiteDesignList[]has an administration design (an editor siteaccess on admin4l). It is report-only there, as on the other administration siteaccesses.- admin3 runs its pre-paint column width from
admin3_head.js; no administration pagelayout has an inline script. - Rolling back is one setting:
Mode=report-only, then clear the INI cache. See Content Security Policy, "Enforcing: nonce, more sources, a stricter companion, rollback". Tests:expContentSecurityPolicyTest,AdminPrePaintGuardsTest,expOEContentSecurityPolicyEngineTest.
17 October 2026: the reports of the Content Security Policy in a log of their own
Added
- The kernel view
csp/reporttakes the reports of the Content Security Policy, in both formats (application/csp-reportofreport-uri,application/reports+jsonof the Reporting API), and writes one line per violation tovar/log/csp.log: directive, what was blocked, enforce or report, page, file, line, column and the first characters of the code. It is open to every visitor (AnonymousAccessList[],PolicyOmitList[]), answers a status without a body, and protects itself: content types, size (MaxReportBytes), rate limits per client and in total (ReportsPerMinute,ReportsPerMinuteTotal), masked and bounded lines (MaxViolationsPerReport). A flood is written tosecurity.logonce a minute asabuse/csp-report-flood. Settings:site.ini [ContentSecurityPolicy]; guide: Content Security Policy, "Reports". csp.logis a kernel log of the log viewer ("Content Security Policy reports", keycsp), rotated like the request logs (5 archives, compressed, 10 MB, pruned after 30 days). The error digest groups it by directive and blocked origin (the new groupingdirective-blocked), and the parser reads its fields.
Updated
- ezformtoken leaves a POST to
csp/reportwith a report content type alone: a signed-in browser sends its session cookie with a report, but no form token. Every other POST is checked as before.
Tests
expContentSecurityPolicyReportTestandexpLogCspReportsTest(no database): both formats, masked and bounded lines, every status, the rate limits and the one flood line, the settings and registrations, the form token exception and the log viewer's key, levels, rotation, parser and digest.
17 October 2026: the setup wizard no longer takes an installed site offline
Fixed
- A GET of
setup/init(a crawler of a signed-in administrator) started the web setup wizard on an installed site, and its maintenance mode answered every page with 503 "The site is being set up". Opening the wizard now changes nothing: maintenance starts only with the first posted step of a fresh installation. - On an installed site
setup/initshows "This site is already installed" with the way to reinstall (the kickstarter or a fresh installation) and writes nothing: no maintenance marker, no setup log run. Installed meansvar/site_installed.jsonexists (the wizard's last page writes it), the site is served normally, or the database holds content. - The warning "Undefined variable $partName" of the setup view.
Added
settings/site.ini [SetupSettings] AllowWizardOnInstalledSite(defaultdisabled, development only): when enabled, the wizard runs on an installed site after a confirm page posted back with a one-time token.expSetupWizardGuard(kernel/classes/expsetupwizardguard.php) decides what a wizard request may do.
Updated
- The wizard's lease is 15 minutes (
expMaintenance::WIZARD_LEASE= 900, was 1,800), renewed by posted steps only and at the end of a long step; the browser's cookie is renewed with it.
Tests
SetupWizardGuardTest(no database): an installed site refuses the wizard whatever is posted, a GET never starts maintenance, it starts only from a posted step or a confirmed POST, the confirm token is used once and only with POST, the switch is off by default, and an abandoned wizard's lease runs out.
18 October 2026: the help system in the administration
Added
- The kernel module exphelp (
kernel/exphelp/): the documentation of the repository inside the administration.exphelp/dashboard(search, the help per audience, categories, featured pages, recent changes, how to report an error),exphelp/viewer/<page>(a page ofdoc/, rendered with its navigation, contents, previous and next page, and an "Edit on GitHub" link),exphelp/search(results page, and JSON for the instant search),exphelp/asset(images ofdoc/) andexphelp/report(which tracker, a prefilled issue, safe diagnostics, the private address for security issues). Guide:doc/eng-us/features/6.0/help-system.md. - The policy function
exphelp/read. Administrators have it; Anonymous does not; give it to the Editor role to let editors read the help. - The help shows only git-tracked files below
doc/(exphelp.ini [IndexSettings]); everything else, untracked files,.., symbolic links, generated HTML and SVG, is a 404. Markdown is rendered safely byexpHelpMarkdownRenderer(raw HTML escaped, unsafe links dropped, images only fromdoc/orhttps://) with server-side highlighting (expHelpHighlighter) and the code colour tokens--a4-code-*(light and dark). - Context help: a Help link in the header of admin4, admin4l and the classic admin opens the page of
exphelp.ini [ContextHelp] Map[]for the current module and view (expHelpContextRegistry, fetchexphelp/context; extensions add entries in their ownexphelp.ini.append.phpor with anexpHelpContextProviderInterface). The link checks its page in a small cached list of served pages, never in the help index, so it adds no measurable time to an administration page. ./console exp:help index|search|check: rebuild the index and the search index, search from the command line, and report broken links and images indoc/.- New settings file
settings/exphelp.ini; module stylesheetstylesheets/modules/help.css(design/admin and design/admin4) and scriptjavascript/exphelp.js(instant search,/to focus, contents that follow the reading position, copy buttons, collapsible sections; no inline script, CSP-clean). - Translations of every new string (context
kernel/exphelp) in the complete catalogues. - Tests:
tests/tests/kernel/classes/help/*(rendering and XSS, served paths, index, search, context, error report without secrets, module policy),AdminHelpScriptTest(the script under the mini DOM), no database. exphelp/contents: every help page as one tree with a filter field; the viewer's side navigation lists the complete tree too (expHelpNavigation, cached per index, siteaccess and language; folder names inexphelp.ini [NavigationSettings] FolderNames[]).- Fixed: the help pages styled
.a4-helpas their page frame, which is also the shared help text under form fields; as help.css loads on every admin page, field help texts across the administration were squeezed to one word per line. The help frame is.a4-help-framenow, and a test keeps help.css off.a4-help. exphelp/contentsopens with every folder closed (title and page count); Expand all and Collapse all in a panel fixed near the bottom of the window, above the debug bar or its minimised tab (--exp-debug-clearance, set byexpdebugbar.css), mirrored in right-to-left languages; the filter opens the folders of its matches and puts them back when emptied;#exphelp-folder-<path>opens a folder. Strings in the 11 complete catalogues.- The help dashboard shows the Exponential logo above "Help center" (the admin4 header's drawing, following the dark mode; the same drawing's file in the classic design), named "Exponential", at a fixed size.
- The help navigation (at the side of every help page and on
exphelp/contents) keeps the filter text and the folders the reader opened or closed (by their path; Expand all / Collapse all too) from page to page in the browser's localStorage (exphelp-nav); the folders of the page in view are always open; a Clear filter button and Esc empty the filter; without storage the page still works. Expand all / Collapse all now sit sticky at the end of the help column, so the Tools sidebar can no longer cover them. - The help navigation's stored folders and filter are applied before the page is first drawn (the script opens each
stored folder as the page is read; a stored filter keeps the tree hidden, with its room, until it is applied), and
help page images carry their width and height, so neither moves the page (layout shift). In the classic design the
page frame clips with
overflow: clipinstead ofhidden, so sticky controls such as Expand all / Collapse all stay in the window. - Search and browse addresses with view parameters (
/exphelp/search/(q)/<words>/(category)/<id>/(audience)/<id>,(type),(offset),(limit)); without(q)the page lists every page the filters allow. Old?q=addresses and the search form without scripts are redirected once to the canonical address; the JSON of the instant search accepts both forms. - The header's "?" opens a Help menu in admin4, admin4l, the classic admin and AdminUI (search, the Help center right
below it, help for this page, related topics, quick links;
exphelp.ini [ContextHelp] MenuTopics,MenuLinks[], fetchexphelp/context_menu); the Help center is linked from the user menu, the footer, the dashboard and the Setup overview. In the classic admin and AdminUI the menu is a native disclosure; Escape closes it and gives the focus back to its button. The root requires exp_adminui 1.0.0.48 and ezwt 6.0.12, whose website toolbar "?" opens the Help center for a signed-in editor. - Print and PDF: a help page prints without the administration around it, and "Download as PDF"
(
exphelp/pdf/<page>,./console exp:help pdf) draws it with the kernel's own PDF library (exphelp.ini [PdfSettings]). - The help states its license, the GNU Free Documentation License 1.3 or later:
doc/eng-us/LICENSE-GFDL-1.3.txt,doc/eng-us/README-LICENSE.md, a notice on every help page, its print view and PDF, and a Free documentation block on the dashboard (exphelp.ini [License]). - The release history (3.x to 6.x) and the branch history are shown as tables per series with a filter
and sortable columns (
expHelpRecordRenderer). - Help pages: developing with Git and growing from user to developer (with a dashboard band), changing the administrator password, the default administrator password per install method, and security hardening.
- Help pages on the supported databases (SQLite 3, the default, then PostgreSQL, MongoDB, MariaDB, Oracle and MySQL;
Microsoft SQL Server is possible with additional work) and on the supported operating systems; the installation
guides state the same order.
composer.jsonsuggestsext-sqlite3as the default database and lists the others in that order. php bin/php/addexphelpreadpolicy.php --apply(./console exp:help-grant --apply) gives the Editor roleexphelp/readon an existing installation.- Examples (
doc/eng-us/examples/, category "Examples", featured on the dashboard): web server configurations for Apache (.htaccess, virtual host), nginx, Caddy, FrankenPHP, LiteSpeed, IIS, PHP-FPM and Velocity, each with the routing of.htaccess_rootand checked (syntax, and test servers for Apache and nginx); a cookbook of fourteen worked tasks; and about 2 400 reference pages generated from the code by./console exp:help:examples(template operators and functions, fetch functions, module views, policies, INI settings per block, datatypes, workflow events and triggers, cronjobs, console commands, extension points).--checkreports pages that are not current.doc/eng-us/examples/htaccessanddoc/eng-us/examples/ezpublish.confwere rewritten and are free documentation now. exphelp.ini: the categoryexamples, the page typegenerated(TypeBoost[generated]=0.5, left off the audience cards) and[NavigationSettings] FoldedFolders[]: a folded folder shows its overview and page count and lists its pages only while a page inside is open, so the generated pages add no links to the other pages.- Two more audiences of the help, Project managers (
project-manager) and Sales & marketing (sales-marketing): their dashboard cards link a start page, the search filters, the page pills and the categories Project leadership (doc/eng-us/help/leadership/) and Sales and marketing (doc/eng-us/help/business/) know them. Help pages for both: planning, scoping, hosting choices, checklists, quality gates, releases, risks, Git teams, templates and measures for project managers; positioning, feature highlights, factual comparisons, demo scripts, objections, licensing, brand rules and the community for sales and marketing; and shared pages on working together, becoming a community leader and a reading path.exphelp.ini [AudienceSettings]and[CategorySettings]list them; installations with their own audience list in an override add the two ids there. - Eight community audiences of the help: Partners (
partner), Advocates (advocate), Designers (designer), Hosting companies (hosting), Application creators (app-creator), Website creators (website-creator), Supporters (supporter) and Sponsors (sponsor), each with a dashboard card, a search filter and a page pill, and their folders underdoc/eng-us/help/(partners/,advocates/,designers/,hosting/,creators/app-creators/,creators/website-creators/,supporters/,sponsors/; pages inextension-development/are for developers and designers). New categories with an introduction each: Partnership, Advocacy and community, Theme design, Templates, Extension development, Theme extensions, Publishing and distribution, Selling themes and services, Maintenance life cycle, Hosting, Building applications, Building websites, Supporting Exponential and Sponsorship. Installations with their own audience list in an override add the new ids there. - Landing pages for every audience of the help,
exphelp/audience/<audience>, opened from the dashboard's audience cards. They are built from blocks set inexphelp.ini [LandingSettings]and[Landing_<audience>]: a hero with a summary, a search box and the start page, the reading path, info boxes, a search button per category, key pages, recently updated pages and related audiences. The developers' page has a paged log of the latest commits to Exponential, read by the server from the GitHub feed with a timeout and a 10-minute cache and kept when GitHub cannot be reached ([CommitFeedSettings]). A Support Exponential block at the bottom of the dashboard and on the supporters' and sponsors' pages asks for monthly sponsorships of 7x and of the Exponential Foundation through GitHub Sponsors ([SupportSettings]). - Two more audiences of the help, each with a dashboard card, a search filter, a page pill and a landing page:
Trainers (
trainer, category Training,doc/eng-us/help/trainers/) and Press, media and influencers (press, category Press and media,doc/eng-us/help/press/). The dashboard's "Grow from user to developer" band has a fifth step, Contribute extensions back, shown once its pagedoc/eng-us/help/contribute-extensions-back.mdis in the help. - The help cache no longer grows with every change of the documentation: rebuilding the index removes the older
indexes and the navigation trees and rendered pages that belonged to them (one stale index is about 5 MB). A new
category, The Exponential project (
project,doc/eng-us/help/project/), holds the pages about the project itself. - Two more audiences of the help with dashboard cards, search filters, pills and landing pages: Businesses
(
businesses, category Exponential for businessesbusiness-solutions,doc/eng-us/help/businesses/) and Governments (government, category Public sectorpublic-sector,doc/eng-us/help/government/); and the category Two ways to build (build-two-ways,doc/eng-us/help/build-two-ways/). The help search weighs the trainers' lessons below the editor and administrator pages they teach ([SearchSettings] AudienceBoost[trainer]=0.6), except while the search is filtered to trainers. - One more audience of the help with a dashboard card, a search filter, a pill and a landing page: Customers
(
customer, category Customer successcustomer-success,doc/eng-us/help/customers/), for the organizations and people who run their own site on Exponential and their teams; the aliasescustomers,site-ownerandsite-ownersselect it too. - The help search keeps to a time budget: the words of each title are stored in the search index (format 4) instead
of being split and stemmed again for every hit of every query, and a test (
expHelpSearchBudgetTest, no database) fails when a query over 7 000 generated pages takes more than 300 ms of CPU time at the 95th percentile.
Fixed
- The search form sent without scripts redirected to itself until the browser gave up; the view now sends its redirect without the query string.
exp:help checkreported a backslash-escaped\[x\](y)as a broken link; it reads it as text, as the renderer does.
18 October 2026: quick location buttons on the upload page
Added
expNodeQuickPick(kernel/classes/expnodequickpick.php): the quick location buttons of a page that asks for a place in the content tree. It finds the well-known places from a short key and content.ini (Websites, Media, Users, a node id,remote:<node remote id>, a URL alias path), never from fixed node ids, and builds a row of the places right below one of them with the current one and the one it lies in marked, filtered by a check the page gives. Guide:doc/eng-us/features/6.0/quick-location-buttons.md; tests:expNodeQuickPickTest(QP-01 to QP-05), no database.- The shared component
.a4-quickpickincomponents.css(design/admin and design/admin4): labelled rows of small buttons or links, the current one filled (aria-pressed,aria-currentor.a4-btn-primary), the one the current place lies in outlined (.is-within), in light, dark and forced colors. - The upload page of ezmultiupload 6.1.8 uses both: under its target location, Websites, Media, Images and Files, then the sites and the other folders of Media, only where the user may create the classes uploaded files become; one click makes a place the target. Required version: ezmultiupload ~6.1.8.
Updated
- The dashboard's Upload files button (admin4 and admin4l) opens the upload page at Media > Images where the user
can upload there, else at Media (ezmultiupload 6.1.8,
ezmultiupload.ini [DashboardSettings] UploadTargetList[], fetch functiondashboard_target), so uploads land in the Images folder first. The Admin UI's dashboard does the same from exp_adminui 1.0.0.43. - The content checks' start location takes its presets and its row of sites from
expNodeQuickPick(expContentCheckScope::presetSource(),presetNodeID(),presetLabel()andsiteRow()keep their names and answers, and a preset can now also beremote:<node remote id>). Its styles moved from.a4-setup-scopeinstylesheets/modules/setup.cssto the shared.a4-quickpick; a site that styled.a4-setup-scoperenames it. - Required versions: ezmultiupload ~6.1.8 (exp_adminui ~1.0.0.44 includes 1.0.0.43). The quick location buttons' scripts are tested
under the mini DOM (
expMultiuploadQuickLocationsScriptTest, QJ-01 to QJ-04, node, no browser).
17 October 2026: Manage logs names every log again, 404.log included
Setup > Manage logs (/setup/logsmanage) showed every log as a link without a name, marked "Web server, read only",
without its actions: the kernel's expLogManager::logs() left out the fields the page reads (label, lines,
managed), which only expLogManagerFallback gave.
Fixed
expLogManager::logs()gives each log itslabel(theLogLabel[<key>]ofsettings/logviewer.ini, else the file name), itslines(about) andmanaged, asexpLogManagerInterfacenow documents for every implementation.- A key of digits only (
404ofvar/log/404.log) is an integer as soon as PHP uses it as an array key. The log viewer keeps it the string it is everywhere:logs()of both managers,select(),plan()(also for every log) andapply()name it"404",expLogFileLocator::resolveKey()andlabelOf()also take the integer, and Log health asks for its settings and builds its link with the string. The key is not renamed:/setup/logs/view/404stays. - Log health showed "Array" for the largest size and the archives kept of every log:
expLogHealth::settings()now reads the{value, source}entries ofexpLogManager::effectiveSettings()as well as the flat limits.
Tests
expLogNumericKeyTest(no database): the key404through the settings, the locator (also a rotation), both managers (fields, label, plan and apply), the permalink, Log health and the digest settings.
18 October 2026: the upload page in Ukrainian
Updated
- Required version: ezmultiupload ~6.1.9, which adds a complete Ukrainian translation of the upload page (its queue,
the location picker and the quick location buttons, the actions on uploaded items, the ZIP import and the server's
messages). Its terms follow the kernel's Ukrainian administration translation (
share/translations/ukr-UA), which also translates the named quick location buttons (Websites, Media, Users ...).
18 October 2026: every country of a translated language gets its translation; fallback chains
Merged pull request #283 (Germany's, France's, Spain's and the Arab world's other countries; 48 new locales from CLDR 48) and built on it. Docs: Translation fallbacks, Writing a fallback rule.
Added
i18n.ini [TranslationSettings] FallbackLanguages[]for every country of a language with a translation: the German-, French- and Spanish-speaking countries fall back toger-DE,fre-FRandesl-ES, the Arabic ones toara-SAonce it exists, andchi-SG,dut-BE,ita-CH,por-AO,por-CVto theirs. Withesl-ARthe administration was English, it is Spanish now.- 48 locales: Spanish of Paraguay, Costa Rica, Panama, Honduras, El Salvador, Nicaragua, Puerto Rico and Equatorial Guinea; French of Luxembourg, Monaco, Haiti, Vanuatu, Mauritius and 20 African countries; Arabic of Bahrain, Oman, Libya, Palestine, Yemen, Sudan, Mauritania, Djibouti, Comoros, Somalia, Chad and Eritrea (right to left); German of Luxembourg, Liechtenstein and Belgium.
- Fallback chains: a fallback can have a fallback of its own (
esl-AR -> esl-MX -> esl-ES), tried in order for each translation root, with cycles stopped andFallbackMaxDepth(default 4). expI18nFallbackResolver, the one source of truth for the translator, the interface language choice,exp:i18nand Setup > Interface languages, with rules:FallbackLanguages[]first, then the classes ofFallbackRules[](expI18nFallbackRuleInterface, for extensions), then the automatic same-language fallback.AutomaticLanguageFallback=enabled: a locale with no translation of its own and no configured fallback takes the most complete translation of its language (fre-NCtofre-FR);AutomaticFallbackExclude[]=eng. It changes nothing for the shipped locales, which all have their line.FallbackMode=merge(opt-in; defaultfirst, as before): every translation of the chain is loaded, the most specific winning, soesl-MXtakes the messages it lacks fromesl-ES../console exp:i18n fallbacks [--locale=] [--only=] [--format=table|json]: per locale its chain and the kernel's and every extension's coverage;./console exp:i18n check [--strict]exits 1 on a cycle, a chain too long, a missing or invalid target or a rule that is none (for CI).- Setup > Interface languages (
setup/languages, policysetup/system_info): the same list in the administration, with one locale's chain step by step and every extension; read only.
Updated
expUISwitchLanguageCataloguecounts a locale without a translation with its whole chain (cache format 5); its entries carrychain.- The translation cache's directory includes the fallback settings, and with
TranslationCheckMTimea changed file of a fallback renews the cache of the locales that fall back to it.
Tests
expI18nFallbackResolverTest,eZTSTranslatorFallbackChainTest,expI18nFallbackReportTest(no database): chains, cycles, depth, the automatic fallback and its exclusions, rule registration and order, problems, first and merge in the translator per root, the report, the command's output and exit codes, the page's rows.expUISwitchLanguageProviderTestandexpLocaleFilesTestfrom the pull request.
17 October 2026: RSS and Collected information in the Setup mega menu
Updated
- admin4 and admin4l: the Setup mega menu lists Collected information (
infocollector/overview, group "Content model") and RSS (rss/list, group "Site and URLs") too.menu.ini [Leftmenu_setup] MegaMenuLinks[]names them after Sections (collected,rss) and keeps its twelve other links, so the default is fourteen; each keeps its access check (infocollector/read,rss/edit). - The mega menu shows at most 14 chosen links (was 12), in
parts/a4_mega_menu.tpl; the debug report (parts/a4_mega_menu_check.tpl) names a list of more than 14 keys. A site that restates a shorter list is unchanged.
Tests
AdminMegaMenuTest: the default choice namescollectedandrss, has at most 14 keys, the panel shows the first fourteen chosen links, and the debug report names 15 keys.
18 October 2026: "New draft" on the edit-conflict page opens the new draft
Updated
- The edit-conflict page (content/edit of an object that already has a draft in the language) of the admin designs
admin, admin3 and admin4 (and admin4l, which uses admin4's): "New draft" creates the draft and opens its editor. The
page posted the hidden field
DoNotEditAfterNewDraft(since 2004), so content/edit created the draft and redirected back to the same page, which looked as if the button did nothing. Not caused by the Content Security Policy: the page has no script, and it behaved the same under the report-only policy. content/edit still honors the field, so a design that wants the old behavior can post it again.
Tests
AdminEditConflictNewDraftTest(no database): no admin design postsDoNotEditAfterNewDraftbeside "New draft"; the kernel still honors it.
18 October 2026: expui's test page under an enforced Content Security Policy
Updated
- Required version: expui ~1.0.0.12. exp::sticky's
scrollToStartfocuses the first text field without scrolling again (preventScroll): on admin4 and admin4l, whose fixed header setsscroll-padding-top, the focus scrolled the field to the middle of the window and undid the scroll to the start (its expui/test case failed there, with or without a policy). - Its test page (
/expui/test) reports the three tests that need inline scripts (a dialog withscripts: true, the classiconclickcalendar icon by click and by keyboard) as "N/A under enforced CSP" with the reason, instead of failing, where the policy'sscript-srchas no'unsafe-inline'(admin4 and admin4l on the admin siteaccesses); they still run where the policy allows inline scripts (the classic design's report-only policy). The test view asksexpContentSecurityPolicy::allowsInlineScripts(). - Required version: exp_adminui ~1.0.0.45. Its edit-conflict page opens the new draft on "New draft", as the kernel's admin designs do (see above).
18 October 2026: more Setup links in the Setup mega menu
Updated
- admin4 and admin4l: the Setup mega menu lists Preload sites (
setup/preload) and Packages (package/list) in the group "Site and URLs" after RSS, and EI: Models (expai/models) and EI: Budgets (expai/budgets) next to EI: Summary in the group "AI", as the Setup menu does.menu.ini [Leftmenu_setup] MegaMenuLinks[]namespreload,packages,expai_modelsandexpai_budgetsand keeps every other link: the default is eighteen. Each keeps its access check (setup/preload,package/list,expai/model_manage,expai/log); the EI links are left out whereexpaiis not active. - The mega menu shows at most 18 chosen links (was 14); the debug report names a list of more than 18 keys.
Tests
AdminMegaMenuTest: the default choice names the new links in their places, has at most 18 keys, the panel shows the first eighteen chosen links, and the debug report names 19 keys.
18 October 2026: AdminNeo keeps its page and its policy in a persistent Velocity worker
On Exponential Velocity with persistent workers, /dse/adminneo lost AdminNeo's document head and its own
Content-Security-Policy whenever the worker had served /dse/dashboard before, so the enforced administration policy
blocked AdminNeo's inline scripts. The dashboard defined NEO_BASE_URL (AdminNeo embedded in the dashboard) and ended
on its warning gate or a redirect before it asked for the worker to be replaced. PHP-FPM was not affected.
Fixed
- Required version: sevenx_dse ~1.1.13. The dashboard asks for the worker to be replaced first, on every path;
dse/adminneodoes too, and a worker that already ran AdminNeo answers 503 with a page that reloads itself (no script).dse/adminneoremoves the kernel's report-only policy header, so the page carries AdminNeo's policy only. The extension ships[ContentSecurityPolicy] ReportOnlyViews[]=dse/adminneo, so the kernel's policy cannot block AdminNeo on a server that keeps both headers.
Tests
SevenxDseAdminNeoWorkerStateTest(no database): a process prepared by the dashboard or one that ran AdminNeo is recognised, both views ask for the worker to be replaced before they define a constant or end the request, and the reload page has no script.
18 October 2026: a mega menu for the Dashboard tab, with every Exponential Intelligence link
Added
- admin4 and admin4l (prototype, opt-in): a mega menu for the Dashboard tab, switched on with
menu.ini [TopAdminMenu] MegaMenuTabs[]=dashboard([Topmenu_dashboard] MegaMenuTemplate=design:parts/my/mega_menu.tpl,MegaMenuSection=Leftmenu_my_mega). It offers seventeen links in five groups: Overview (the Dashboard), Your work (drafts, pending items, collaboration, bookmarks), Activity and audit (the audit trail, the AI events), Exponential Intelligence (its dashboard, Vector, Chains, Missing alt text, Log, Help) and My account (notification settings, e-mail preferences, Change password and Edit profile last). Every function of the Dashboard menu is there but the shop's preferred currency. menu.ini [Leftmenu_my_mega]: a section that only sorts the tab's functions into groups. Its groups add[Leftmenu_my],[Leftmenu_audit]and[Leftmenu_design](GroupSections[]), so each link keeps the address, policies, icon and description of the menu that defines it, and the links of an extension that is not active (expai, the help) are not shown. Icons and descriptions for the Dashboard menu's links, expai's among them, for expai's AI events ([Leftmenu_audit]) and Missing alt text ([Leftmenu_design]); read only where a menu is drawn as tiles or as a mega menu, so the left menus are unchanged.GroupSections[<group>]may name several sections, separated by;, added in that order (parts/ini_menu.tpl); the check of the settings (parts/a4_mega_menu_check.tpl) reads every one of them. A single section works as before.[Topmenu_<tab>] MegaMenuLayout=wide: the panel sets its groups in up to four balanced columns of 15rem (four at 1440px, three at 960px, the 200% zoom of a 1920px screen, two below), so a long choice fits the window's height instead of scrolling inside the panel. Set for the Dashboard; the Setup and Users panels are unchanged.- The group names and descriptions in English, German, French, Spanish, Mexican Spanish, Ukrainian, Hebrew, Latin, Klingon and Arabic, and in the untranslated catalogue.
Tests
AdminMegaMenuTest: the Dashboard section and its added menus, the choice (at most 18, every function of the tab, every EI link, the audit before Exponential Intelligence, Change password and Edit profile last), icons, names and descriptions in every complete catalogue, links without access, disabled or of an inactive extension left out, the wide panel only for the Dashboard, and the check of a group that adds two sections.
18 October 2026: the administration in Arabic, and Hebrew for the newest strings
Added
- Arabic administration translation
ara-SA(Modern Standard Arabic, right to left): the whole kernel catalogue (17,307 messages) and the ezoe and ezjscore catalogues. Choosing Arabic as the interface language shows admin4l, admin4 and the classic admin in Arabic and right to left; every Arabic country falls back to it. Machine-assisted, to be reviewed by a native speaker. Contributed by ekked (pull request #284). expTranslationQualityTestchecksara-SAtoo, with the reasons for every text that may stay identical to the English source intests/tests/lib/ezi18n/translation-identical-allowlist.json.
Updated
- Hebrew (
heb-IL) translates the eight newest kernel messages of the setup wizard (pull request #285). - Arabic wording: the shop tab "Store" is the shop (it said "save"), "Login" in the user, session, audit and package lists and forms is the user name, "empty" is the adjective where a value is empty, and "State" in an address is the state or region.
doc/eng-us/languages.mdlists Arabic among the complete translations and describes both right-to-left translations.settings/i18n.inino longer givesara-SAandheb-ILthe fallbackuntranslated: both have a complete translation of their own, and the Arabic countries fall back toara-SA. A site that set either fallback itself keeps its setting.
Tests
expAdminTranslationsRegisteredTest(no database) counts Arabic among the complete translations (locale, switch, ranked group, documentation, shop tab) and checks the ezoe and ezjscore catalogues of Arabic and Hebrew for completeness. New: both right-to-left translations have a right-to-left locale named in their script, a fallback setting, and catalogues written in their script; "Login" takes the user-name sense in lists and forms.
18 October 2026: Spanish "tú", right-to-left admin4 and the header's tab icons
Merged pull requests #282, #286 and #287 and built on them.
Updated
- Spanish (#282): the remaining "usted" messages use "tú", like the rest of the catalogue; five more found by the new
check are changed too ("Vuelve a cargar el formulario", "Cambia tu contraseña", three "Selecciona" in
esl-MX). - Right to left (#286): admin4's dashboard facts, the version and the debug bar's figures keep their order in an
Arabic or Hebrew interface, the greeting is one translated message with the name isolated (Arabic writes "،"), and
ara-SA's locale is regenerated from CLDR 48. - The header's tabs (#287) show an icon before their name (
menu.ini [Topmenu_<tab>] Icon), and the header and dashboard use one set of symbols.design.ini [AdminDesignSettings] TopMenuIcons=disabledswitches the tab icons off (defaultenabled).
Tests
expSpanishTuFormTest(no database): no finishedesl-ESoresl-MXtranslation of the kernel, ezoe or ezjscore uses "usted", "Ud." or a formal imperative at a sentence's start; the conventions and forms are lists in the test.AdminRightToLeftTest(no database): every administration pagelayout setsdir="rtl", every right-to-left catalogue translates the greeting (Arabic with its comma), the isolation rules stay, the Interface languages chain points the other way, everyTextDirection=rtllocale names itself in a right-to-left script.AdminIconsTest::testTheTabIconsCanBeSwitchedOff.
18 October 2026: oAuth admin in the Setup mega menu
Updated
- admin4 and admin4l: the Setup mega menu lists oAuth admin (
oauthadmin/list) in the group "Users and messages", after Audit, as the Setup menu does.menu.ini [Leftmenu_setup] MegaMenuLinks[]namesoauth_listand keeps every other link: the default is nineteen. - The mega menu shows at most 19 chosen links (was 18); the debug report names a list of more than 19 keys.
Tests
AdminMegaMenuTest: the default choice namesoauth_listafteraudit, has at most 19 keys, the panel shows the first nineteen chosen links, and the debug report names 20 keys.
18 October 2026: the current page's tab stays in the admin4 header row
Updated
- admin4 and admin4l: when the header's main menu is too narrow for every tab, the tab of the current page is the last
to move into "More": the other tabs go first, so the page's tab shows selected in the row as on every other page,
and "More" is drawn as selected only when the row has no room even for that tab. On
setup/menuat 1440px with many extension tabs, "Setup" showed as an orange "More" before.
Tests
AdminHeaderTest: the script keeps the current tab in the row while another tab can go.
18 October 2026: every admin4 mega menu opens under its own tab, and the header's row keeps its tabs
Updated
- admin4 and admin4l: each mega menu panel (Setup, Users, Dashboard) opens under its own tab instead of in the middle
of the window: its start edge at the tab's start edge (right to left, the right edges), moved back inside the
window with 24px to spare when the tab is near its end.
a4_mega_menu.jssets the panel's--a4-mega-startthrough the CSSOM when it opens, when its links have loaded and when the window changes size; without the script the panel stays centred. - The mega menu's button is a caret inside the tab's pill, in its end padding: a tab with a mega menu is 12px wider than one without (26px before), and a selected tab keeps its pill with the caret in it. The tab is still a link.
- The tabs of the header's one-line row show their icons from 1600px; between 576 and 1599px their names alone, so at 1440px the row keeps Setup with the Help link and three mega menus (in "More" and the phone menu the icons stay).
- The Dashboard's wide panel has a fixed width (four columns of 15rem, at most the window less 48px), so it is placed as reliably as the others.
Tests
AdminMegaMenuTest: the anchored panel (its custom property, the clamp, right to left, no style attribute), the caret in the pill and the row's icons from 1600px.
18 October 2026: help maintained as content (phase 1 of the help system plan)
Help topics, manuals, figures and the glossary as content of the site, after the plan help-system.md and the help style guide. Guide: help maintained as content.
Added
./console exp:help:content(bin/php/exphelpcontent.php) with the actionsinstall(classes,tree,roles),check [--fix],seed-glossary,seed-topics [--release],reportanduninstall [--with-content].-s <siteaccess>is required,--dry-runchanges nothing, every step is idempotent (a second run prints "No change."). Nothing runs on its own: an installation creates the model by runninginstallonce.- The classes
help_manual,help_section,help_concept,help_task,help_reference,help_troubleshooting,help_screenshot,help_glossary_entry(class group Help; text fields are Markdown ineztext), the sectionshelp_publicandhelp_internal, the object state grouphelp_status(Draft, Review, Released), the folder tree below the Media root (exphelp.ini [ContentSettings] ParentNode), the roles "Help editor" and "Help release", and onecontent/readpolicy of Anonymous limited tohelp_publicand Released. - The validator of the style guide (
expHelpContentValidator): required fields per topic type, help_id form and uniqueness, one sentence of short description, exactly one numbered list of steps, alt text, glossary words, relations of manuals and chapters, review dates, and the section that the audience requires (--fixsets it on the object only). share/help/seed-topics.json(the five templates of the style guide and a manual for editors) andshare/help/seed-glossary.json.exphelp/topic/<help_id>,exphelp/manual[/<remote_id>](policyexphelp/read) and the context help JSONexphelp/context/<help_id>,exphelp/context/manual/<remote_id>with ETag (new policy functionexphelp/context). Released topics for who may read them; Draft and Review only for who may edit them, as a preview. Anonymous getsexphelp/contextonly with[ContentSettings] AnonymousContextHelp=enabled(default disabled).expHelpContextPageInterface: a context help provider implementing it may answercontent/<help_id>, and the Help control of a page opens that topic (expHelpContentProviderwith[ContentSettings] ContextHelpIds[]).
Tests
expHelpContentTest(no database, 17 cases): the model and roles, help_id parsing, the style guide's templates pass, each validator rule, sections from audiences, maps, the topic's Markdown and document, the registry acceptscontent/...only from a page provider and only with an address below/exphelp/, one Anonymous policy with both limitations, ETag handling, policy signatures, and the shipped seed files.
18 October 2026: e-books of help manuals (EPUB and PDF)
Added
./console exp:help:content export -s <siteaccess> --manual=<remote_id> --format=epub|pdf [--output=] [--dry-run]and Download EPUB / Download PDF on a manual's page in the help (exphelp/manual/<remote_id>?format=). Only Released chapters and topics the user may read go into a book. EPUB 3 with a navigation document, the figures with alt text,xml:langand EPUB Accessibility 1.1 metadata; PDF through the help's PDF writer where it is installed. Guide: help maintained as content.
Tests
expHelpContentExportTest(no database): headings below the topic title, well-formed XHTML without buttons, scripts or heading anchors, xml:lang, and no file outside the storage put into a book.
18 October 2026: well-known passwords refused everywhere, and forgot-password links expire
Added
expWeakPasswordList(kernel/classes/expweakpasswordlist.php): one list of well-known and common passwords and the check, with simple variants (case, digits and symbols at the ends, separators, letter swaps, common endings, a word typed twice) and patterns (repeats, keyboard, alphabet and digit runs). More words withsite.ini [UserSettings] WeakPasswordList[]andWeakPasswordFile;WeakPasswordCheck=disabledswitches it off for the forms (defaultenabled). See the upgrade note.site.ini [UserSettings] ForgotPasswordKeyLifetime(default3600;0: a link works until it is used, as before).
Updated
- The setup wizard refuses a well-known or common administrator password, also when it is long enough.
- The kickstarter replaces a
kickstart.iniadministrator password that is shorter thanMinPasswordLength(as it already did for an empty or well-known one) with a random one, printed once and written tovar/log/initial-admin-password(mode0600);exp:installuses the same list. user/password, registration, the user edit form,exp:resetuserpasswordand the web services (expaccount,expuser) refuse a well-known or common new password (the rulenot_commonofexpPasswordPolicy). Existing passwords keep working; nobody is refused at sign-in.- "Forgot your password?" links expire after
ForgotPasswordKeyLifetime: an expired link is refused with "This link has expired." and removed, the mail says how long the link works, and expired keys are removed on every visit of the page. The web services use the same lifetime (they had a fixed 24 hours). No database change: the tableezforgot_passwordalready stores the time.
Tests
expWeakPasswordListTestandeZForgotPasswordExpiryTest(no database);eZSetupStepsTest,InstallPasswordTestandexpPasswordPolicyTestcover the wizard, the installers, the account form and the console.
18 October 2026: clear help titles, a Standards section and the design pattern catalogue
Added
exphelp.ini [TitleSettings]:Titles[<path>],Summaries[<path>]andTitleRules[]=<pattern>;<template>(placeholders{title},{name},{fileversion},{dirversion}) give a clear title and summary to help pages whose own is unclear; a front matter title or summary always wins. 47 inherited pages are retitled (doc/eng-us/standards/header.txtshowed as "//", two README pages as "5.90", six changelogs as "CHANGELOG"), and the help has no duplicate titles any more. Help system.- A Standards section in the help (
doc/eng-us/standards/, categorystandards, featured on the dashboard): which standards Exponential follows and its compliance with each, with evidence and known gaps (accessibility, security, privacy, the web platform, internationalization, PHP and Composer, coding standards, testing and CI, source control and releases, documentation and publishing, APIs and protocols, databases and SQL), and its architectural rules (kernel architecture, layering and naming, compatibility, releases and deployment). Standards. php bin/php/exppatterncatalogue.php [--write|--check]: generates the design pattern catalogue (doc/eng-us/standards/patterns/) from the source code with PHP's tokenizer, no database: every class, interface, module view, policy function, fetch function, template operator, cronjob part, event hook and INI plugin point of the kernel, lib/ and the se7enxweb extensions, in 29 pattern categories, with file, description and extension point, one page per pattern and per area. The output is reproducible (sorted, no dates). The pages have the new help page typecatalogue([SearchSettings] TypeBoost[catalogue]=0.5, left out of the dashboard's audience cards), so the written pages rank first. Design pattern catalogue.
Tests
expHelpPageTitlesTest(no database): exact titles and summaries, title rules and their placeholders, front matter wins, pages without an entry keep their title, and everyTitles[]entry of the shipped settings names a file.
18 October 2026: ten more complete administration translations
Added
- Translations: complete administrations in Turkish (
tur-TR, new), Polish (pol-PL), Italian (ita-IT), Portuguese (Brazil) (por-BR), Dutch (dut-NL), Swedish (swe-SE), Norwegian Bokmål (nor-NO), Danish (dan-DK), Czech (cze-CZ) and Croatian (cro-HR): all 17,763 strings of the kernel, and the catalogues of ezoe and ezjscore (ezjscore 1.8.5). Machine-assisted with a glossary per language; the older hand-made translations of these languages keep every message, and those with broken placeholders, tags, line breaks or English leftovers were redone. A review by a native speaker is welcome. Languages. - Translations: the Layouts tab and its pages in the ten languages (explayouts_ui 1.3.26).
Fixed
- Translations: the header's Store tab names the shop in the ten languages (it said "save"), Turkish save buttons say "Kaydet" (they said "shop"), and Polish buttons and actions use the imperative ("Zapisz", "Otwórz") instead of the infinitive.
Tests
expTranslationQualityTestchecks the ten languages too (placeholders, tags, entities, whitespace, line breaks, English left in), each identical text with its reason intranslation-identical-allowlist.json.
18 October 2026: the MySQL export for MongoDB needs a password from the environment
Updated
bin/mongodb/export_mysql.shhas no default database password any more:MYSQL_PASSWORDmust be set (from a secret store), or the script asks for it in a terminal; without it the script stops with a clear message and writes nothing. The password reachesmysql2ndjson.pyin its environment instead of on the command line, so it is not in the process list;mysql2ndjson.pyreadsMYSQL_PASSWORDwhen--passwordis not given.
18 October 2026: the role and policy template operators are registered
Updated
has_role,has_role_id,has_any_role,has_role_by_user,has_role_by_user_id,has_policyandhas_policy_by_user(role and policy template operators) are registered inlib/eztemplate/classes/eztemplateautoload.php; before, a template that used them got "unknown operator".
Tests
eZTemplateRoleOperatorRegistrationTest(no database): the autoload entry names the class's seven operators, the engine knows them, and templates using them render, interpreted and compiled, without an error.
19 October 2026: package fixes: the index address, escaped index data, version comparison, the wizard's tiles
Updated
- The setup wizard's fallback for an empty
[RepositorySettings] RemotePackagesIndexURL(package.ini) buildsRemotePackagesIndexURLBaseplus6.0/6.0.15/, the folder the package server has, through the newExponentialSDK::packageServerPath(). Before, it appended the release state (6.0/6.0.15stable/index.xml) and got404. A release that is not stable keeps its state (6.0/6.0.16alpha1). Package server settings. - Security: the Site package step (
setup/init/site_types.tpl) escapes every field of a remoteindex.xmlentry it prints:description(plain text, line breaks kept; before printed as HTML),summary,version,url, the requirement names and versions and the repository address.eZStepSiteTypes::sanitizeRemotePackageAttributes()keeps aurlorthumbnail_urlonly when it is anhttp://orhttps://address (nojavascript:,data:orfile://, so neither an image nor a download can use one) and skips an entry whosenameis not letters, digits,.,-and_. The standard package view escapes the maintainer and changelog e-mail addresses in itsmailto:links. The index.xml format. eZPackageVersion::compareFull()reads a release with a state word, the form the 7x site packages carry:6.0-15-stableis version6.0, release15, and meetsmin-version="6.0". Before, it was read as the prerelease15-stableof6.0.0, lower than6.0. A stable word (stable,final,release,ga) is no prerelease, any other (rc1) sorts before the plain release; releases compare as numbers (9-stable<15-stable). New:eZPackageVersion::splitReleaseState(),isStableWord(). Version numbers.- Package creation wizard (
package/create, admin4 and classic): each wizard's description fills its tile again; it showed one character per line. The cause was the help module's frame rule for the bare.a4-helpclass (a grid withcontainer-type: inline-size), which also matched the shared hint text under form fields and made it 0px wide in a flex row. The help module's rules name the help pages' frame,.a4-help-frame(the same fix also landed separately). doc/eng-us/install/04-choosing-an-install-method.md: twelve site packages in the 6.0.15 index (withcjw_multisite), and the type element written aspackage.xmlhas it,<type value="site" />.
Tests
eZPackageVersionTest(releases with a state word, stable words after a legacy number),ExponentialSDKPackageServerPathTest,SetupRemoteIndexSanitizingTestandAdminHelpFrameClassScopeTest, all without a database.
19 October 2026: packages keep their install scripts on export, dependencies down the whole chain, more package servers, uninstalling files
Updated
eZPackage::domStructure()(used when a package is stored or exported) writes<settings>(the install scripts of a site package) and every attribute of a dependency item (min-version,max-version...). Before, a site package re-exported from the repository lost both.exportToArchive()also takes every other file and folder at the top of the package (eZPackage::extraTopLevelEntries(), such as thecjw-*.jsonside files ofcjw_multisite_democontent), not hidden ones or symbolic links. package.xml reference.- The setup wizard, the kickstarter and
exp:installdownload a site package's requirements down the whole chain, each package once (eZStepSiteTypes::downloadDependantPackages()); before only the direct ones. The Create sites step reads the content languages of the whole chain (eZStepCreateSites::requirementsInInstallOrder()). TheSitePackagesstep stays out ofStepTable, now with the reason in place: it depends oneZSetupFunctionality(), which no longer exists. Dependencies and install order. - More package servers: the three setup tools read, after
RemotePackagesIndexURL, the servers of the newpackage.ini [RepositorySettings] AdditionalRemotePackagesIndexURLs[](empty) and of the lists named byRemotePackagesIndexSources[], by defaultezupdate.ini;PackageServerSettings;Servers, the servers added on ezupdate's Package servers page. A package an earlier index lists is kept as it is; only http(s) addresses are read; a server that cannot be read is skipped. EmptyRemotePackagesIndexSources[]to read onlypackage.ini. Package server settings. extensionis a package type ofpackage.ini [PackageSettings] TypeList, so the Type limitation of the package policies can name extension packages.- Uninstalling a package removes the files its
ezfileitems created, when they are unchanged, inside the installation and one of the package's own files (eZFilePackageHandler::uninstall(), install datainstalled_files); a file the install replaced or the site changed stays.ezsqlandeziniaddonitems say in the debug output why they undo nothing. Install items and handlers.
Tests
eZPackageSitePackageExportTest,eZFilePackageHandlerUninstallTestand more cases inSetupRemoteIndexSanitizingTest, all without a database.
19 October 2026: ezupdate 1.1.24
Updated
- Required packages:
se7enxweb/ezupdate~1.1.24. Its Package servers page built the built-in server's fallback address (an emptyRemotePackagesIndexURL) asRemotePackagesIndexURLBaseplus6.0/6.0.15stable, a folder the package server does not have; it now builds6.0/6.0.15throughExponentialSDK::packageServerPath()(eZUpdatePackageServers::releasePath(), the same rule on an older kernel). Package servers in ezupdate.
Tests
ezUpdatePackageServersReleasePathTest(no database; skipped without ezupdate 1.1.24).
19 October 2026: the 7x site packages on the package server corrected
Updated
- Package server, 6.0.15 index: the four 7x site packages and
cjw_multisiteare listed with the version theirpackage.xmlstates,6.0-15-stable(the texteZPackage::getVersion()returns); they were listed as1.0.0-0and6.0.15-0. The setup wizard compares the index version with an imported copy, so an imported copy now compares equal and is kept. sevenx_multisite,sevenx_multisite_clean: themd5sumoflogo.pnginpackage.xmlmatches the file.sevenx_multisite_clean,sevenx_multisite_democontent,sevenx_multisite_democontent_clean:documents/LICENCEnames the package itself (it named the package it was copied from). The license is unchanged, GPL 2.0 or later.- The source repositories
sevenx-ezpackageandcjw_multisite-ezpackagehave a README and aLICENSEwith the full GPL version 2 text;cjw_multisite-ezpackagehas thepackages/build.shof the 7x repository; both build scripts leave backup copies (*.bak,*.bak.*) out of every package. - The package server repository has a
CONTRIBUTING.mdon submitting a site package by pull request (Contribute your site package).
Removed
sevenx_multisite_democontent: 113 unused backup copies of object files (object-media-o-<n>.xml.bak.*). Nothing read them; the archive is 355 KB smaller.
Tests
- The rebuilt archives were imported into a throwaway installation (every
md5sumchecked, no backup copies,LICENCEnames the package) and installed on SQLite withexp:installfrom a local copy of the package server with the new index:sevenx_multisite,sevenx_multisite_cleanandcjw_multisite(withger-DE) each INSTALLED with 0 errors and 0 warnings.
19 October 2026: the design extension wizard writes a theme that installs and registers its operators
Updated
- The
composer.jsonof every Setup > RAD extension wizard carriesextra.ezpublish-legacy-extension-name(next toinstaller-name), the key the legacy extension installer reads. Before, a theme namedmy_themeinstalled intoextension/my-theme, after the dashed package name. - The design extension wizard's Template operators part writes
autoloads/eztemplateautoload.php, so the example operator is registered once the extension is active. Before, only the class and theExtensionAutoloadPath[]line were written, and a template using the operator got "unknown operator". - The design extension wizard writes
FrontendCSSFileList[]andFrontendJavaScriptList[]instead of the deprecatedCSSFileList[]andJavaScriptList[]. The generatedpage_head.tplandpagelayout.tplload both lists, as the shipped designs do. The page head, title and script loading were corrected earlier the same week (the generated templates load their stylesheets and scripts withezcss_loadandezscript_load). - The designer help (The design wizard's parts) says what is fixed from 6.0.15 on and keeps the hand corrections for themes generated by older wizards.
Tests
expDesignExtensionWizardTest(no database): the generatedcomposer.jsoninstalls into a folder named after the extension, by the installer's own rule; the operator autoload file is written, and with the extension's autoloads directory handed to the template engine the operator is registered and works;design.ini.append.phphas no deprecated keys.expExtensionWizardBaseTestchecks the newextrakey.
19 October 2026: the RAD template wizard's field help is text again, and wizards open without errors
Updated
- Setup > RAD > Template extension (
/setup/templateoperator) printed template code as the help of the Operators, Functions, Fetch functions and Fetch aliases fields and of the "Takes input" switch, in admin and admin4: a}inside the quoted text of ani18ntag closed the tag early. Inside a quoted string the closing brace is written\}, as the URL wildcard page already does; the translations keep their source texts. - The RAD wizards (datatype, workflow event, template, design, content, handler, module, settings extension and kernel override) show their problems once the form has been sent, not on a first visit. Writing and the archive still check every problem.
Tests
AdminI18nStringTagsTest(no database): every{'text'|i18n( 'context' )}tag of the setup pages of every admin design renders, interpreted and compiled, as its text, with no|i18nor quote left over and its braces kept.expRadWizardFirstVisitProblemsTest(no database): the ten wizard views hide the problems of a first visit, and every wizard form postsSubmitted.
19 October 2026: more corrections in what the RAD wizards write, a view count without a warning, and no blank autoload entry
Updated
- Template extension wizard: the README and
doc/examples.phpcall an operator with its values by position ({$value|op( 3, 'text' )});name=valuein an operator's brackets is refused by the template parser. Template functions keepname=value. The generatedoperatorTemplateHints()no longer listsinput-as-parametertwice. - The READMEs of the template, datatype and settings extension wizards give
php bin/php/ezpgenerateautoloads.php --extension;--extensiontakes no value, and--extension=<name>was refused. - Datatype wizard: the templates go to
design/standard/templates/, whereDesignExtensions[]finds them, not totemplates/in the extension root. The edit and collect templates post_<type>_<column>_for the storage column chosen (the class carries it asVALUE_VARIABLE, andobjectAttributeContent()reads that column), not always_data_text_.DataTypeGroup[], which nothing reads, is no longer written, and the form no longer asks for a group. - Module wizard: the templates go to
design/standard/templates/<module>/, notdesign/<extension>/, which is in no design chain. The view script casts only number parameters (ID,NodeID,Offset...) toint; text parameters stay text and are washed by the template. - Design extension wizard: the README and
design.ini.append.phpsay that a siteaccess has to name the design inSiteDesignorAdditionalSiteDesignList[];DesignExtensions[]alone does not make a site use it. - Handler wizard: the mail transport stub logs an error and returns
false, with a TODO, instead of returningtrueand sending nothing. eZContentObjectTreeNode::viewCount()returns 0 for a node without a row inezview_counter, without the warning "Trying to access array offset on null".- The autoload generator no longer takes a method called
classorenum(function class(),$o->class()) for a declaration.expObjectServices::class()in extension expservices gave an entry with the blank class name' 'invar/autoload/ezp_extension.php. - RAD tools lists the corrections to make by hand in output from an older wizard.
Tests
expRadWizardGeneratedCodeFaultsTest(no database): one test per fault above, on the generated files.eZContentObjectTreeNodeViewCountTest(no database): 0 and no warning without a counter row, the count with one.eZAutoloadGeneratorTest: a class with methods calledclassandenumgives one entry.expRadWizardListsTestandexpModuleWizardTestfollow the operator call and the template path.
19 October 2026: the help index looks at its files once every few seconds
Updated
- The help index looks at the size and time of every listed help file at most once every
[IndexSettings] StatFilesIntervalseconds (exphelp.ini, default5), for every request and process: the answer is kept insignature.phpin the help cache directory. Before, each help request looked at all of them (6,195 files on a full installation), about 25 ms behind PHP-FPM and half a second under Velocity, whose file layer answers each look. An edited page now shows within the interval; a page added or removed (the git index) and./console exp:help indexare seen at once.StatFilesInterval=0keeps the old behavior. - Together with Exponential Velocity's change of the same day (a file that cannot need the include transform is not tokenized: the help index, a 14 MB data file, cost each worker 3 to 5 seconds on its first help request after it changed), this removes the two costs that made the help views on Velocity take 1 to 5 seconds where PHP-FPM took about half a second.
Tests
expHelpDocumentIndexTest(no database): HX-08 checks that a look at the files is reused within the interval and never after it, thatStatFilesInterval=0sees an edit at once, that a rebuild always looks again and leaves its look for the others, and that a commit is seen at once.
19 October 2026: visitors can register on a new installation, and the top folder carries the current welcome text
Updated
- The base data (
share/db_data.dba, which the setup wizard and the kickstarter load, and the clean datakernel/sql/common/cleandata.sqlandkernel/sql/sqlite/cleandata.sql) gives the Anonymous role the policy User / Register (policy 336, no limitation). A new installation takesuser/registerout ofPolicyOmitList[](PolicyOmitListExclude[]=user/register), and without the policy its visitors could not register and the login pages showed no register link. To close registration, remove the policy from the Anonymous role. An upgraded site is not changed.user/activate,user/successanduser/forgotpasswordstay inPolicyOmitList[]. - The top folder (node 2) of a new installation carries the current Welcome to Exponential text in its short
description and description, with the links written as addresses (
href), since a new installation has none of the link rows of the site the text was written on. share/db_data.dbagives the Editor role the four policies the clean data files already gave it: uiswitch / language, uiswitch / design, oauth / authorize and exphelp / read (policies 332 to 335, no limitations). An installation through the setup wizard or the kickstarter, which load the.dba, gave Editors none of them: no interface language or design switch, no OAuth authorization and no help pages. The roles, policies, limitations, their values and the role assignments are now the same rows in all three files.- The search index of the base data (
ezsearch_word,ezsearch_object_word_link, in all three files) indexes the top folder's current text, so a new installation finds it without a reindex. The rows were still those of the first eZ plain site text. They were generated by the kernel's owneZSearchEngine(remove, then add the object) in a throwaway SQLite database loaded from the.dba, and are word for word the index that a development installation's live database holds for the same text: 1,531 rows for the top folder, 625 words, each word'sobject_countthe number of objects linking it. The rows of the other objects are unchanged; words used only by the old text are gone. - Every table now holds the same rows in
share/db_data.dbaand both clean data files:share/db_data.dbaseeds the content language eng-US "English (American)" (id 2), as the clean data files did. The setup wizard added it itself when the table was empty; it looks the language up first (eZContentLanguage::fetchByLocale(), andaddLanguage()does too), so a seeded row is used and never doubled, and id 2 is the id every language mask of the base data is built on.- The site meta data (object 54,
ezinisetting) in both clean data files names the Exponential Foundation and the Exponential keywords, as the.dbadid, instead of eZ Systems. - Both clean data files say
ezpublish-version6.0.15stable (they said 6.0.10stable and 6.0.6stable), the value the.dbaalready had, the 6.0.15 database updates write and the upgrade checks (ExponentialSDK::version(), the LoveStack check) expect for this release.
Tests
expBaseDataSeedTest(no database): the Anonymous role hasuser/registerwithout a limitation in all three files, policy ids are unique, and the top folder's two texts are the same in all three, well-formed, with every link an address. BD-03: every table of the.dbaholds the same rows in both clean data files, which have no table the.dbalacks; the Editor role has the four policies in the.dba; the base data says 6.0.15stable and has the one language eng-US, id 2. BD-04: every word'sobject_countmatches its links, the top folder's placements run without a gap with the previous and next words of each slice of 1,000 thateZSearchEnginewrites, and each indexed word comes from its text.
19 October 2026: demo mode for public demos (EXP_DEMO_MODE=1)
Added
- Demo mode: the environment variable
EXP_DEMO_MODE=1(only the exact value1; never an INI setting or a request parameter) marks an installation as a public demo whose administrator sign-in is published.expDemoMode(kernel/classes/expdemomode.php) reads it on every call, so persistent Velocity and FrankenPHP workers follow the environment they have. See the upgrade note. - The template operator
exp_demo_mode()(expDemoModeOperator) and the bannerdesign/{admin,admin4}/templates/parts/exp_demo_mode_banner.tpl, shown on every page of admin4, admin4l and the classic admin while demo mode is on (the shared component.a4-notice.is-warn, outside the cache-blocks). - The audit event
system.demo.mode(warning, always on) and ademo/modeline invar/log/security.log, once per process that starts with demo mode on. - Help pages: Demo mode, Demo mode for hosts and Demo mode for developers.
Updated
- In demo mode the password of an administrator (unlimited access to every module and function) cannot be changed or
reset by any path:
user/password, "Forgot your password?" (no key and no mail; the same answer for every address; an old link is refused), the user account in content/edit,exp:resetuserpassword(also with--allow-root-user), the web services (expaccount.reset,expaccount.changePassword,expaccount.forgotRequest,expuser.setPassword: HTTP 403), the operationsuser/passwordanduser/forgotpassword, and as the last lineeZUser::store(), which keeps an administrator's stored password. Each refusal shows "This installation is a public demo: the passwords of administrators cannot be changed or reset." and is recorded with the reasondemo_mode. Other accounts keep every rule. - In demo mode the installers (setup wizard, kickstarter,
exp:installand the demo installers built on them) accept a well-known administrator password of any length (publish,publishing), so a demo installs the password it publishes; an empty one is still refused, and every other account keepsMinPasswordLength. The setup wizard's administrator page says that demo mode is on. The installation sets the administrator password insideexpDemoMode::installing()andexpDemoMode::settingInstallationAdministratorPassword(), the only placeeZUser::validatePassword()accepts a short one. - Installation, security hardening (13.6 "Demo mode must be off" and the go-live checklist), the default administrator password and the demo pages of the help describe demo mode.
Tests
expDemoModeTest(no database): the value rules, the order getenv then$_SERVERand never the request, no caching, who is an administrator, the store guard's decision, theinstalling()scope, the installers' relaxation and its limits, the once-per-process record, the operator, the banner in the three pagelayouts, every password path, the audit catalogue and the catalogues.expAuditRecordTestcounts the new event.
20 October 2026: demo mode across the documentation, and a test for the password page overrides
Added
- Demo mode FAQ, the hosting recipe
Run a public demo on LiteSpeed and Plesk,
the runbook Demo mode does not show, or the demo build fails with a generated password,
the reference Environment variables, and
demo mode examples:
.htaccessand virtual hostSetEnv, a PHP-FPM pool, nginxfastcgi_param, a systemd drop-in for Velocity, a crontab, a container environment file and the generic shape of a demo rebuild script (exp:installon SQLite in demo mode; a generated password fails the build). DemoModeUserTemplatesTest(no database): everyuser/forgotpassword.tplanduser/password.tplin the kernel's designs shows the demo mode notice throughfirst_set()and without inline CSS; an installed extension's override that does not yet is reported as incomplete, never silently passed.
Updated
- Demo mode is described where readers need it: the installation overview and chapters 2, 4 and 8 to 12, upgrading, cronjobs, roles, the audit and logs, the REST API and the services, the password and account pages, the log viewer, the environment settings, the glossaries, and the hosting pages for Plesk, cPanel, LiteSpeed, Apache, nginx, PHP-FPM pools, Velocity, FrankenPHP, containers, SQLite, cron, provisioning, the checklists and the security pages, plus the start pages for administrators, hosts, developers, press, advocates, trainers, partners and sales. Each links to the upgrade note, which lists them all.
config.php-RECOMMENDEDandconfig.env.php.examplesay that demo mode is not a constant of theirs.
20 October 2026: the documentation moves to doc/eng-us/, and the help is read in the reader's language
Renamed
- The documentation moves from
doc/todoc/eng-us/, every file with its relative path and its history (git mv), so each language of the documentation has a directory of its own (doc/<language>/).doc/changelogs/stays: it is not translated. A shortdoc/README.mdlists the language directories for readers of the repository.
Updated
- Every reference to the moved files follows them: the root files' links, the templates' guide and GitHub links, the
settings (
exphelp.inipath rules, titles and license rules, theDoc=entries of the other INI files), code comments, command line scripts, tests, the CI workflows,composer.json, the translations whose source names a guide, and the file manifest. The links between the guides and the changelogs are rewritten for their new places. The released changelogs and the history pages keep the paths of their time. - The help center reads the interface language (
site.ini [RegionalSettings] Locale, set by the language switch) and shows each page in it:exphelp.ini [DocLanguages] Map[]mapseng-USandeng-GBtoen/usandger-DEtode, and a locale without an entry uses its language's (ger-ATreads German). A page that is not translated yet is shown in English with a translated note. Page ids are the paths belowdoc/eng-us/in every language, so every help address of before the move keeps working and shows the same page in each language; an address that names a file (/exphelp/viewer/en/us/<id>, a translated path) is redirected to the id. Translated file names are listed indoc/<language>/.paths.json. Each language has its own index, search, navigation and rendered pages (lang-<language>/in the help cache); the search finds the reader's language and the English pages not translated yet. Links inside a page are resolved against the file it was read from, as on GitHub. ./console exp:help indexbuilds every language's index and search and reports how much of each is translated;--language=<directory>limits it, and makessearch,show,checkandpdfread that language.
Added
expHelpLanguage, the languages of the documentation and the path maps;exphelp.ini [DocLanguages](Default,Directories[],Map[],Names[]).- Translated documentation: what moved, what to change in your own links, settings and scripts, and how a translator adds a language.
- Reading the help in your language, a help page.
Tests
expHelpTranslatedDocumentationTest(no database): TD-01 no tracked file belowdoc/outside a language directory,doc/changelogs/anddoc/README.md; TD-02 no tracked text file names a path of before the move (the history excepted); TD-03 to TD-05 the locale map, the shipped settings and the path map; TD-06 to TD-09 the index per language with its fallback, one id per page in every language, links, the search per language and the classification of a translation. The help tests follow the new paths.
Operator note
- Links or scripts of your own that name a documentation file need
en/us/afterdoc/(doc/eng-us/INSTALL.md); settings overrides that name one in a path rule need it too. Help addresses and page ids do not change. Run./console exp:help index --rebuildonce after the deploy.
20 October 2026: the project files (README, security policy, contributing) in the help
Added
- The project files of the repository are help pages: the tracked root
*.md(README.md,SECURITY.md,CONTRIBUTING.md,COPYRIGHT.md,LICENSE.md) and theREADME.mdofbin/,design/<design>/andkernel/. Their ids areproject/<path in the repository>(/exphelp/viewer/project/README.md;/exphelp/viewer/README.mdredirects there). They have the category Project files and the type Project file (dashboard, search filters), a place at the top of the page tree of the contents page and every help page (no folder), audiences by[ProjectFiles] AudienceRules[](developers and administrators at least), and the title of their first heading. expHelpProjectFilesandexphelp.ini [ProjectFiles](Enabled,Include[],Exclude[],Category,Type,Audiences[],AudienceRules[],LicenseName,LicensePage). Test fixtures and the copies of other projects (a bundled JavaScript library's README) are excluded by default.- Project files, a help page; the section "Project files" of The help system.
Updated
- Links of the documentation to a project file (
../../../../README.md) open it in the help instead of leaving the link out, and the project files' links intodoc/eng-us/open the help pages; both are resolved against the file, as on GitHub. - The project files are English only, like the changelogs: a reader of another language sees them with the note that the page is not translated. The license notice under them names the license of the software (GPL 2.0 or later), not the documentation license.
Security
- Only files git tracks can be project files: the list comes from the git index,
git ls-filesor the release manifest, never from the directory, so the private notes an installation keeps in its root without committing them are never shown, searched, linked or redirected to, whatever a setting asks for. A tracked file in a place.gitignoreor.git/info/excludeignores is refused, and so are anything but Markdown and every path belowdoc/,extension/, atests/directory,var/,vendor/,settings/override/,settings/siteaccess/andnode_modules/, and every hidden file or directory (.github/included). These rules are not settings.
Tests
expHelpProjectFilesTest(no database; fixtures are real git repositories): PF-01 the patterns; PF-02 the names and the paths that are never project files; PF-03 untracked root notes are never listed, indexed, searched or linked, from the git index,git ls-filesand the manifest alike, even whenInclude[]names them; PF-04 ignored locations; PF-05 to PF-09 ids, classification, links both ways, the language fallback and the navigation; PF-10 this installation's tracked root files and the shipped settings; PF-11 the license; PF-12Enabled=disabled; PF-13 the translations.
Operator note
- Run
./console exp:help index --rebuildonce after the deploy.
20 October 2026: the contributing guidelines state the copyright assignment (CLA)
Updated
- Contributing guidelines state the copyright assignment (CLA):
CONTRIBUTING.mdhas a section Copyright assignment (CLA). 7x and the Exponential Foundation require a signed Contributor Licensing Agreement for every code contribution to the core kernel, whether it is sent as a pull request, a patch, an issue or by mail, before it is merged; users' own custom extensions are not affected. The README, the pull request template (new,.github/PULL_REQUEST_TEMPLATE.md, with a CLA checkbox), the contribution and pull request pages of the help, the glossaries and the FAQs link to it. - The contributing guidelines state plainly that a signed CLA gives control of a contribution to 7x: the section is now called "Copyright and control of your contribution (CLA)" and opens with what a contributor gives up (a perpetual, worldwide, irrevocable, royalty-free license, patents included, under which 7x may use, change, sublicense, relicense and distribute the contribution without asking or paying, and which cannot be withdrawn), with only a short note that formally the CLA is a license (section 4 of the agreement). The FAQ answers "Do I give up control of my code?" with yes. Every page that mentions the CLA says the same.
- The "Code Reviews" section of
CONTRIBUTING.mdwarns that 7x reviews in many more rounds than usual, sometimes hundreds of revisions, and says how to get through faster (small focused pull requests, the coding standards, tests, answers in the pull request, a branch rebased onmain). The pull request template and the pages that walk through a pull request link to it. Its link to the missingCODE_OF_CONDUCT.mdnow points at the project's conduct page. - Contributing guidelines state that kernel contributions are a copyright assignment to 7x: by signing the CLA and
submitting a contribution to the core kernel, the contributor assigns (transfers) its copyright to 7x, which
becomes the copyright owner and may use, change, relicense and distribute it in any way; the contribution cannot be
withdrawn and no conditions can be set. Users' own custom extensions are not affected. The section is called
"Copyright assignment (CLA)" again (
#copyright-assignment-cla; the anchor of the previous name still works), the FAQ answers "Do I give away my copyright?" with yes, and every page that mentions the CLA says the same. The agreement form on the CLA page is being updated to the assignment. - Contributors squash and rebase their pull requests: 7x requires a pull request to be squashed into logical
commits (usually one) and rebased onto
mainwhile it is open (git fetch origin,git rebase -i origin/main,git push --force-with-leaseto the pull request branch only), so the merge is clean.CONTRIBUTING.md("Code Reviews") states it with the commands, the pull request template has a checkbox for it, and the pages that said a contributor's commits are kept as written with fixes on top now say thatmain's history is never rewritten after the merge and later fixes come as new pull requests (Continuous integration, the source control standard, the pull request pages and the press, partner, advocate and leadership pages). CONTRIBUTING.mdis titled "Exponential Contributors Guidelines".
20 October 2026: help notices and client help_ids checked, the help standards, the help classes as a package
The help system plan and the help style guide (pull request #288) compared with what the help does, item by item: help maintained as content, compared with the specifications.
Added
./console exp:help:content checkreports the notice levels of the style guide (IEC/IEEE 82079-1): an Attention or Data protection notice outside the step of a task (notice.position), after the action in its step (notice.before-action), a second notice in one step (notice.per-step), a notice of more than three sentences (notice.too-long).- The same command reports the help_ids that clients call and no topic has (
client.help-id-unknown), or whose topic is not Released (client.help-id-not-released), fromexphelp.ini [ContentSettings] ContextHelpIds[]and the newClientHelpIds[]. ./console exp:help:content package -s <siteaccess> [--output=<dir>] [--version=]: the eight help classes as the content class packageexp_help_classes(directory and.ezpkg);verify-package --file=<.ezpkg or directory>compares a package with the classes of the release without a database (expHelpContentPackage). Build and handoff: the help classes as a package.- Help standards: what the help takes from DITA 1.3, IEC/IEEE 82079-1:2019, ISO/IEC/IEEE 26514:2022, ISO 3864-2:2016, EPUB Accessibility 1.1 and WCAG 2.2 AA, what it does differently and how far it goes (no conformance claimed). Pull request #288 announced the page; it was not in the merge.
Updated
- The help viewer shows
> **Attention:**,> **Data protection:**and:::data-protectionas callouts with that title; Data protection has its own icon.
Tests
expHelpContentTest(notices, client help_ids),expHelpMarkdownRendererTest(the notice callouts),expHelpContentPackageTest(the package's definition, a package and an.ezpkgcompared with the schema), no database.
20 October 2026: the documentation of the active extensions in the help center
Added
- The help center shows the documentation of every active extension, found when it looks
(
exphelp.ini [IndexSettings] ActiveExtensionDocs=enabled): the files ofextension/<name>/doc/that the extension's repository tracks (or itsshare/filelist.md5lists) and its README, CHANGELOG, LICENSE, INSTALL, USAGE and COPYING at the top of the extension (ExtensionTopFiles[]), nothing else: no PHP or other source file, no settings, no untracked file, nothing of an inactive extension, and a text file without a suffix that starts like a program is left out. The pages have the idsext/<name>/<path>, the type "Extension documentation" and the category and navigation folder Extensions, and are read, searched and printed like every help page. exphelp/extensions: the extensions with documentation, andexphelp/extensions/<name>, one extension's index with its pages by folder and a search limited to them. The search takes(extension)/<name>(view parameter, form and JSON). The contents page links to the index; the breadcrumb of an extension's page goes through it.expHelpDocumentIndex::extensions(),expHelpAllowlist::extensions()andisExtensionTopFile().
Updated
ExtensionDocDirs[]now adds extensions whose documentation is shown whether they are active or not. An extension's page has no "Edit on GitHub" link (EditBaseURLnames the installation's repository).exphelp.ini [ContentSettings]carries a commentedClientHelpIds[]example.
Tests
expHelpExtensionDocsTest(no database): a fixture installation with extensions in their own repositories and in the installation's, active and inactive; the pages shown, the files refused (source, settings, scripts without a suffix, untracked, inactive, traversal), ids both ways, the overview, the per-extension list, the search filter, the viewer's breadcrumb and edit link.expHelpSearchViewParametersTestcovers(extension).
Operator note
- After the deploy, run
./console exp:help index --rebuildonce. An extension's documentation shows only while it is active and only when its files are tracked by its repository or listed in itsshare/filelist.md5. expai 0.0.1.14 moves its own help pages here (expai/helpredirects); release it after this.
20 October 2026: the user manual, one question per page
The user manual of the administration with the Admin 4 design: more than fifty pages, each answering one question with who may do it (the role or policy), numbered steps, what can go wrong and the deeper pages to read next.
Added
- The admin tabs: the Store dashboard, order statuses, archiving orders, VAT, discounts, currencies and the products overview; what the Design tab holds; the Setup overview and why a tile is missing; who changed what in the Audit console; My account, drafts, pending items and notification settings; switching the administration design; turning on two-step sign-in, and what an administrator does when a user lost their phone.
- The daily tasks: translating and finding what is not translated, sending for approval and approving, following content, finding content, the trash, the order of sub items, changing the class and the owner, tags and the tag tree, images, links, tables and embeds in the Online Editor, the SEO title and description, the sitemap, form submissions, object states, a page with Layouts, a newsletter and the AI assistant (which points to the extension's own manual).
- The tasks of administrators and site managers: edit rights on one section, disabling and unlocking accounts, which cache to clear, broken links, URL aliases and wildcards, adding a content language, the log viewer, the content checks, extensions and system information, RSS export and import, and API access for another application.
- Twenty-five Admin 4 screenshots (light mode, WebP), taken with a throwaway account; names, addresses and keys are replaced by examples.
- The help category User manual (
manual,exphelp.ini [CategorySettings], translated), first after Getting started; the folder's audience is editors, administrators and users. - Pages that already answered a question of the list are linked from the manual's index instead of written again (adding a product, changing your own and another user's password).
20 October 2026: help maintained as content leaves Exponential as DITA 1.3
Guide: help maintained as content.
Added
./console exp:help:content dita -s <siteaccess> [--manual=<remote_id>] [--output=<dir>] [--dry-run] [--json](alsoexport --format=dita, the plan's name): the Released manuals as DITA 1.3 bookmaps with a DITAVAL file of their audiences, the Released topics asconcept,task,referenceandtroubleshootingtopics named by their help_id, the chapters, the figures, the glossary asglossentrytopics, and a map of the whole topic pool. Only changed files are written and files of an earlier export that are gone are removed, so the directory can be a git repository of the help's history; a second run prints "No change.". The command checks the result: well-formed XML, the DITA 1.3 DOCTYPE of each root element, unique topic ids, and every reference resolves (exit 1 otherwise).- Download DITA on a manual's page in the help and Download all as DITA on the list of manuals
(
exphelp/manual[/<remote_id>]?format=dita): the same export of what the user may read, as a zip file, checked before it is sent. expHelpContentDita(the mapping, the check, writing and zip) andexpHelpDitaMarkup(the help's Markdown HTML as DITA elements: bold UI labels asuicontrol, callouts as typed notes, tables assimpletable, one numbered list of steps asstepswithcmdandinfo).
Tests
expHelpContentDitaTest(no database, HCD-01 to HCD-11): a fixed set of topics, a chapter, a manual, a figure and glossary entries through the export; the files, DOCTYPEs and references, each topic type, ids and filter attributes, Released only, the bookmap and DITAVAL, informal steps, figures, the glossary and pool map, one manual, the check finding broken files, an idempotent write that keeps a.gitdirectory, and the markup. The export of the test set and of the shipped seed topics is valid against the OASIS DITA 1.3 DTDs (xmllint --validwith the DITA-OT catalog).
20 October 2026: the e-books of help manuals are stored as content and rebuilt on release
Guide: help maintained as content.
Added
./console exp:help:content ebooks -s <siteaccess> [--manual=<remote_id>] [--dry-run] [--json]: the EPUB and PDF of every Released manual stored as an object of the class file below E-books (help-ebooks), remote_idhelp-ebook-<manual remote_id>-<format>, state Released, sectionhelp_publicfor a public manual andhelp_internalotherwise (on the object only), so the help's policies decide who may download it. A new build is a new version of the object; the same book changes nothing ("No change." on a second run).- A release rebuilds the books: setting an object of the help to Released builds the stored e-books of every
Released manual that contains it again (
expHelpContentEbooks::onStateAssign(), called fromeZContentOperationCollection::updateObjectState();exphelp.ini [ContentSettings] EbooksOnRelease, defaultenabled). A book that cannot be made is logged; the release stands. - The book of a public manual carries only public objects, whoever releases (
expHelpContentEditionProvider). - The manual's page in the help lists its stored e-books (file, version, size, date) for who may read them.
Updated
- The entries of an EPUB are dated by the book's content (the newest manual or topic change), and so is its
dcterms:modified: the same content gives the same file.
Tests
expHelpContentEbooksTest(no database, HCB-01 to HCB-05): the public and internal editions, the manuals a release rebuilds, remote_ids, the EPUB dated by its content, and the release hook. In a sandbox copy of a site: a topic set to Review leaves the books, set to Released stores a new version of each.
20 October 2026: help topics overdue for review on the help's dashboard
Guide: help maintained as content.
Added
- Help topics overdue for review on the help's start page (
exphelp/dashboard): the topics whose review date has passed, or that have been in Review longer thanexphelp.ini [ContentSettings] ReviewMaxDays(default 14) days without a change, the longest overdue first, with their state, the reason and links to the topic and its object. Shown only to who can act on a topic (edit it, or assign it another state ofhelp_status): help editors see the topics in Review, release managers also the Released ones; nothing is shown while no topic is overdue. At mostReviewListLimit(default 10) are listed. Shared admin components only, light and dark mode. ./console exp:help:content review -s <siteaccess> [--json]: every overdue topic.expHelpContentReview(the rule, without a database inoverdue()).
Tests
expHelpContentReviewTest(no database, HCR-01 to HCR-05): a past review date in every state, too long in Review and the setting, a topic with both reasons once, the settings, and the dashboard part (shared components, no style, identical in admin and admin4). In a sandbox copy of a site, in a browser (light 1440 at scale 1, dark 960 at scale 2): an administrator sees both overdue topics in a dark card in dark mode, a help editor only the one in Review, a help editor with Help release both; no horizontal overflow, no serious accessibility finding.
20 October 2026: help maintained as content in every language of the help
Updated
- The interface strings of the DITA download, the stored e-books, the overdue-review list and the headings of task and
troubleshooting topics (Before you begin, Steps, Result, What to do next, Condition, Cause, Remedy) are in the
context
kernel/exphelpof all 24 translation catalogues that have it: translated in 22 languages, the source ineng-US, unfinished inuntranslated. - The overdue-review list names a topic's state with the name of its
help_statusstate (in the interface language where the site wrote one), not with an interface string: "Released" is already a release date in several of the help's translations.
Tests
expHelpContentTranslationsTest(no database, HCL-01 and HCL-02): every string in each catalogue with the context, translated, placeholders kept; the templates and topic headings use exactly these strings.
20 October 2026: the help's project files without .github, tests and extensions, at the top of the page tree
Updated
- The help's project files leave out
.github/(the pull request template), every test README and everything belowextension/(the extensions' documentation is the category Extensions).[ProjectFiles] Include[]no longer names.github/*.md,.github/**/*.md,tests/README.md,tests/**/README.mdandextension/*/tests/**/README.md, and hidden files and directories,tests/directories anywhere andextension/are now rules that no setting can change. - The project files are pages at the top of the page tree of
/exphelp/contentsand of every help page, next to the main folders, instead of a "Project files" folder; a project file's breadcrumb is Help > its title, and a title that is only "README" gets its directory in front. The category Project files stays for the search and the dashboard.
Tests
expHelpProjectFilesTest: PF-02, PF-05 and the new PF-14 (.github/, tests andextension/are never listed, even whenInclude[]names them), PF-09 (no folder of the project files, titles, breadcrumb), PF-10 (this installation lists none of them).
20 October 2026: the order of the top-level pages of the help's page tree
Added
exphelp.ini [TreeSettings] PageOrder[<page id>]=<integer>sets the place of a page in the help's page tree, the folder lists and the category lists, and wins over the page's front matterorder; the project files and plain text pages, which cannot carry front matter, can be placed too. The key is the page id, the same in every language. Exponential ships the order of the top level: the README, installing, languages, branch history, release history, then contributing, copyright, the documentation license, the GFDL, the GPL, the security policy, and the glossary last.branch-overview.mdandREADME-LICENSE.mdno longer carry anorderof their own. Guide: help system, "Order of the pages".
Tests
expHelpPageOrderTest(no database, HO-01 to HO-04): an entry wins over the front matter order and places a page without front matter, other pages keep theirs, invalid entries are left out, and the shippedexphelp.inilists the same order as the built-in default, for pages that exist.
20 October 2026: the documentation's language directories are named by the locale codes
A language directory of the documentation is now the Exponential locale code of its language in lower case, so the interface, the content languages and the documentation name a language the same way and a new translation needs no mapping. Upgrade notes: Translated documentation, "Upgrading a site that links the old directories".
Renamed
en/us/toeng-us/(the original,eng-US) andde/toger-de/(German,ger-DE) belowdoc/, with.paths.jsonand.dirnames.json, as pure renames (git mv) in a commit of their own;git log --followreaches the history before it.doc/changelogs/anddoc/README.mdstay where they are. Page ids do not change, so help addresses,[TreeSettings] PageOrder[], context help and bookmarks keep working.- The per-language help caches:
var/<site>/cache/exphelp/lang-<directory>/(lang-ger-de/, waslang-de/); the formats were raised (the index to 4, the search index to 5, the viewer's rendered pages to 5), so the caches are built again on their own.
Added
exphelp.ini [DocLanguages] Legacy[](Legacy[en/us]=eng-us,Legacy[de]=ger-de, also built in): the old directory names of 6.0.14 and before, for old addresses only; no file is read from an old directory (expHelpLanguage::resolveDirectory(),splitLegacy()).
Updated
expHelpLanguageandexphelp.ini [DocLanguages]: a locale reads an alias ofMap[](now aliases only:eng-GBtoeng-us,ger-ATandger-CHtoger-de), else the directory of its own lowercased code whenDirectories[]lists it (ger-DEreadsger-de), both tried with and without the variant, else the first directory of its language (any otherger-*readsger-de), elseDefault=eng-us;Directories[]listseng-usandger-de,Names[]is keyed by the new names.- The help keeps every old address working:
/exphelp/viewer/en/us/<id>,/exphelp/viewer/doc/en/us/<id>and/exphelp/viewer/de/<translated path>are redirected to the page id, like the new/exphelp/viewer/doc/eng-us/<id>and/exphelp/viewer/ger-de/<translated path>;?lang=takes the directory name (eng-us,ger-de) or a locale, and still acceptsen-usandde. - Every reference to the old directories in the repository: kernel code, templates, settings (
exphelp.inipath rules and theDoc=entries of the other INI files), translations, tests and fixtures, command line scripts, CI, the root files and the links of the documentation itself, including GitHub links intodoc/eng-us/. The released changelogs and the history pages keep the paths of their time. - Translated documentation: the naming rule, the alias settings
and an upgrade table for sites, overrides and scripts written for the old
en/us/andde/directories; help system and Reading the help in your language follow it.
Tests
expHelpTranslatedDocumentationTest: TD-01 the tracked files belowdoc/are indoc/eng-us/,doc/ger-de/,doc/changelogs/ordoc/README.md; TD-02 no tracked text file names the old directories belowdoc/outside the history; TD-03 and TD-04 the lowercased locale, the aliases and the fallbacks; TD-11?lang=with the new and the old keys. The redirects of the old addresses are tested with the viewer.
Operator note
- After the deploy, remove the old copies (
en/us/andde/belowdoc/) if the deploy copied files without deleting (the help does not read them), deploy with--kernel, and run./console exp:help index --rebuildso every language's index and search are built at once; the oldlang-de/cache directory can be removed.
20 October 2026: the system card: exp:expfetch, exp:info and exp:about
Guide: The system card. For hosts: The system card for hosts; for extension developers: Facts, themes and logos for the system card; for partners: Verifying a customer installation; for demos: The system card in demos; for trainers: Lesson 5.5. Reference: exp:expfetch, syscard.ini.
Added
./console exp:expfetch: the system card. The orange Exponential "e" in block art (three sizes, and ASCII drawings) next to the facts of the installation, grouped in sections: Exponential (version, line, git branch and commit of a checkout), System (operating system, kernel, host, uptime, load, CPU, memory, disk, shell, terminal), PHP (version and SAPI, OPcache, JIT, the extensions that matter,memory_limit), Engine (Exponential Velocity: version, running, live and configured workers, ports and TLS, envvars, demo mode; Apache, nginx, Caddy, FrankenPHP, PHP-FPM masters and pool workers), Database (type, version, host only) and Site (siteaccesses, extensions, demo mode, debug, the mail transport's type, caches, help, cron, Composer). A Hints block closes it, worst first, and the color bars (the terminal's 16 colors and a gradient bar)../console exp:info: the card plus an inventory: every active extension with its version (extension.xml,ezinfo.php,composer.json), each siteaccess with its design, language and SiteURL host and the URL mode, the settings of an allowlist (syscard.ini [Inventory] Settings[]), the cronjob parts and their scripts, the expqueue service and workers, the size of the cache directory and of its largest caches, the help coverage per language, every local disk, the Velocity instances and worker model, and the PHP-FPM pools../console exp:about: the card plus the project: what Exponential is, the Exponential Foundation and 7x, Exponential Velocity as part of 6.0.15, the license (GPL 2.0 or later, documentation GFDL 1.3 or later), links, how to contribute (CONTRIBUTING.md, the CLA), "Become an Exponential Partner" and the credits. The text isshare/syscard/about.txt, written from the documentation, each block naming its source.- Options of all three:
--json(sections, hints, how each provider ran),--plain(no logo, no colors, for pasting),--no-logo,--section=,--no-color,--color=auto|truecolor|256|16|none,--theme=,--width=,--ascii,--no-hints,--no-bars,--watch[=seconds]with--count=(refreshes in place liketop: load, memory, disk and the Velocity workers; Ctrl-C stops),--animate(a reveal of the logo and its gradient in under a second, only on a color terminal),--mode=card|info|about,--list-providers,--list-themes,--completion=bash|zsh. - The layout follows the terminal: the big "e" with the rainbow wordmark EXPONENTIAL from 82 columns, then the big,
small and mini "e", and stacked below 41 columns. Colors only on a terminal and never with
NO_COLOR; the depth is detected (truecolor fromCOLORTERM, 256 fromTERM, else 16) with the nearest color at each depth. Themes:rainbow(default),orange,mono,high-contrastandlight(for a light background), each with<Role>16fallbacks for 16-color terminals. - Health hints: load above twice the cores, memory or a disk nearly full, a PHP branch near or past the end of its
security support (
syscard.ini [Php] SecuritySupportEnds[]), OPcache off, a recommended PHP extension missing,memory_limitbelow 256M, no database connection, demo mode on, debug output for everyone, the view cache off, a stale help index, no recent cronjob run, Composer packages that disagree (ezupdate's inventory, local files only, cached), a queue without workers. - The extension API: providers implement
expSysCardProvider(most extendexpSysCardBaseProvider), read through anexpSysCardContext(a fake one for tests) and returnexpSysCardFactrows with a severity and an optional hint.settings/syscard.ini [Providers] ProviderClasses[]lists them in card order; an extension adds its own in itssyscard.ini.append.php, andEnabled[<id>],Priority[<id>]andBudget[<id>]switch them off and tune them. Themes are[Theme-<name>]blocks; logos are text files inshare/syscard/logos/and in the directories of[Logo] Directories[]. - Time budgets: each provider has one, and all together
[Runtime] TotalBudget; with[Runtime] Isolation=enabledeach provider runs in a child process that is killed when its time is up, so a hanging database or disk showsn/a (timeout)and never hangs the card. - Classes:
expSysCardFact,expSysCardContext,expSysCardProvider,expSysCardBaseProvider,expSysCardRegistry,expSysCardColor,expSysCardTheme,expSysCardLogo,expSysCardRendererand sixteen providers inkernel/classes/syscard/; the command inkernel/private/classes/commands/syscard.php, its entry pointbin/php/expfetch.php;settings/syscard.ini;share/syscard/. - No secrets: the card never prints a password, key, token, database user, database name or full DSN, nor the values
of Velocity's envvars. Every fact passes
expSysCardRegistry::safe(): a fact named like a secret is dropped and a password, token or key inside a value is masked, whatever a provider (also an extension's) returns. - Naming: only the command is called "expfetch", after the tools it is modeled on. Its code is the "system card"
(
expSysCard*,syscard.ini), because "fetch" already names the template fetch functions, the fetch definitions of modules, function collections and the fetch methods of persistent objects.
Updated
- The console (
bin/php/console) passes the name it was called by, an alias too, to the command in the environment variableEXP_CONSOLE_COMMAND, so a command can show more under an alias (exp:info,exp:about). - The help: the canonical page, pages for hosts, extension developers, trainers, advocates and press, supporters and
partners, the installation and upgrade chapters (run
exp:expfetchafter installing or upgrading), the glossary and the FAQs of supporters, partners and advocates.
Tests
tests/tests/kernel/classes/syscard/(no database): each provider with a fake context; the registry (the order ofProviderClasses[], a provider added by an extension,Enabled[]=disabled, priorities and budgets, a provider that hangs shown as a timeout); the renderer at 40, 80 and 120 columns, at each color depth and withNO_COLOR;--jsonis valid JSON and carries no secret; the alias set of the command; and a check that the new code has no "fetch" outside the command name.
20 October 2026: Exponential Velocity is required again and runs on PHP 8.0
Updated
- Updated:
composer.jsonrequires Exponential Velocity (se7enxweb/exponential-velocity) at~0.0.4.54again, instead of only suggesting it. From releasev0.0.4.54(2026-10-20) the engine declares"php": ">=8.0"(it was>=8.1) and runs on PHP 8.0 to 8.5 with nothing extra, so acomposer installon PHP 8.0, the stock PHP of Red Hat Enterprise Linux 9, installs the engine too. The el9 rpm depends onphp-cli >= 8.0and installs without enabling a module stream; on RHEL 8, enablephp:8.0and use the phar or Composer. On PHP 8.0, AVIF image output (needs 8.1) and the dashboard's per-request memory peak (needs 8.2) are not available. Engine changelog: CHANGELOG.md; details: PHP 8.0 support and Velocity engines. - The README, the installation guide (chapters 1, 2, 3, 8, 11, 12, 14 and 15), the PHP 8.0 and PHP 8 pages, the Velocity feature pages and the help pages for hosts, businesses, governments, partners, the press and trainers state PHP 8.0 as Velocity's minimum, with the RHEL notes and what differs on 8.0.
20 October 2026: the administration views take their options as view parameters only
As the help center does, every administration view and endpoint below takes its options as view parameters,
/(name)/value, and never as GET query parameters: a ?name= in the address is not read, there is no fallback and
no redirect of a GET. Filter forms without scripts are sent with POST (with the form token) and answered with a 303
to the canonical /(name)/value address. Each module.php declares the parameters as unordered_params.
Added
expViewParameters(kernel/classes/expviewparameters.php): reads a view parameter off$Params, builds the/(name)/valuepart of an address, and answers a posted filter form with a 303 to it.
Updated
- Updated: content/job (
(json)/1,(log)/1), content/jobs ((state),(type),(user),(sort),(order),(offset)), content/subitemsexport ((columns),(sort),(order)), the(q)search of content/bookmark, content/draft, content/urltranslator and content/urlwildcards (the Try form of the wildcards posts its address); contentcheck storage, storageaction, brokenrelations, unreferenced and fixfiles; contentfinder/search ((text),(type)); settings/view ((q),(scope),(changed),(compare)); role/list, user/unactivated, url/list, search/stats, oauthadmin/keys, pdf/list, mailpreferences/admin ((export)/csv|json), shop/orderreceipt ((download)/1); setup cronjobsstream, staticcachestream, logsfeed, languages, sitedata, cronjobs (the Ajax flag is posted), logs (the old?file=,?key=andarchive=links are no longer read); visual/templatelist ((filter)); package list, compare and view (their filter forms are posted). Links, pagers, the scripts' fetch and stream addresses and the templates of every admin design build the/(name)/valueform. - The guides that showed a
?name=option (bookmarks, drafts, URL aliases, content jobs, subitems table options, PDF exports, roles, mail preferences, the log viewer, content checks, the content finder, the settings page).
Not changed: the classic content/search and content/advancedsearch (a long-standing public API), the call options of ezjscore and the passthrough of switchlanguage.
Tests
AdminViewParametersOnlyTest(no database): VP-01 the converted views read no$_GET,getVariable()or$http->variable(); VP-02 theirmodule.phpdeclares the options; VP-03 and VP-04 their templates and scripts build no?name=option and have no GET form.expViewParametersTestfor the helper; the bookmark, draft, content check, content finder, PDF export and package tests follow the new addresses.
Operator note
- Deploy with
--kernel(a new kernel class). Bookmarked addresses with?q=and the like open the unfiltered page.
20 October 2026: one reversible encoding for the values of view parameters
A view parameter, /(name)/value, cannot carry a / as it is: Apache refuses %2F, and the kernel decodes the
address (twice unless DecodeOnce is enabled) before it splits it. The views handled that in three ways: most of
them turned /, ( and ) into spaces (a link list or URL alias search for example.com/a lost its slash, and a
+ became a space), the content finder and the settings page wrote ~ and a base64url, the template list and the
log viewer ~XX. They now share one encoding. Guide:
View parameters: how a value travels in an address.
Updated
- Updated:
expViewParameters(kernel/classes/expviewparameters.php) writes a value withencode():/,(,),~,%,+,\,?,#and control characters as~and two hexadecimal digits (~2Ffor/), a value of only.or..as~2Eeach, everything else readable (letters, digits,, : @ = ! *as they are, a space%20).decode()is the inverse;get()andtext()return the value decoded,raw()as the kernel handed it over,escape()what the kernel hands over for a value.suffix()encodes, andclean()no longer turns/,(and)into spaces. - Updated: every converted view writes and reads its values that way: the searches of url/list, content/urltranslator,
content/urlwildcards, search/stats, content/bookmark, content/draft, role/list, user/unactivated (its search keeps
/and brackets now), oauthadmin/keys, mailpreferences/admin and pdf/list; contentfinder/search(text)and settings/view(q); visual/templatelist(filter); the log viewer's permalinks and feed (expLogPermalink::encode()anddecode()call it); the search of package/list, package/compare and package/view (written once instead of twice); the content check options. - Updated: templates write a value with the new template operator
view_parameter(expViewParameterOperator,kernel/common/expviewparameteroperator.php): the setting links of the settings page and the link back to the content finder. Scripts write it withwindow.a4ViewParameter.encode()(and read it withdecode()), added toa4_url.jsof the admin and admin4 designs: the log viewer (page/setup_logs.js, its permalinks, feed, content finder and audit links), the subitems export and the static cache stream. - Updated: links written before keep working: contentfinder/search and settings/view still read the
~+ base64url form (Contentfinder\Search::textFromParameter(), when the value is not one the new encoding writes), the~XXform of the log viewer and the template list (a space~20) is read by the samedecode(), and the package views still read their search encoded twice (demo%2520content). - Updated: the guides of URLs and aliases (section 9, English and German), the settings page, the content finder, the
log viewer (English and German) and finding an error by its reference; the kernel operators page and the operator
index list
view_parameter; the generated module view and operator pages.
Tests
expViewParametersTest: the encoding character by character, its inverse and the~XXlinks of before, round trips through the kernel's decoding once and twice for/,(,),~, spaces,%,+, unicode, emoji, dots, control characters and quotes, how the kernel splits an encoded address, and the template operator.AdminViewParametersOnlyTestVP-05 (no hand-written encoding in the converted views, templates and scripts: no base64url, no doublerawurlencode(), no|urlencodeor bareencodeURIComponent()after/(name)/) and VP-06 (a4_url.jsthe same in admin and admin4; in node,a4ViewParameter.encode()writes exactly whatexpViewParameters::encode()writes anddecode()gives the value back).- The content finder (CF-08, both forms), log viewer, package, PDF export, bookmark and unactivated users tests follow the new addresses.
Operator note
- Deploy with
--kernel(a new kernel class,expViewParameterOperator, and a new template operator) and--packer(a4_url.jsand the admin scripts changed). Bookmarked links keep working.
20 October 2026: the help shows external images from a local copy
Updated
- Updated: the help shows the external images of a page (README.md's screenshots on
github.com/user-attachments) as images again, without widening the administration's Content Security Policy:./console exp:help indexstores a copy of everyhttps://image a page references invar/<site>/cache/exphelp/images/<sha256 of the address>.<suffix>(expHelpRemoteImages), and the new viewexphelp/image/(id)/<sha256>(policyexphelp/read, the id a view parameter, never GET) serves it with its content type,nosniffand a long private cache lifetime, soimg-src 'self'allows it. Fetching follows redirects over https only, never to a private or reserved address, stops at 5 MB and 15 seconds, and keeps only PNG, JPEG, GIF and WebP whose bytes match the type (no SVG). An image without a copy keeps the labeled link of before. Thewidth,heightandaltof an<img>tag are kept as attributes.exp:help index --no-imagesskips the fetching; the rendered pages are cached again (RENDER_VERSION6, and the key follows the image directory). Guide: help system.
Tests
expHelpRemoteImagesTest(no database, a fake fetcher): redirects followed and the copy stored readable by the other server user; plain http, a redirect to http orfile:, another port, too many redirects, a status other than 200, a body over the limit, SVG and bytes of another type refused; the references of a page; the renderer's<img>with the local address and the size and alt of the tag; the view refuses unknown ids, upper case, path tricks, a further path and a symbolic link.
Operator note
- After the deploy (
--kernel: a new class and a new view), run./console exp:help indexas the user the help cache belongs to, so the copies are made; the server needs to reachgithub.comover https once for it.
20 October 2026: Exponential Velocity is part of Exponential
As of 6.0.15 the Velocity Internet / Web Application Server is part of Exponential. This marks a turning point for the project, a cornerstone and a capstone at once: a new level of application system integration, which provides even greater features out of the box in the core product, by default. Exponential is now a whole working unit, not just a framework: a rapid application development (RAD) stack for Internet applications that is a content management system, a digital experience platform and an e-commerce system in one installation, from the server to the rendered page.
The story, for every reader: Velocity is part of Exponential 6.0.15. Every feature page: Exponential 6.0 features.
Updated
composer.jsonrequiresse7enxweb/exponential-velocity ~0.0.4.54; it moved fromsuggesttorequire. Acomposer installon any supported PHP installs the server with the rest of the product.- Velocity 0.0.4.54 runs on PHP 8.0 and later (it needed 8.1 before), so the kernel and the server now share one
minimum: PHP 8.0, the stock PHP of Red Hat Enterprise Linux 9 included. It needs
ext-sockets;pcntlandopensslare recommended for the worker pool and HTTPS. - Velocity runs next to Apache with PHP-FPM, nginx and FrankenPHP. Its own server (
[ServerSettings] Engine=qbix) is the recommended engine for every stage; FrankenPHP is the production-ready alternative; Apache and nginx keep working as before. The shippedsettings/velocity.inistill starts PHP's built-in server (Engine=php) until a site chooses. - The commands:
./console exp:velocity deploy(autoloads, settings, caches, PHP-FPM and Velocity in one ordered run, stopping at the first failure),start,restartandcache clear. - The documentation tells it throughout: the README, the installation chapters (requirements, getting the code, serving the site, upgrading, troubleshooting), the Velocity feature pages, the BC notes on the engines and on PHP 8.0, the help start page, the project FAQ, the press kit and the Velocity stories, and the start pages for hosting companies, extension developers and decision makers. Every page that said Velocity needs PHP 8.1, or that it is only suggested, now says it runs on 8.0 and is required.
Added
- The help page Velocity is part of Exponential 6.0.15, linked from the help start page and from the new features index Exponential 6.0 features, which lists every 6.0 feature page by subject.
Upgrading
- Run
composer installas usual; it installsse7enxweb/exponential-velocity. Nothing switches engines on its own: a site served by Apache, nginx or FrankenPHP is served the same way after the upgrade.
20 October 2026: Velocity's opcode cache has room for the help center
Updated
- Updated:
settings/velocity.inigives the Velocity server process 512 MB of opcode cache (opcache.memory_consumption, was 256) and 64 MB for interned strings (opcache.interned_strings_buffer, was 32). The help center's index files are 18 to 24 MB per language, and with the old values the interned strings buffer was full after a few help requests. The memory is shared by the workers of an instance and reserved, not used, until it fills. The PHPSettings reference, the FrankenPHP and opcode cache pages and the hosting page on PHP settings show the new values.
20 October 2026: the help center takes its options as view parameters only
Updated
- Updated: The help center takes every option as a view parameter (
/(name)/value), never as a GET query parameter, with no fallback and no redirect:/(lang)/<key>instead of?lang=(the original of a translated page),/(format)/jsoninstead of?format=json(the instant search),/(format)/epub|pdf|ditainstead of?format=(the manuals' downloads) and/(tracker)/<key>instead of?tracker=(the error report); a page id is no longer read from?p=.kernel/exphelp/module.phpdeclares them asunordered_params, the templates and the help script build only view parameter addresses, and the help search forms (the dashboard, the pages, the header menus, the audience pages, the extension report) are sent with POST, which the search view redirects (303) to its canonical address.expHelp::url()appends its options as view parameters.
Tests
AdminHelpViewParametersOnlyTest: no help template, header help template or help script builds?lang=,?format=,?q=,?p=or?tracker=, the help views read no$_GET, module.php declares the parameters and no help form is a GET;AdminHelpScriptTest,expHelpLinksTestandexpHelpSearchViewParametersTestfollow the view parameter addresses.
20 October 2026: admin4 serving, style sheets, address checks and the admin4l zones
Merged pull requests #291, #292, #293 and #294.
Added
- The rewrite templates
.htaccess_rootand.htaccess_root_staticserve the icon themes of extensions (extension/<name>/icons/,icon.ini [ExtensionSettings] IconExtensions[]) directly, images with a single extension only (#291). The same rule is in every web server example (doc/eng-us/examples/htaccess,ezpublish.conf, Apache, LiteSpeed, nginx, Caddy, FrankenPHP, IIS), the install and hosting chapters that list the rules, and the static path list of Velocity, FrankenPHP and the PHP built-in server (expVelocity::STATIC_PATHS). - admin4 frames the API access page (
apikey/list) like its other pages (#291), and copying several items at once shows its browse page description (content/browse_copy_subtrees.tpl, in design/admin and design/admin4). javascript/a4_url.js(design/admin and design/admin4):window.a4SafeURL(), the one check the admin scripts make before they follow an address that came from data; the page heads of admin, admin3 and admin4 load it beforeBackendJavaScriptList, and the log and content job pages load it themselves (#293).
Updated
- The sorting controls and the VAT type removal title escape their text (#291).
- The context menu, the select boxes that navigate on change, the log viewer and the content job page follow only addresses of this site; the CSRF token of the legacy preference posts is URL-encoded, and an error text in the content tree is shown as text, never parsed as markup (#293).
- The admin4l zone template draws only
admin_*block definitions (others are skipped and logged), and the popup menu, debug area and overlay only from the footer zone, so they appear once; the page reads the safety switch through the fetch functionexplayouts/admin_layouts_enabled(#294).
Removed
- The view scripts
kernel/class/delete.php,kernel/content/error.phpandkernel/workflow/list.php, for which no module defines a view, and design/admin's unusedpage_menuhead*.tpl(#291). - admin4 style rules that nothing uses or that a later rule overrides completely, and the old YUI data table and
paginator rules of
theme/admin_datatable.css(#292).
Upgrading
- An installed
.htaccessor a server configuration of your own is not rewritten: copy the rule into it, before the front controller rule, if an extension ships icons..htaccess:RewriteRule ^extension/[^/]+/icons/[A-Za-z0-9_/-]+\.(svg|png|gif)$ - [L]; a virtual host and the other servers: Web server rules. Velocity and the generated FrankenPHP configuration take it with their next restart (exp:velocity deploy). - A design that overrides
page_head_script.tpland loadsBackendJavaScriptListmust loada4_url.jsbefore it ({ezscript_load( array( 'a4_url.js' ) )}); without it the context menu follows no links and says so in the browser console.
Tests
AdminExtensionIconsRewriteTest,expVelocityEnginesTest(the static list serves extension icons, nevera.php.svg),AdminKernelDesignTemplatesTest(every literaldesign:template of the kernel views resolves in an admin design),AdminTemplateFramesTest(#291);AdminBaseStylesheetsTest(#292);AdminContextMenuTestand the script tests of the context menu, node tabs and content job (#293);Admin4lReviewTestAL-R7 to AL-R9 (#294). All without a database.
21 October 2026: CORS for the REST API, PKCE and state for OAuth
Added
- CORS for everything
index_rest.phpserves (the content API v1 and v2, the OAuth token endpoint and the routes of every REST provider), off by default:rest.ini [CorsSettings]withCors, an allowlist of exact origins (AllowedOrigins[];*only without credentials,nullnever),AllowedMethods[],AllowedHeaders[],ExposedHeaders[],MaxAgeandAllowCredentials. A preflight is answered by the REST kernel without running the request (204, or 403 when the origin, method or a header is not allowed); every response carriesVary: Originwhile CORS is on, and only an allowed origin getsAccess-Control-*headers (expRestCors, CORS for the REST API). - PKCE (RFC 7636) for the OAuth authorization code flow:
oauth/authorizetakescode_challengeandcode_challenge_methodand stores them with the code, the token endpoint checkscode_verifierand uses the code up on a mismatch.S256is accepted,plainonly withAllowPlainPkce=enabled. A client stored without a secret is a public client: it sends noclient_secretand must use PKCE (RequirePkceForPublicClients, enabled by default);RequirePkceForConfidentialClients(disabled by default) requires it for every client (expOAuthPkce, PKCE and state).
Updated
oauth/authorizesends thestateof the request back unchanged with the code, the token and every error that goes to the client's end point, and carriesstateand the PKCE parameters through the authorization form.- An authorization code or refresh token is redeemed only by the client it was issued to.
- An unknown
response_typeis answered witherror=unsupported_response_type; an end point with a query string of its own keeps it when the parameters are added. - The REST and OAuth guides, the app creator and business help pages, the API standards page and the procurement
requirements describe CORS, PKCE and
state, in English and German.
Upgrading
- Run the
ezprest_authcodestatements ofupdate/database/<engine>/6.0/dbupdate-6.0.0-6.0.15.sql(two nullable columns). Kernel schemas andshare/db_schema.dbahave them for new installations.
Tests
expRestCorsTest,expOAuthPkceTest,expOAuthPkceSchemaTest, and new cases inezpOauthAuthorizeRedirectTest(state returned, challenge stored, plain and malformed challenges refused before the login, PKCE required for a public client). All without a database.
21 October 2026: the administration's site links of host-matched siteaccesses
Updated
- Fixed: the administration's site menu and "Open the site" link (
siteaccess_url(),siteaccess_menu(),ezpSiteAccessURL::root()) gave a host-matched siteaccess its URI as well when the page was on another host: withMatchOrder=host_uri;host;uri, the administration on its own host linked the site tohttps://example.com/site/, where the host map picks the site and/site/is read as a module, so the link answered 404.HostUriMatchMapItemswere not taken into account at all. The address is now the first one the matching rules (host_uri,host,uriinMatchOrderorder) lead to the siteaccess with nothing of the path left over: this host with its host and URI entry or its URI, its own[SiteSettings] SiteURL(host, port and path), a host the host map gives it, then one of its host and URI entries; the URI on this host remains the fallback. Siteaccesses matched by URI on the host the administration runs on keep the address they had. Documented in thesiteaccess_urloperator reference.
Tests
ezpSiteAccessURLTest: host-matched, host and URI-matched and URI-matched siteaccesses seen from the administration's own host,SiteURLused only when the rules lead there, and the address check itself. No database.
21 October 2026: the audit syslog sink under open_basedir
Updated
- Fixed: the audit syslog sink (
expAuditSyslogSink) calledfile_exists()on/run/systemd/journal/socketand/dev/log, so on a host withopen_basedir(PHP limited to the site folder and/tmp) every request wrote an open_basedir warning into the error log. The sink now readsopen_basedirfirst (parsed as PHP does, decided once per process,expAuditSyslogSink::pathAllowed()), never touches a socket outside it and sends through PHP'ssyslog()instead. Withoutopen_basedirnothing changes.
Tests
expAuditSinksTestSK-08: the open_basedir parsing, the fallback with an injected setting, and a PHP process started with a realopen_basedirthat raises no warning. No database.
21 October 2026: layout mappings by route and time, imports and helpers
Fixed
- Fixed: A layout mapping with a Route target matches on the site. The value is a module view (
content/search,user/*,content/viewfor every content page,*); the media design lays out its pages that are not content pages through such a mapping only (explayouts 1.4.42, sevenx_themes_media 1.5.75). - Fixed: A Time condition switches the layout at its start and end. The resolver kept the answer for
[ResolverSettings] CacheTTL(an hour) and the page cache kept the page as long again; the answer now expires at the condition's next start or end, and the page is kept out of the page caches when that comes within[HttpCacheSettings] MaxAge. A Query parameter condition is evaluated on every request, so/pageand/page?variant=bcan show different layouts (explayouts 1.4.42). - Fixed:
exp:layouts import --publish(and an import that writes rules) purges the page cache itself; the pages kept the old layout until the cache was cleared by hand (explayouts_core 1.0.10). - Fixed:
./console ext:explayouts:block_list,layout_info,collection_list,layout_copy,layout_list,rule_list,share_createand the other helpers of explayouts ended withClass "..." not found; they run on their own now (explayouts 1.4.42). - Fixed: A block an extension adds with
AvailableBlocks[]is offered wherever the extension is listed inActiveExtensions[]: explayouts' lists no longer start with a reset line (explayouts 1.4.42). The media design's Product highlight block appears in the block list for the first time.
Updated
- Updated: The "Two ways to build" help pages no longer describe these as limitations or workarounds (English and German).
Upgrading
- Update
se7enxweb/explayoutsto 1.4.42,se7enxweb/explayouts-coreto 1.0.10 andse7enxweb/sevenx-themes-mediato 1.5.75. An installation that relied on explayouts' reset line to drop a block an extension adds writes the reset insettings/override/explayouts.ini.append.phpinstead.
Tests
- explayouts:
tests/explayoutsresolvertest.php(RS-04, RS-08 to RS-10),tests/explayoutsbinhelperstest.php,tests/explayoutssettingslistresettest.php; explayouts_core:tests/TransferPurgeTest.php. All without a database.
21 October 2026: REST children lists without ezprestapiprovider
Fixed
- Fixed:
GET /api/ezp/v1/content/node/<id>/list/...and the same route atv2answered 500 (Class "ezpRestContentController" not found) on every installation that activatesezprestapialone, which is what a new installation and the public demos do. The content model looked up the Fields response group on the controller of the deprecatedezprestapiprovider;ezprestapi1.2.7 reads it from its own controller. Node reads (/content/node/<id>) were not affected. Installations that also activateezprestapiprovidernever saw it.
Upgrading
- Update
se7enxweb/ezprestapito 1.2.7 (~1.2.6incomposer.jsonalready allows it). The REST interface needs authentication: an unauthenticated request answers 401 withWWW-Authenticate: OAuth; send a personal API key (Authorization: Bearer expk_..., Personal API keys) or an OAuth token.
Tests
ezpRestContentServiceModelTest::testGetChildrenListWithAndWithoutFieldsinezprestapi(with a database).
21 October 2026: a payment gateway made in Setup > RAD is found
Updated
- The handler wizards for payment gateways (
/setup/handlerextension/paymentgatewayandpaymentgatewaydirect,kernel/setup/exphandlerwizard.php) write the class toextension/<name>/paymentgateways/<alias>/classes/<alias>gateway.php, which is whereeZPaymentGatewayTypelooks (<GatewaysDirectories>/<gateway>/classes/<gateway>gateway.phpfor each name inAvailableGateways[]). They wrotepaymentgateways/<alias>gateway.phpbefore, so a generated gateway was never loaded and never offered in the Payment Gateway workflow event. - The generated
registerGateway()call names the class itself, not<alias>gateway, which was a different name whenever the extension name had an underscore, socreateGateway()could not build it. - The RAD catalogue (
expRADCatalogue), Extension points and the help page on payment gateways for website creators give the new path; the services API's gateway list (expPaymentServices) reports the files the kernel would load.
Upgrading
- A gateway generated by an earlier version is not found as it stands. Move its class file to
extension/<name>/paymentgateways/<alias>/classes/<alias>gateway.phpand change the second argument of itseZPaymentGatewayType::registerGateway()call to the class name. The settings it came with need no change.
Tests
expRadWizardPaymentGatewayIsFoundTest: both gateway kinds are generated into a scratch directory, registered with the settings the wizard writes, loaded byeZPaymentGatewayTypeand built withcreateGateway(). Without a database.
21 October 2026: downloads on Velocity carry their type again, and an EPUB is application/epub+zip
Updated
- Updated: The HTTP cache contract (
kernel/private/classes/httpcache/ezphttpcachecontract.php) no longer loadseZFileandeZDir. Velocity's parent process loads the contract before it installs its source transform, and every worker inherits the classes compiled there: aneZFilecompiled that way kept PHP's ownheader(), which does nothing under the CLI SAPI. Everycontent/downloadon Velocity (:8080) went out astext/html; charset=utf-8, withoutContent-Disposition, and a range request was answered 200 instead of 206; Apache was not affected. The contract limits the modes of the files it writes itself (ezpHttpCacheContract::fileMode(),dirMode()), witheZFileandeZDirwhen they are loaded and by the same rule (EZP_FILE_MODE_MAX,EZP_DIR_MODE_MAX) otherwise. - Updated:
settings/mime.iniknows.epubasapplication/epub+zip, so the help e-books and every stored EPUB are downloaded with that type instead ofapplication/octet-stream.
Tests
expFileModeLimitsTest(FM-08): loading the contract (with the repair queue, before the autoloader) declares neithereZFilenoreZDir, and the contract limits modes exactly as they do for ten ways of setting the constants.eZUtilsHelpersTestlooks up an.epubfile. Without a database.
21 October 2026: the VAT rules page says when no handler applies the rules
Updated
- VAT charging rules are applied only by a VAT handler (
shop.ini [VATSettings] Handler,ezdefaultfor the shippedeZDefaultVATHandler), and the shippedsettings/shop.inisets none. The VAT rules page (shop/vatrules, design/admin and design/admin4) now says so when rules exist and no handler applies them, and when the handler that is set cannot be loaded (Exponential\View\Kernel\Shop\Vatrules::handlerNotice()). - The shipped file keeps
Handlercommented out on purpose. With a handler set,eZVATManagerreports dynamic VAT charging as enabled, and an existing store would change at checkout:eZShopOperationCollection::handleUserCountry()stops an order whose customer has no known country (withRequireUserCountry=true, the shipped value), the "Determined by VAT charging rules" VAT type appears on price fields, and that type is charged 0 percent instead of "unknown" where no rule matches. Fixed VAT types are charged the same with or without a handler. - The help pages on VAT rules and VAT types and on products, prices and VAT (English and German) explain the setting.
Upgrading
- Nothing to do unless the new notice appears. To apply your VAT rules, set
Handler=ezdefaultin[VATSettings]ofsettings/override/shop.ini.append.php, check that the customer's country is known at checkout (UserCountryAttribute, or the country of the shop account handler), and test an order before going live.
Tests
eZShopPricesTest: the shippedshop.inisets no VAT handler, a fixed VAT type is charged the same with and without one, and the VAT rules page's notice (no rules, no handler, a handler, a handler that cannot be loaded). Without a database.
21 October 2026: the media design loads the stylesheets of design.ini
Updated
- sevenx_themes_media 1.5.74: the media design (Fit & Healthy) loads the stylesheets extensions add to
design.ini [StylesheetSettings] CSSFileList[]andFrontendCSSFileList[], after its own files and in the same packed bundle. Before, its page layout listed its stylesheets inpagelayout/head/style.tplonly, and a file an extension added (cjw_newsletter.css, eztags.css, ezgmaplocation.css ...) never reached a media page. The entries the theme replaces are named in its newMediaReplacedCSSFileList[]and stay off the page, as before. - The help pages Styling your blocks and the examples Move inline styles into the stylesheet and
Restyle blocks without touching markup (English and German) add a stylesheet with
FrontendCSSFileList[]instead of a copy ofstyle.tpl.
Upgrading
- Repin
se7enxweb/sevenx-themes-mediato 1.5.74, then clear the INI, packer and template-block caches. A site that copiedpagelayout/head/style.tplinto its own extension keeps working; it can drop the copy and list its file inFrontendCSSFileList[].
20 October 2026: twenty-two more audiences of the help, in groups
Added
- Added: Twenty-two more audiences of the help, each with a dashboard card, a search filter, a pill, a category, a
landing page (
[Landing_<audience>], start pagehelp/<folder>/<folder>-start-here.md) and aliases: Community (community), Ecosystem (ecosystem), PHP Developers (php-developer), Theme designers (theme-designer, category Theme design), Resellers (reseller), Bundlers (bundler), AI, agents and agentic (ai-agents), Software Engineers (software-engineer), Adopters (adopter), Mobile App Developers (mobile-app-developer), Web Framework Developers (web-framework-developer), CMS Developers (cms-developer), DXP Developers (dxp-developer), E-commerce (ecommerce, foldere-commerce), Integrators (integrator), Database Administrators (dba, folderdatabase-administrators), Podcasters, radio and streaming (podcaster), Non-profits (non-profit), Local businesses (local-business), Individuals (individual), Non-technical people (non-technical, foldernon-technical-people) and Students and learners (student, folderstudents). Overlapping audiences name each other as related on their landing pages, both ways. - Added: The "Who makes Exponential" band of the help dashboard lists Partners, Community and Ecosystem after "Who is 7x?" and "Who is the Exponential Foundation?", each opening its audience's landing page.
Updated
- Updated: With forty-one audiences, the dashboard shows their cards in five groups (Use and run Exponential,
Organizations, Build with Exponential, Host, sell and partner, Community and learning), each a folding section with
its count: the first is open, the others folded. The search hero shows the filters of the first group and the others
behind More audiences. No inline CSS:
stylesheets/modules/help.cssof admin and admin4, shared tokens only; the iconsbook,chat-left-dots,chat-square-textandcollection-play(Bootstrap Icons, MIT, already inshare/icons) get their.a4-bi-*classes. - Updated: The new strings are translated in the twenty-three complete catalogues that have the
kernel/exphelpcontext and added tountranslated.
Tests
expHelpCommunityAudiencesTestcovers the twenty-two audiences and their categories, and CA-07 the dashboard groups (every audience in exactly one group), the "Who makes Exponential" links and each audience's start page (checked once the page exists);expHelpLandingPagesTestchecks their landing groups and that overlapping audiences relate both ways. All without a database.
21 October 2026: the Become an Exponential Partner block of the help pages is a card
Updated
- Updated: The Become an Exponential Partner paragraph that ends almost every help page, in every language
(Werden Sie Exponential-Partner in German), is shown in the help viewer as a card like the footer blocks of the
help dashboard ("Grow from user to developer", "Found an error?"): the partner icon on a tile, the bold lead as its
heading, the text, and the two links (Partners: start here, How to apply) as buttons, the first one primary.
expHelpMarkdownRendererrecognizes the block by the markers every page already carries (<!-- exp-partner-block:start -->and<!-- exp-partner-block:end -->), with optionpartner_card, which the viewer sets; the Markdown files are unchanged and read on GitHub as before. A marked block that is not one paragraph with a bold lead, or has no end marker, is shown as written. Both admin designs (admin and admin4), light and dark: the CSS is instylesheets/modules/help.css(.a4-help-partner*), shared tokens only, no inline CSS. - Updated: The PDF of a help page prints the card as a box with its title, its text and its links on one line. The search, the DITA and content exports and the generated example pages (which carry no such block) are unchanged. Cached pages render again (render version 7, PDF writer version 2).
- Updated: A page shown in another language than the reader's (
/(lang)/ger-defor an English reader) resolves its links in its own language: they were resolved against the English original's file, so the relative links of a German page (../partner/partner-hier-beginnen.md) named no page and were shown as text, and the German partner card had no buttons. The viewer renders the page with its language's index (expHelpDocumentIndex::setInstance()for the render, restored after;fixedInstance()is new), render version 8. A partner block whose links lead nowhere keeps its last sentence as text instead of showing it as buttons.
Upgrading
- Nothing to do: the cached help pages and PDFs are made again on their next view.
Tests
expHelpPartnerCardTest: the English and the German block become the card (heading, text, buttons with the links kept, the partner icon), other bold paragraphs and broken blocks stay paragraphs, the buttons follow the link policy, two real pages render one card each, the CSS of both designs is the same and uses tokens only, and the PDF prints the card. PC-08: a German page rendered with the German index has its two buttons to the pages they name, with the English reader's index none (the sentence stays text). Without a database.
21 October 2026: the layout editor saves a view type change alone and gives added items positions of their own
Updated
- explayouts_ui_api 1.3.29: on the Design tab, changing only the View type of a list is saved also when the new
view type leaves no item view type to choose (
list_zigzag,list_accordion, one of your own); + Add items gives every new item a position of its own (the new items first, in the order chosen) instead of storing all of them at the collection's offset, and moving an item moves the ones in between. - The pages of Two ways to build that described both as known editor behaviors
(
doc/eng-us/help/build-two-ways/known-editor-behaviors.md, the view type and list examples, and their German versions) say they are fixed in explayouts_ui_api 1.3.29 and keep the workaround for older versions.
Upgrading
- Collection items that already share a position are sorted out on the next change to their collection. No such items were left on the test installation.
Tests
- explayouts_ui_api:
CollectionItemPositionsTest(without a database) andtests/js/view-type-save.test.mjs(the bundle's own modules with jQuery 4).
21 October 2026: the kernel service container (first slice)
Added
- The kernel service container: shared objects by name (
expKernelServices::get( 'db' ),has(),tagged()), defined insettings/kernelservices.ini([Service_<id>]: class or factory, arguments, worker, request or prototype scope, tags), made on first use; extensions add theirs inkernelservices.ini.append.php. ClassesexpKernelServices,expKernelServiceContainer,expKernelServiceScope,expKernelServiceCompiler,expKernelExtensionOverrideList; consoleexp:services:compile,exp:services:list,exp:services:showandexp:services:check(kernel services). - The compiled container in
var/cache/kernelservices/(an index and one file per siteaccess): a recording of the web kernel's two extension activation passes, made by booting the kernel once per siteaccess in a separate process. A request applies it only when it starts from exactly the recorded state, and otherwise runs the passes as before; withAutoRebuild=enableda siteaccess without a current file records itself on its next web request. The deploy andezcache.php --clear-tag=inirebuild it (cache itemkernelservices, tagini). - The worker scope of the settings files (
WorkerScope=auto: whenEZP_INI_FILEMTIME_CHECKis false):eZINI::instance()hands out copies of settings objects built once per worker and siteaccess (eZINI::attachSharedStore()), kept on Exponential Velocity throughexpVelocity::DEFAULT_KEEP_GLOBALS(expKernelServicesWorker); any change of the override directories, a reset, a save or an INI cache clear drops them.
Updated
- The seven per-request resets of the web kernel's constructor are the request scope's reset hooks
(
expKernelServiceScope::DEFAULT_RESET_HOOKS, same order, same conditions), and code can add its own (expKernelServiceScope::onRequestReset()). - Measured on the test installation (62 active extensions, 18 siteaccesses): the start of a request up to the module run 38 to 50 ms -> 27 to 36 ms PHP-FPM-like (-15 to -35%), 21 to 31 ms -> 7 to 14 ms with Velocity's pool emulated (-51 to -72%); the constructor alone -40 to -54% and -74 to -78%.
Upgrading
- Deploy with
exp:velocity deploy --kernel --allow-root-user(new kernel classes). Nothing else: the container is compiled by the deploy, and[ContainerSettings] Mode=disabledinsettings/override/kernelservices.ini.append.phpswitches it off.
Tests
expKernelServicesTest(without a database): definitions and their validation (the shippedkernelservices.inihas no error), scopes, arguments, the request reset hooks, eZINI's worker store, compiled files, a recording applied only from the recorded state, the kernel wiring, and no change without a booted container.
21 October 2026: the classic administration carries no inline handlers or javascript: URLs
- Fixed: The context menus of the classic administration (the class icon of a node and of a version preview, the
icon of a sub item in the line and thumbnail views) open through the
data-menu-*attributes ofezpopupmenu.js, as in admin4, instead ofonclick="ezpopmenu_showTopLevel( ... )". - Fixed: The calendar of the date and date and time fields opens from a button with
data-a4-datepicker(page/content_datepicker.js), the "No wrap" switch of the package file viewer works throughpage/package_view_full.js, and the registration form focuses its first field and disables its buttons after a click throughpage/user_register.js. The classic administration loads the same files as admin4. - Fixed: The search scope pane's close link and the attribute group headings of the edit form use no
javascript:URL any more. - Updated: The content security policy test (CSP-10) reads every template of every administration design (classic,
admin3, admin4, admin4l and the admin designs of the shipped extensions) and fails on any
on...=attribute orjavascript:URL, also one whose code holds a<; its allowlist is gone.
2026-10-21: help images in code examples, eZTrustedProxy before the Velocity warm-up
Updated
- The help's image store skips image syntax inside code blocks and code spans
(
expHelpRemoteImages::references()): an example such asin a page about Markdown is no longer fetched and reported as a failed download. The help system pages write the example address asURL. - Velocity's early-load allow list (
appCache) nameseZTrustedProxy, which sends no header, cookie or exit, so the early-load warning no longer reports it. Takes effect afterexp:velocity restart(a deploy restarts it).
Tests
expHelpRemoteImagesTestHRI-03 also checks fenced blocks (backticks and tildes) and inline code spans.
21 October 2026: design extensions of another siteaccess, admin template errors, ezprestapi tests
Merged pull requests #296 and #297 (contributed by ekked).
Fixed
eZTemplateDesignResource::allDesignBases( $siteAccess )read the design list from thesite.iniof that siteaccess but its design extensions from the current one, so a design extension only the other siteaccess activates was left out (visual/menuconfig found its page layout in design/standard).designExtensions()takes an optional siteaccess and reads itsdesign.ini(#297). WithDesignLocationCache=enabled, clear the cache once.- setup/info sets every PHP setting it shows (an empty
open_basedirlogged "No such attribute"), and its OPcache and APCu boxes no longer read$cache.enabledof a cache that is not installed (#297). - class/view (admin, admin4) falls back to the name and description of the class's prioritized languages when the class or an attribute has none in the language shown (#297).
Added
design/admin/templates/notification/status.tpl: the notification status page in the admin design (and admin3, which uses it), without javascript; it had a template in admin4 only (#297).
Tests
- Testsuite
ezprestapi(without a database): the extension names no class of the deprecatedezprestapiprovider, and its routes, constants andrest.iniproviders are declared (#296). It skips whenextension/ezprestapiis not installed. SiteAccessDesignAndAdminViewWarningsTest(without a database) (#297).
21 October 2026: the help's manuals are made from the Markdown help
The help as content had one manual, one chapter and five topics; the real help is the Markdown in doc/eng-us/ and
doc/ger-de/. The manuals are now made from it, and the Markdown stays the source: they are rebuilt on each
release, and an edit made only in the administration is overwritten by the next build
(Manuals from the Markdown help).
Added
- Added:
./console exp:help:content build-manuals -s <admin siteaccess> [--manual=<key>] [--language=ger-DE,...] [--release] [--dry-run] [--json]: one manual per line ofexphelp.ini [ManualSettings] Manuals[](52: the 45 folders ofdoc/eng-us/help/, Getting started from the pages directly inhelp/, and install, guides, features, bc as Contributor notes, standards and specifications; not history and examples), each subfolder a chapter, each page a topic (concept, task, reference or troubleshooting: front mattertopic_type, else from the page), in the help's order, with the page's audiences, the section from them, and the German pages (doc/ger-de/) as the ger-DE translation when the installation has that language (never created; reported). Links between pages that are topics become links to the topic, other links and images point to the help. Everything it makes has a remote_id starting withhelp-gen-: a rebuild updates those objects in place, removes those whose page is gone and never touches any other object. A second run printsNo change.. - Added:
exphelp.ini [ManualSettings]:Manuals[](the scope is a list edit),Languages[],TranslatedTitles[],License,BuildOnInstall,BatchSize.[ContentSettings]gets the other audiences of the help as audience options with theirAudienceMap[], and the areadocof the generated help_ids (doc.help.editors.publishing). - Added: The kickstarter and
./console exp:install --applyqueue the build of the manuals at the end of an installation (exp:help:content build-manuals --queue -s admin) and do not wait for it: the content jobhelpmanuals(expContentJobHelpManuals) installs the help as content and builds the manuals in the background over the next 20 to 30 minutes, at the lowest CPU priority, ten objects per transaction with a pause after each, one such job at a time, resumed by the cronjob partcontentjobswhen its worker dies. In a fresh SQLite sandbox the installation took 36 seconds (28 seconds without the help step; the build in the foreground had made it 22 minutes). The help's manual pages andexp:help:content reportsay "Manuals are being built (n of 52)" while it runs (translated in the 24 catalogues with the context kernel/exphelp).BuildOnInstall=disabledleaves it out; after the setup wizard, runbuild-manuals --queueonce. - Fixed: A content job started by root on an installation that belongs to root (a container, a sandbox) never ran:
its worker refused to run as root. The worker command gets
--allow-root-userwhen root starts it; on an installation with a site user the worker still becomes that user first. - Added:
exp:help:content ebooksandexporttake--language=ger-DE: the German edition of a manual's e-books, stored ashelp-ebook-<manual>-ger-de-<format>next to the default language's and listed on the manual's page.
Updated
- Updated: A first build makes about 5,700 objects; it publishes each new object once with its translation, writes in
transactions of 50, publishes without notification, leaves the search index to the
indexcontentcronjob and does not clear the view cache per object. On a copy of the installation's SQLite database (52 manuals, 342 chapters, 5,315 topics, 4,614 with German): 28 minutes for the first build on a busy server, 34 seconds for a run without changes (No change.); without the single publish per object the first build was on course for about 45 minutes.
Upgrading
- Run after the deploy (and on each later release):
./console exp:help:content install --apply -s admin, then./console exp:help:content build-manuals --apply -s admin --release, then./console exp:help:content ebooks --apply -s adminand./console exp:help:content ebooks --apply -s admin --language=ger-DE. Theinstallstep appends the new audience options to the help classes.
Tests
expHelpManualBuilderTest(without a database): the manual list (the shipped 52, every help folder, every audience mapped), remote_id and help_id stability, links, topic types, short descriptions, the plan (chapters, order, parents first, audiences, sections, the validator finds no ERROR), German pairing, idempotence, what a rebuild removes and the background job (registered, "n of 52", the installer only queues it);expHelpContentTranslationsTestchecks the new string in every catalogue.
2026-10-22: admin4's header tabs show their icons at every width
Updated
- The tabs of admin4's header row draw their icons (
menu.ini [Topmenu_<tab>] Icon) at every width. Since 2026-10-18 the row hid them between 576 and 1599px to fit more tabs, so on most screens no tab showed an icon. A tab that does not fit the row moves into "More", as before. The extensions of the se7enxweb repositories give their tabs icons in their next releases.
Tests
AdminMegaMenuTestchecks that no rule hides the row's tab icons.
2026-10-22: extensions with header tab icons and modern release files
Updated
- Required extension versions, each with an icon for its admin header tab and a rewritten README, composer.json,
ezinfo.php and extension.xml: bccie 1.1.19 (
arrow-down-up), cjw_newsletter 4.2.15 (envelope), exp_adminui 1.0.0.50 (images,box-seam,people), eztags 2.4.24 (tags), git_manager 2.0.23 (git), sevenx_dse 1.1.15 (brush), syndication 1.3.11 (rss), xrowextract 2.5.19 (download).
2026-10-22: admin4 escaping, multi-edit autosave, page scripts on plain JavaScript, no jQuery 4-removed APIs
Merged pull requests #298, #299 and #300 (contributed by ekked).
Fixed
- The reverse related list heading (admin, admin4, standard) washes the object name, and the existing URL alias warning (admin, admin4) washes the alias it links (#298).
- The option editor (admin4, standard) gives the additional price field an id of its own instead of the one of the option text (#298).
- The multi-edit autosave takes the form's state when it sends it, so changes typed while a save is in flight go out with the next save, and a failed save is tried again (#298).
- The content edit page labels its fields without
jQuery.trim(), which jQuery 4 removed (#299). - The content edit attribute groups set
aria-expandedon their toggle link, and their names are washed (admin, admin4). - The online editor's link dialog focuses its first field with
.first()instead of the jQuery-only:firstselector; TinyMCE's jQuery adapter binds with.on()and.off().
Updated
- admin4's small page scripts run on plain JavaScript: content edit (attributes, languages, relation uploads), URL alias, node details, locations and ordering, translations, template look edit, user register (admin and admin4) (#300), and class edit.
Tests
AdminPageScriptsWithoutJQueryTest(without a database) (#300), extended to class edit and multi-edit.AdminJQuery4RemovedApisTest(without a database): no script of the admin designs, design/standard or the extensions the kernel ships uses an API jQuery 4 removed (jQuery.trim,isArray,type,isFunction...),.size(),.andSelf(),.live(), the.load(fn)shorthand, the deprecated event binders or a jQuery-only positional selector.
2026-10-22: the transfer registry, import and export tools that know each other
Every import and export tool declares itself in kernelservices.ini, and every link to a tool reads that list, so a
user finds the right tool from where they are and never a link to a tool that is off or not theirs. Guides:
doc/eng-us/features/6.0/transfer-registry.md, for users doc/eng-us/help/import-export-tools.md (both in German
too).
Added
- The transfer registry:
expKernelTransferRegistry(kernel servicetransfer.registry) collects the services taggedtransfer.tool, each anexpKernelTransferToolwith its name, description, direction, scopes, formats, entry URL, permission, help page, round-trip partners, task words and context entries. It lists a tool only when its module exists and the user has its policy. - The kernel's own tools: Packages, RSS import, RSS export, Collected information, Audit export, PDF export.
- The template operators
transfer_tools,transfer_actions,transfer_related,transfer_tool, and the shared template partstransfer/looking_for.tpl,transfer/left_menu.tpl,transfer/actions.tpl,transfer/search_suggestions.tpl(shared admin components, no CSS of their own) for the kernel and the extensions. - The help page "Which import or export tool do I need?" (English and German): from the task to the tool, the round trips, and which one when two seem to fit.
Updated
- The Import/Export menu of an item and the context menus' Advanced > Export list the tools' item entries (for example "Import into this folder…" on a container the user may create in) and end with "Which import or export tool do I need?".
- The class page lists the tools that work on the class's objects; the Setup menu has an "Import and export" list.
- The pages of Packages, RSS, Collected information, Audit export and PDF export show a "Looking for another import or export?" box with the other tools, the ones that read their files first.
- The administration's search shows up to three import and export tools that match the search words ("import csv", "auf eine andere Website kopieren") above the results.
- The ? help menu of a tool's pages lists the "Which tool" page and the tool's own help.
Tests
expKernelTransferRegistryTest(without a database): the shipped blocks validate and their help pages exist, parsing, availability by module and policy, filters, context entries and placeholders, round-trip order, search, the help menu topics, the operators, and that the help page names every kernel tool in English and German.
2026-10-22: transfer runs, one engine and one run history for every import and export tool
The tools of the transfer registry run their background work the same way, on whichever engine the installation has,
and every run of every tool is recorded in one table, which the run list and the Statistics of Imports read with a
tool filter. Guide: doc/eng-us/features/6.0/transfer-runs.md (in German too).
Added
expKernelTransferRunner: a run goes to the job queue (expqueue) when it is active, the tool declares its job type and a worker is alive, else to the kernel's content jobs, else the tool runs it its own way as before.transfer.ini [TransferSettings] Engine(auto),ToolEngine[<tool>],QueueNeedsWorker; per toolEngine=,QueueJobType=andHistoryAllPermission=in its registry block.expKernelTransferTask(the work of one run),expKernelTransferContext(progress, heartbeat, cancel, log, the same on every engine),expKernelTransferCancelledException, the expqueue handlerexpKernelTransferQueueHandler(declared only where expqueue is active) and the content job typetransfer(expContentJobTransfer, which runs the task outside its batch transaction).- The run history: the table
exp_transfer_run(tool, direction, kind, title, scope, user, state, counts, bytes, duration, format, file reference, engine and its job, the tool's own job, the link to its page, errors) on every engine,expKernelTransferRun, andexpKernelTransferHistoryto write a run (start, progress, finish, record) and to read runs with the user's rights (a user sees every user's runs of a tool only with its HistoryAllPermission), as rows in the shape of a run of Imports for its Statistics. update/common/scripts/6.0/createtransferruntable.phpcreates the table on any engine; the 6.0.0 to 6.0.15 update files of MySQL, PostgreSQL and SQLite create it too (only when missing).
Updated
- The cronjob part
contentjobsalso removes runs of the history older than[TransferSettings] KeepDays(365). - The help page "Which import or export tool do I need?" says where the runs are listed; the transfer registry guide and the database update guide link to the new guide.
Tests
expKernelTransferRunsTest(without a database): the table is the same inshare/db_schema.dba, the persistent object, the three kernel schemas and the update files, within Oracle's identifier limits; the SQLite update runs twice; the values a row can hold, the statistics row, the filters, the engine choice and its fallback, the payload check, a task's run with progress, cancel and failure, the new registry keys and who sees which runs, the settings, and the content job type.
2026-10-22: data tables and the secret store
The tables of the persistent classes become part of the system: a catalogue of every class with its table and its
relations to content, a description of each table as its database has it, named database connections, and classes
that read and write on their own connection. Their passwords, and those of every other tool, live in one encrypted
secret store. Guides: doc/eng-us/features/6.0/data-tables.md and doc/eng-us/features/6.0/secret-store.md (both in
German too).
Added
- The data table catalogue (
expDataTableCatalogue): everyeZPersistentObjectclass of the kernel and the active extensions with its table, fields, keys, sort, function attributes, connection and relations (from the definition'srelationskey, a field's foreign class,datatable.ini [Relation_<class>_<column>], or inferred from the column's name), kept in the cache until the autoload arrays change. expDataTableDescriber: a table as its database has it (columns, rows, from the engine's own catalogue, a sample of documents on MongoDB) and where it differs from the definition (missing table or column, extra column, type).- Named database connections (
expDataConnections, tableexp_data_connection,datatable.ini [Connection_<id>]): SQLite files invar/datatable, MySQL/MariaDB, PostgreSQL, Oracle, SQL Server and MongoDB, here or remote, each an eZDB instance of its own per request; Test connection with the server's version, tables and time. expPDODB: a generic database driver over PDO for the engines the kernel has no driver for (SQL Server, Oracle without ezoracle).- Persistent classes on a connection:
'connection' => '<id>'indefinition()ordatatable.ini [ClassSettings] Connection[<class>];eZPersistentObject::dbFor(), with conditions and values escaped for that connection. - The secret store (
expSecrets,expSecretKey, tableexp_secret): values encrypted with XChaCha20-Poly1305, the key fromEXP_SECRET_KEYor a key file made on first use invar/secrets/, key rotation, names resolved fromenv:, the store andsecrets.ini. - The administration pages Setup > Data tables (module
datatable: the catalogue, a class's page, Connections, one connection) and Setup > Secrets (modulesecrets), a Setup menu group "Data tables", and the policiesdatatable/read,datatable/manage,datatable/connections,datatable/syncandsecrets/manage. expKernelTableSchema: whether a table exists on any engine (from its catalogue), and creating missing tables of a.dbaschema; the Data tables and Secrets pages create their tables with it, no command needed. The 6.0.0 to 6.0.15 update files of MySQL, PostgreSQL and SQLite create both tables too (only when missing).expDataTableSchemaand the page Setup > Data tables > Schema (datatable/schema): a table made from a class's definition or an extension'sshare/db_schema.dba, created or upgraded on any connection, the SQL shown before it runs; dropping and changing columns or indexes only when ticked; the install SQL of MySQL, PostgreSQL, SQLite, Oracle (sequence and trigger) and SQL Server (IDENTITY).- The rows of a data table (
datatable/rows/<class>), page by page, filtered by one field, relation values linked to what they point at, columns that look like secrets left out. - The tab Data tables of the content view: the rows of extensions' tables that point at the node or its object
through a declared relation (
datatable.ini [DataTableSettings] ContentTabInferredadds the inferred ones). expDataTableWriter: the one way to write rows into a data table (insert, update, upsert, replace, dry run; matched by unique keys; references@object:,@node:,@path:; a refused row fails alone).- The fetch functions
datatable/list,count,row,pointing_atandcanfor templates. - The data tables in the transfer registry (
[Service_transfer.datatable]), with 7x SQLI Import as round-trip partner. - The module table wizard (Setup > RAD > Module table,
setup/moduletable): a new data table from a list of fields, a content class, an existing table or a field source of an extension (expDataTableFieldSource, tagdatatable.field_source), written as a new extension or added to an existing one: the persistent class, the schema and the install SQL of every engine, a module with list, edit, remove and a JSON endpoint and the policies read and edit, the admin templates, fetch functions and a Setup menu entry. - The wizard installs from the browser: it creates the table on its connection, activates the extension through the kernel's activation (with a copy of the settings file kept first), regenerates the autoload arrays and clears the caches.
- The synchronisation between data tables and content (
expDataSync, Setup > Data tables > Synchronisations,datatable/syncsanddatatable/sync/<id>, policydatatable/sync): a data table, a content class and a node, a map of columns to attributes and@sources (object and node ids, remote ids, name, times, class, section, owner, URL alias) with a proposed map, key columns, a direction (content to table, table to content, both ways), and a run mode (once, scheduled, live); every run goes through the transfer runner and the run history. - The tables
exp_data_sync,exp_data_sync_link(the pairs of a row and an object with the hashes of both sides) andexp_data_sync_conflict, in the kernel schemas and the 6.0.0 to 6.0.15 update files of MySQL, PostgreSQL and SQLite (only when missing), or created from the Synchronisations page. - The live mirror (
expDataSyncListener): the kernel notes published, moved, removed, purged and trashed objects and writes them to the tables of the live synchronisations of their class at the end of the request; the eventscontent/publish,content/moveandcontent/remove. - The workflow event Mirror to data tables (
expdatatablemirror) for the mirror as a step of a workflow. - The cronjob part
datatable(cronjobs/datatablesync.php, also infrequent) for the scheduled synchronisations. - Conflict rules for two-way runs (the newer change, the content, the table, or ask) and the page of open conflicts
(
datatable/conflicts) with Take the content and Take the table.
Tests
- The two tables are the same in
share/db_schema.dba, the persistent classes, the three kernel schemas and the update files; the catalogue, the description, the connections, persistent classes on a SQLite connection, the secret store's encryption, key and rotation, andexpPDODB(all without a database). - The schema (definitions, the SQL of every engine, plan and apply on SQLite), the writer, the relations, the fetch functions and the registry entry, on a SQLite connection (without an installation).
- The module table wizard: the field list and sources, the settings and their problems, every generated file (PHP syntax, no inline styles), adding a table to an existing extension, and the generated class on a SQLite connection.
- The synchronisation: its values, map and problems, hashes, keys, conflict rules and typed values; definitions, pairs and conflicts on SQLite; the live mirror's listener, the cronjob part, the workflow event type and the kernel's calls of the listener; its three tables in every schema and update file.
2026-10-22: image variations follow StorageFilePermissions
New images and their variations took their mode from image.ini [FileSettings] ImagePermissions, shipped as 0666,
not from site.ini [FileSettings] StorageFilePermissions. An installation that narrowed only the latter (for example
to 0644) still wrote every variation world-writable: measured on a production site, 215 variations were
-rw-rw-rw- while every other stored file was 0644.
Updated
image.ini [FileSettings] ImagePermissionsships empty. Empty means the mode of every other stored file,site.ini [FileSettings] StorageFilePermissions; a value of its own still wins. With the shipped settings nothing changes (both are0666).eZImageHandler::imagePermissions()returns the mode that applies.- Upgrading: an installation that sets
StorageFilePermissionsand wants images to follow it needs no change; one that relies on images being wider than its other files setsImagePermissionsitself. Variations written before keep their mode:find var -type f -perm -o+wlists them. - The documentation of the setting, the file modes page and the hosting page on writable directories say so (English
and German); the hosting page also says that
image.ini DirPermissionsis not read (image directories are made withStorageDirPermissions).
Tests
expFileModeLimitsTestFM-09: an emptyImagePermissionstakesStorageFilePermissions, a value of its own wins.
2026-10-22: expimageeditor, an image editor in the browser under the Content Security Policy
A new extension, expimageeditor, edits the image of an image attribute in admin4 in the browser; the server only
stores the result. It works without jQuery and under the administration's Content Security Policy (no inline script
or style). It is shipped with the kernel but not active: activate it with ActiveExtensions[]=expimageeditor and
switch ezie off. Guide: extension/expimageeditor/README.md.
Added
- Raster images (JPEG, PNG, WebP) with Cropper.js 2.3.1 and canvas tools: crop (free, the original shape, the fixed
shapes of the image aliases and
AspectRatios[]), rotate by 90 degrees or an angle with a background color, flip, resize, brightness, contrast, black and white, sepia, pixelate or blur a region, a watermark (a text or an image ofWatermarks[]), undo and redo, zoom. - SVG images with an admin4 toolbar on the drawing engine
@svgedit/svgcanvas7.4.2: select, rectangle, ellipse, line, path, freehand, text, delete, duplicate, bring to front and send to back, fill, stroke, stroke width, opacity, font size, undo and redo, zoom. The full SVG-Edit interface is not used: it adds<style>elements and style attributes the policy refuses. - Fonts of the site (
Fonts[], loaded with the FontFace API) for the watermark and the SVG text; in an SVG, text in such a font can be saved as paths (opentype.js, loaded on the first save that needs it), so the image looks the same where the font is not installed. - The view
expimageeditor/save/<attribute id>/<version>(policy functionexpimageeditor/edit): it takes the result only for the user's own draft with edit access in the attribute's language and the form token in theX-CSRF-Tokenheader, checks the file by its content (eZImageType::imageFileError()), keeps an SVG an SVG and a raster image a raster image, allows the types ofOutputMIMETypes[]and the class attribute's maximum size, and stores it througheZImageAliasHandler, an SVG only as the output ofexpSvgSanitizer. Cancel leaves the draft untouched; the previous version keeps its image. - Keyboard use (the toolbar is one tab stop with the arrow keys, shortcuts, the focus returns to the button), light
and dark color modes, English and German texts. The dialog uses admin4's own controls (
.a4-btn,.a4-btn-primary,.a4-icon-btn,.a4-field,.a4-input). - New SVG drawing beside an image attribute without an image, where SVG is accepted: the SVG editor starts with
an empty drawing of
NewDrawingSize(800x600) and the save view stores it asdrawing.svg. admin4'scontent/datatype/edit/ezimage.tplincludes the newezimage_new.tplthere; admin4's own copy draws nothing. - Black and white and sepia are shown on the image at once (selection or whole image) and become a step with Apply.
- Sample fonts under the SIL Open Font License 1.1, not configured by default: Lora, and Noto Serif and Noto Sans 2.015 (Latin, Greek, Cyrillic, Vietnamese).
Tests
expImageEditorStoreTest(without a database): the stored name, broken uploads, the kind of image, the output types, a hostile SVG, the maximum file size, and that only the user's own draft is editable.expImageEditorDesignTest(files only): the template's guard and data, every text translated and used, every icon with its file and class, the scripts it names, and no inline code or style in the scripts.AdminContentSecurityPolicyTestchecks the templates ofexpimageeditoras a shipped extension. Suiteexpimageeditor.
2026-10-22: a section is assigned to the whole subtree, the subtree without the branches in another section, or the object only
Every place that assigns a section (section/assign with the content browser, the Details tab of a node, the edit form,
the content job section and ./console expcontentjob section --scope=...) offers which objects below the chosen
item get the section: all of them (subtree, the default and the behavior before), those in the section the item
had (inherited: an object below in another section keeps its section, with everything below it), or the item alone
(object). Guide: Sections, section 8.5.
Added
eZSection::SCOPE_SUBTREE,SCOPE_INHERITED,SCOPE_OBJECT,scopes(),isValidScope(),scopeFromHTTP()(the POST variableSectionScope);eZSection::applyTo(),eZContentObjectTreeNode::assignSectionToSubTree(),eZContentOperationCollection::updateSection()and the operationcontent/updatesectiontake the scope as an optional parameter whose default is the old behavior.sectionAssignmentObjectIDs(),sectionBoundaryPaths()andpathIsInAny()find the objects; the content job builds its work list once, before anything changes.- The choice in admin and admin4: above the content browser of section/assign, beside the section of the Details tab
and in the edit form's section menu (
page/section_browse_assign.js, no inline script or style). - The audit event
content.node.sectionnames the scope (after.scope). eZSection::applyToOrStartJob(): from the Details tab and the edit form, a subtree with more nodes thancontent.ini [ContentJobSettings] NowLimit(1000) is assigned by the content jobsection, in batches; the Details tab opens its progress page. The object alone is always done at once.eZContentObjectTreeNode::sectionAssignableObjectIDs(): the objects the user may assign a section to.
Updated
- section/assign (done at once) and
eZSection::applyTo()change only the objects the user's section/assign limitations (Class, Section, Owner, Subtree) allow, as the content job already did.assignSectionToSubTree()andcontent/updatesectioncheck it only when asked (check_access, default false), so the kernel's own changes (a new main location takes its parent's section) are not narrowed. applyTo()updates the search index once, after the commit, with the objects that changed.
Tests
eZSectionScopeTestSC-01 to SC-13: the scopes, the operation's parameters, each mode on a small tree of its own (subtree, inherited, object, an unknown scope), the tree function and the operation, the content job, the access check with and without limitations, the audit event, and the content job above NowLimit.AdminSectionScopeTest: the choice in the templates of admin and admin4.
2026-10-22: refinements of the image editor, the section scopes and the image modes
Updated
expimageeditorre-encodes a raster image on the server before it is stored (GD decodes it and writes it anew,expimageeditor.ini ReEncodeRaster, enabled): nothing but the pixels reaches the storage. The copy lives under a random name in<cache directory>/expimageeditor/and is removed when the request ends; stale copies after an hour.expimageeditorhas limits of its own, checked before anything is decoded:MaxPixels(25 megapixels) andMaxFileSize(20 MB).- The save view of
expimageeditorcomparesX-CSRF-Tokenwith the session's form token while ezformtoken is active, and clears the stat cache before it looks for the stored file (a Velocity worker remembers file answers within a request). expimageeditorcarries the version it ships with (6.0.15) inextension.xmlandezinfo.php, and is onexphelp.ini [IssueTrackers] BundledExtensions[]. Documentation in English and German: expimageeditor. The dialog says "color" (American English).- An image variation without a usable mode (
ImagePermissionsandStorageFilePermissionsboth empty, or a value that is not an octal mode) gets the shipped0666, limited as every other mode, never mode 0.
Tests
expImageEditorStoreTestIE-09 to IE-12: the re-encoding, the limits, the sanitizer, the form token and the temporary files.expFileModeLimitsTestFM-10: the fallback mode of an image variation.
2026-10-22: admin4's frame, tree, node view, edit pages and dashboard run on plain JavaScript
Merged pull requests #301, #303, #305, #306 and #307 (contributed by ekked).
Updated
- admin4's menu bar, sidebars and column grip, and the content structure tree (admin and admin4) run without jQuery;
the tree loads its sub items with
fetch()(#301). - admin4's node view: the tabs, the sub items table on
Exp.datatable.create()and the drag and drop of the priorities run without jQuery (#303). - The relation upload (
expajaxuploader.js,expAjaxUploader(), with the jQuery plugin kept where a page has jQuery) and the relation search (admin and admin4), and queued publishing run without jQuery; admin4's class edit binds its move buttons and keeps the focus on the button pressed (#305). - The Online Editor requires jQuery only for the built-in TinyMCE 3 (#306).
- The dashboard's community activity block runs without jQuery (#307).
a4_dom.js, loaded by admin4's page head right aftera4_url.js: the helpers the scripts on plain JavaScript share (a4Dom.ready,each, a delegatedon,formToken,headers, a formpostwith the form token, the CSRF header andX-Requested-With, andgetJSON). Class edit and the node tabs use it; class edit listens once on the attribute list and marks itaria-busywhile a move is saved, and still puts back a move the server did not store.- admin4's relation search template requires Exponential UI (
exp::core::shared,exp::io), which its script runs on, instead of jQuery.
Fixed
- The community activity block writes the forum posts as text (title, author, date), links only http and https addresses, and shows "The latest forum posts could not be loaded." instead of loading for ever when the feed fails.
- Queued publishing shows the server's error text as text, not as markup.
- The relation search escapes the box id of the answer before it uses it in a selector, and an answer without content no longer throws.
Tests
AdminPageScriptsWithoutJQueryTest(without a database), extended to the sub items page script and the two relation upload page scripts, and to every other script converted here (frame, tree, node tabs, sub items, drag and drop, relation search and upload, queued publishing, dashboard,a4_dom.js): no jQuery, noevalor string timers, no inline handlers; the head loadsa4_dom.jsin both lists; the queued publishing and relation search templates require no jQuery; the Online Editor asks for jQuery only for TinyMCE 3; answers go in as text.AdminClassEditMoveTest(#305) checks the move is posted througha4Dom.post(); the node tabs harness runsa4_dom.jsbeforenode_tabs.js.
2026-10-23: the node view's Tabs switch shows the new state at once
Fixed
- Switching the node view's "Tabs" on or off left an empty view (no panel, or no blocks) until the page was reloaded by hand: the server draws "with tabs" and "without tabs" with different markup, and since the node view's scripts run on plain JavaScript the switch only changed classes. It now saves the preference and loads the page again once the save is done (the switch's own link, a GET, if the save fails), in admin4 and in the classic admin.
Tests
AdminKeyboardTestchecks that both switches load the page again after saving.
2026-10-23: URL aliases for titles in scripts without a transliteration table
Under the shipped site.ini [URLTranslator] TransformationGroup=urlalias a title in a script without a
transliteration table lost every letter: Arabic دليل المستخدم, Persian, Pashto, Chinese 用户手册, Japanese,
Korean, Thai, Devanagari, Bengali, Georgian and Armenian titles gave an empty alias, so the node was called
node_<node id> (a custom alias noname1, any other element _1). Latin, Cyrillic, Greek and Hebrew are
transliterated and were not affected.
Added
site.ini [URLTranslator] ScriptFallbackTransformationGroup, shipped asurlalias_iri: when the configured transformation leaves no letter of a title that has letters, that title alone is converted with this group (دليل المستخدمbecomesدليل-المستخدم). A title that keeps a letter, also a mixed one (Exponential دليلstaysexponential), gets exactly the alias it got before; a title without letters still getsnode_<node id>,noname1or_1. Empty turns it off.eZURLAliasML::convertToAlias()applies it, so it covers node aliases, custom and global aliases,updateniceurls.phpand image file names;convertToAliasCompat()(path_identification_string) is unchanged.- Upgrading: existing aliases stay as they are. A node with such a title gets the new alias when it is published
again or the aliases are rebuilt, and the old
node_<node id>alias stays as a redirect to it. - Titles in scripts without a transliteration table (English and German), the URL alias settings of the help and the right-to-left section of the languages page say so.
Tests
eZURLAliasMLScriptFallbackTest(SF-01 to SF-06, no database): Arabic, Persian, Chinese and Thai get the IRI alias; Latin, Cyrillic, Greek and Hebrew keep today's aliases; the mixed title rule; titles without letters; the setting off;urlalias_compatandurlalias_irias configured group,convertToAliasCompat()andidentifierunaffected.eZURLAliasMLTest(database group) checks the plain groups with the fallback off.
2026-10-23: month names on their own and inside a date
A locale file had one list of month names, used inside a date and on its own. Languages that inflect the month name
need two: the calendar of month_overview read октября 2025 in Russian instead of октябрь 2025.
Added
- Locale files: the optional groups
[LongMonthNamesStandalone]and[ShortMonthNamesStandalone](jan...dec), the month names as they read on their own (CLDR's stand-alone form,LLLL/LLL).[LongMonthNames]and[ShortMonthNames]stay the form used inside a date (MMMM/MMM). eZLocale::longMonthNameStandalone(),shortMonthNameStandalone()andmonthsNamesStandalone(), the template attributemonth_name_standalone_list; each falls back, month by month, to the normal name where a locale has no stand-alone name.- The stand-alone groups, from CLDR, in 51 locale files whose stand-alone names differ from the format names, among
them
pol-PL,cze-CZ,slk-SK,cro-HR,rus-RU,ukr-UA,bel-BY,lit-LT,fin-FI,ell-GR,hye-AMandcat-ES.
Updated
month_overview(the month and the previous, current and next months) and the month list ofshop/statisticsshow the stand-alone name.
Fixed
pol-PL,cze-CZ,cro-HR,slk-SK,fin-FIandcat-ESprint the month inside a date in the form CLDR uses there, with CLDR's day names and long date formats:środa, 15 października 2025,dimecres, 15 d’octubre del 2025.ukr-UAprints15 жовтняinstead of15 Жовтень, and its short month names are CLDR's (жовт.). The short November ofell-GRis written with a Greek Ν again (Νοε, it had a Latin N).
Tests
expLocaleStandaloneMonthNamesTest(suitelib, no database): the format form in dates and the stand-alone form on its own for twelve inflecting locales, the fallback, and that every stand-alone group has twelve names and differs from its format group.eZKernelCommonOperatorsTest:month_overviewnames the months in the stand-alone form (rus-RU,pol-PL).
2026-10-23: admin4 and admin4l load jQuery only on the pages that request it
Updated
- admin4's
page_head_script.tplno longer prependsezjsc::jqueryandezjsc::jqueryioon every page: its firstezscript_loadstarts witha4_url.jsanda4_dom.js, thendesign.ini [JavaScriptSettings] BackendJavaScriptList[], then the scripts the page's templates requested. The kernel's admin4 pages request no jQuery; a template whose script needs it, or an Exponential UI module, requests it withezscript_require, outside every cache-block. - The page layouts of admin4 and admin4l include
page_head_script.tploutside the head's cache-block, so the head prints what the page requested on every request, and a request made later in the page prints its scripts in place. - The templates that run on Exponential UI request the modules they use instead of relying on expui's
BackendJavaScriptList[]: the sub-items table (exp::io,exp::datatable), the toolbars of content edit and class edit (exp::sticky), the date fields (exp::datepicker) and the relation upload (exp::io,exp::dialog,exp::upload). - Reduced motion also switches jQuery's effects off when a page loads jQuery after
admin4.js.
Added
design.ini [JavaScriptSettings] ClassicBackendJavaScriptList[], empty: the older admin designs (admin,admin2,admin3) load it on every page after jQuery andBackendJavaScriptList[]; admin4 and admin4l do not read it. An extension moves its jQuery plugins andexp::modules there fromBackendJavaScriptList[].
Upgrading
- An extension or override that puts a script needing jQuery or an
exp::module intoBackendJavaScriptList[]has to request it from its admin4 templates (and list it inClassicBackendJavaScriptList[]for the older designs). - Several packages of the root
composer.jsonstill put jQuery or Exponential UI on every page and follow in their own releases: eztags 2.4.24 (which keeps jQuery on every admin4 page while it is active), expui 1.0.0.15, xrowmetadata 1.4.16 and expai 0.0.1.17; ezautosave, ezmultiupload, ezstarrating and the layout editor of explayouts_ui_api run on jQuery until theirs. See admin4 and admin4l without jQuery.
Tests
AdminJQueryOnDemandTest(JQ-01 to JQ-13, without a database): admin4's head loads no jQuery, the kernel'sBackendJavaScriptList[]names nothing that needs jQuery or Exponential UI, every admin template that loads a script calling jQuery requests it, no such request sits in a cache-block, the page layouts print the head's scripts outside the cache-blocks, the older designs readClassicBackendJavaScriptList[], and the node view and edit pages request only the modules they use. JQ-13 checks the installed packages; it is skipped while a package still puts jQuery on every page, and names what still relies on it.AdminPlainScriptsExpUIPacksTestcountsClassicBackendJavaScriptList[]for the admin design;AdminContextMenuTest,AdminHeaderTest,AdminPageScriptsWithoutJQueryTest,AdminSiteAccessMenuTestandAdminSubitemsPriorityDragTestfollow the new head.
2026-10-23: the layout editor runs without jQuery
Needs explayouts_ui_api 1.3.30 (se7enxweb/explayouts_ui_api pull request #12); composer.json requires
se7enxweb/explayouts-ui-api ~1.3.30.
Updated
- The layout editor of Layouts loads no jQuery, no jQuery UI, no Bootstrap JavaScript and no Moment.js:
app.tplno longer requestsezjsc::jqueryandezjsc::jqueryUI, and its app bundle runs on a small DOM layer of its own. The page loads 529 KB less. - Blocks, block types and collection items are dragged with the editor's own drag and drop on Pointer Events: mouse, touch and pen at every screen width, and the canvas keeps scrolling while the pointer is held at its edge.
- Dialogs of the editor say what they are, keep the focus inside while open and give it back on close; the date field is the browser's own and sends the same value as before.
Added
- Blocks, block types and collection items move with the keyboard: Space or Enter picks up, the arrow keys move (into the zone before or after), Home and End, Space or Enter drops, Escape cancels; every step is announced.
Fixed
- The Options, Items and Advanced panels of the editor's sidebar fold again: under jQuery 4 Bootstrap's collapse stopped with an error on the first click.
Tests
AdminLayoutEditorWithoutJQueryTest(without a database): the editor page requests no jQuery, loads only the checked scripts, none of them uses jQuery, the bundle carries neither jQuery nor jQuery UI, and no setting adds jQuery to the page. Skipped, with the reason, while explayouts_ui_api is missing or older than 1.3.30.
2026-10-23: demo mode stays with its installation under Velocity, and admin3 shows the demo banner
Updated
- Exponential Velocity:
EXP_DEMO_MODE=1in the shared/etc/vc/envvarsno longer switches demo mode on. Every installation's Velocity server on a host reads that file, so a demo set up there switched demo mode on for every other site of the host too, and their administrators could no longer change or reset a password. A Velocity worker now refuses a demo mode value the engine took from an envvars file of a configuration directory outside the installation, keeps demo mode off and recordsdemo/refused ...once per process invar/log/security.log;exp:velocity startno longer passes the shared file'sEXP_DEMO_MODE. A tree inside the installation (var/vc/qbix/etc) still counts as the installation's own. PHP-FPM, Apache, LiteSpeed, FrankenPHP and the console are unchanged. Demo mode. - Demo mode is recorded once per PHP-FPM, mod_php, LiteSpeed or built-in server process (an empty marker file per
process in the cache directory,
<cache>/demo-mode/<pid>-<start time>), no longer on every request: such a process forgets PHP's statics between requests, so a demo wrote ademo/modeline tosecurity.logand asystem.demo.modeaudit event for every page. - The system card (
exp:info, Setup > System information) reads Velocity's demo mode from the installation's site envvars, and showsrefused (shared envvars)with a warning when the shared file still has it. - The demo mode pages, the Velocity pages, the environment variable reference, the hosting runbook and recipe and the demo mode examples, in English and German, name the site envvars as the Velocity place for demo mode.
Added
- The site envvars of an installation,
/etc/vc/sites-available/<site>.envvarsbeside its site file:exp:velocity startreads it for that installation's server only (after the shared file, so it wins; the same rules as envvars), andexp:velocity layoutlists it on the linesite envvars. The engine loads only*.confthere. - admin3 run as its own design shows the demo mode banner, at the top of its own main area
(
design/admin3/templates/page_mainarea.tpl, outside every cache-block); with admin4 first, admin4's main area draws it, so it shows once whatever the order of the designs.
Upgrading
- A public demo served by Velocity with
EXP_DEMO_MODE=1in/etc/vc/envvarshas demo mode off after the restart that brings this update. Move the line to the demo's/etc/vc/sites-available/<site>.envvars(<site>:./console exp:velocity layout) and remove it from the shared file before restarting. Updating by copying files: clear the template-override cache for the new admin3 template.
Tests
expDemoModeTestDM-13: a Velocity value from a shared envvars file is not taken; one from a tree inside the installation, the start environment or the site envvars is; outside Velocity nothing changes. DM-09 covers admin3.expVelocityConfigLayoutTest: the shared envvars never passesEXP_DEMO_MODE, the site envvars does and wins, and another installation's site file is never read.expSysCardProvidersTest: shared file refused, site file on.
2026-10-23: the Anonymous role of a new installation lets visitors register
Fixed
- A new installation takes
user/registerout ofPolicyOmitList[](since 12 October), but until the base data carried User / Register (19 October) no site package granted it, so an installation made in between refused every registration. The setup wizard and the kickstarter now check, after the site package's roles, that the Anonymous role hasuser/registerand add it when it is missing (expAnonymousPublicPolicies::ensure()inkernel/setup/steps/ezstep_create_sites.php), for every site package, base data and database engine.
Added
bin/php/addanonymouspublicpolicies.php(--dry-run,--only,--skip,--role): gives the Anonymous role the public policies it lacks,user/register,rss/feed, and where the module is installedtags/viewand the newsletter'ssubscribe,configure,unsubscribeandarchive; a policy the role has in any form is left alone, a second run adds nothing, the role cache is expired and the result checked (PASS/FAIL).kernel/classes/expanonymouspublicpolicies.php(expAnonymousPublicPolicies): the list, the plan and the grant.- Upgrade note with the policies a public site gives the Anonymous role and where each comes from.
Tests
expAnonymousPublicPoliciesTest(without a database): the plan for the old and the current base data, policies of extensions only where their module exists, existing policies in any form left alone,--onlyand--skip.
2026-10-23: the role and policy template operators answer for the Administrator, and every documented example works
Updated
has_policyandhas_policy_by_user(role and policy template operators) count a policy for every module (*, the Administrator role's) or for every function of a module (content/*), as the kernel's access check does. Before,eZRole::hasPolicy()was asked, which compares names only, sohas_policy( 'content', 'edit' )was false for the admin user and the matching code meant for*was never reached.has_role_by_useranswers false when it gets something that is no user object (a number, an empty variable), ashas_policy_by_useralready did; before, the template stopped with "Call to a member function attribute() on int".- A
user_idpassed as a string ('0',$node.object.owner_id) is read as a number, so'0'also means the current user. - The class comment and the documentation show only forms that work:
has_role_by_user_id( 42, 1 )instead of42|has_role_by_user_id( 1 )(the piped value is ignored, so that form asked about user 1 and role 0), and thematch_allargument by position instead of a named argument the template language does not have. The English and German feature, help and designer pages say how*policies count and drop the note that the registration was still to come.
Tests
eZTemplateRoleOperatorLiveTest(installation's database; skipped without one): a throwaway user in a throwaway group with two throwaway roles, the admin user and the anonymous user; every operator and every example of the feature page, rendered interpreted and compiled, with the same output both ways. Everything it creates is removed again.eZTemplateRoleOperatorRegistrationTest(no database):has_role_by_user( 42, 1 )andhas_policy_by_user( 42, 'content', 'read' )render false without an error.
2026-10-23: every new installation requires two-step sign-in of administrators and editors, and has imports
Updated
- The setup wizard, the kickstarter and
exp:installgive every new installation, whatever its site package, what the maintained packages (sevenx_multisite, sevenx_site, cjw_multisite and their_cleancopies) set up themselves (expInstallDefaultExtensions), each part only when the extension is in the installation and the package left it out:- two-step sign-in (sevenx_authentication_2fa): switched on globally, its login handler before
standard, andsettings/override/sevenxauthentication2fa.ini.append.phpwithEnforce2FA=enabledfor the groups Administrator users and Editors (EnforcedGroups[], by remote id),DefaultMethod=totp,RecoveryCodes=10, no exemptions, no grace period and aSecretKeymade for the installation; the user class gets the "Two-step sign-in" field, and the roles Member, Editor and Partner the policiesuser2fa/setupanduser2fa/verify. Members of other groups choose for themselves. - imports (sqliimport 2.0): switched on in the administration siteaccess, after
expuiandexpqueuewhen they are not global, with its tables and those of expqueue, and its Imports tab after Export where the package wrote a tab list. The editor siteaccess is made without it. Its schedules need the cron partsexpqueueandsqliimportevery minute with the admin siteaccess (php runcronjobs.php -q -s admin expqueue, andsqliimport). - ezodf stays in the wizard's own list (7.0: no tables; its converter only where LibreOffice is installed).
- two-step sign-in (sevenx_authentication_2fa): switched on globally, its login handler before
composer.jsonrequiresse7enxweb/sqliimport~2.0.0andse7enxweb/ezodf~7.0.0.- The demo rebuild scripts (
doc/eng-us/examples/demo-mode/) switch two-step sign-in off after the installation, because a public demo's visitors share one administrator account (DEMO_ENFORCE_2FA=1keeps it on);crontab-demoruns the queue and the imports every minute.
Tests
expInstallDefaultExtensionsTest(without a database): the policy, a package with and without its own two-factor settings, the admin's access extensions with and without global dependencies, the editor without imports, the position of the Imports tab, and which policies grant the second step.
2026-10-23: editors of a new installation can hide, restore from the trash and multi-upload
Fixed
- The site packages replace the Editor role's
content/*with single functions and left outcontent/hide,content/restoreandcontent/reverserelatedlist, and none granted ezmultiupload: an editor could remove an object to the trash but not get it back, could not hide or reveal anything, could not open the list of what uses an object, and the media library's multi-upload was refused. The setup wizard and the kickstarter now check, after the site package's roles, that the Editor role has them and add what is missing (expEditorPolicies::ensure()inkernel/setup/steps/ezstep_create_sites.php); alsouser/password,user/selfeditandnotification/usewhen no other role of the Editors group has them (plain_site, which assigns no Member role). - The installers of
sevenx_multisite,sevenx_multisite_clean,sevenx_site,sevenx_site_cleanandcjw_multisitegrant the four policies themselves and let editors creategeo_articleandvideobelow a folder.
Added
bin/php/addeditorpolicies.php(--dry-run,--only,--skip,--role): gives the Editor role of an existing installation the everyday policies it lacks; a policy the role, or another role of the same groups, has in any form is left alone, a second run adds nothing, the role cache is expired and the result checked (PASS/FAIL).WARNlines name what the role holds that an editor role should not (emptying everybody's trash, the installation's languages, URL aliases and wildcards, everybody's content jobs, the administration modules); nothing is removed.kernel/classes/expeditorpolicies.php(expEditorPolicies): the list, the plan, the grant and the over-grant report.- Upgrade note with what an editor could not do, what each package's Editor role holds, and what stays with administrators.
Tests
expEditorPoliciesTest(without a database): the plan for a package's Editor role with and without the Member role alongside, the base data'scontent/*, extension policies only where the module exists, existing policies in any form left alone,--onlyand--skip, and the over-grant report.
2026-10-23: editors get the image editor
Fixed
- The image editor answered "module not found" (kernel error 20) on a development installation for every user: the installation had
switched ezie off in favour of expimageeditor (root pull request #302), so
ezie/*addresses no longer exist; the edit form offers expimageeditor's Edit image in its place. Editors saw no image editor at all, since the button is drawn only for a user withexpimageeditor/editand no role gave it to them.expEditorPoliciesnow plansexpimageeditor/editfor the Editor role where the module exists: the setup wizard and the kickstarter add it, andphp bin/php/addeditorpolicies.php --apply --only=expimageeditor/editadds it on an installation that exists. The save view still checks that the draft is the user's own and that the user may edit the object.
Tests
expEditorPoliciesTest::testImageEditorWhereTheModuleExists(no database).
2026-10-23: editors manage the site's members, with every action in the audit log
Added
- Member management (
settings/membermanagement.ini,kernel/classes/expmembermanagement.php): rules say which user groups manage the accounts of which others, with which policy set, in which role; protected groups are never managed through a rule. The default gives the Editors group the role "Member management" over Members: create, edit (name, e-mail, login, password), enable and disable, move and remove members, with the Users tab; never the accounts of Administrator users or Editors, never roles, sections or setup. The setup wizard and the kickstarter apply the rules after the site package's roles (kernel/setup/steps/ezstep_create_sites.php). bin/php/addmembermanagement.php(--dry-run,--rule): applies the rules on an installation that exists; creates the role when missing, adds only what the role lacks, assigns it to the manager groups, checks the result (PASS/FAIL).- Audit events
access.user.change(the fields of an account that changed, before and after, never the account attribute; login, e-mail and password keep their own events) andaccess.user.move(an account moved to another group). Refusals (access.permission.refused) name the account a refused view was about (id and login). - Member management: the default, the settings, the guard, the audit events, variations, the upgrade.
Changed
- The access check of an account with a location below a protected group: an allowed edit, move, remove, location
change, hide, translate or version removal holds only when each of those locations allows it on its own
(
eZContentObject::checkAccess(),eZContentObjectTreeNode::checkAccess()); a Subtree policy on Members no longer reaches an administrator who also has a location there. A managed group itself (Members) needs the function at its parent, so a manager cannot rename, move or remove the whole group. access.permission.refusednames the object of a refusedcontent/action(a move, a removal) from the form.user/setting(enable, disable, concurrent logins) also asks forcontent/editof the account ([MemberGuardSettings] SettingRequiresEdit=enabled):user/preferencesalone, a function without limitations, reached every account of the installation.
Tests
expMemberManagementTest(no database): the policy line format, the default plan, what a manager must not get (wildcards, administration modules, unknown classes, own Subtree/Node), rules that reach a protected group or the manager's own group, a second run that adds nothing, and the shipped settings.
2026-10-23: the legacy admin of a Platform Legacy project without script, toolbar and icon errors
Added
site.ini [ExtensionSettings] RequiredExtensions[](shipped:expui) andeZExtension::requiredExtensions(): the extensions the kernel's own designs need. Each one that is installed is active in every siteaccess, ahead ofActiveExtensions[], whatever an override sets there; one that is not installed is left out without an error. The admin designs' sub-items table, relation search, uploads and date fields request theexp::script packs of Exponential UI (exp::core::shared,exp::io,exp::datatable...), which the kernel package installs. A Platform Legacy project resetsActiveExtensions[]in its own override withoutexpui, and ezjscore logged "Not a valid ezjscServer function" for each pack on every legacy admin page. Nothing is to be changed in a project: the kernel activates what it requires, and a project on 6.0.14 (which has neither) gets no "does not exist" error from an entry it does not need.eZExtension::extensionLists()counts them as active in every siteaccess.
Updated
{tool_bar name=...}draws no tools, without an error, for a toolbar the siteaccess does not configure. A siteaccess that only falls back to the admin design (the front siteaccess of a Platform Legacy project rendering a legacy 404) logged "Undefined group: 'Toolbar_admin_right' in toolbar.ini" and the same foradmin_developeron each page.- The icon operators (
class_icon,classgroup_icon,mimetype_icon,icon,flag_icon,icon_info) look upshare/iconsunder the legacy root wheneZSys::siteDir()is no directory. Under a Symfony front controller (the Legacy Bridge'sweb/app.phporpublic/index.php) it is<web dir>/app.php/: every theme was skipped, each icon was logged as "Missing icon file" and pointed atshare/icons/crystal/<size name>/mimetypes/empty.png, a 404.
Tests
eZExtensionRequiredExtensionsTest(no database): an installed required extension is active ahead of a resetActiveExtensions[], once, and one that is not installed is left out without an error.eZWordToImageOperatorIconRootDirectoryTest(no database): with the site directory of a Symfony front controller the classic admin's icons resolve to their crystal-admin files, without an error.eZTemplateToolbarUnconfiguredTest(no database): a toolbar without a[Toolbar_<name>]group renders empty and logs no error, interpreted and compiled.
2026-10-23: change password at next sign-in
An administrator who creates an account with a temporary password can make the user replace it: after the next
sign-in the user is sent to user/password before any other page, on every siteaccess, until a new password is set
(feature page,
upgrade note).
Added
- The checkbox Change password at next sign-in on the user account edit form (
design/standardanddesign/admin4), ticked by default when an administrator creates an account for someone else, never shown on one's own account or on self-registration. expPasswordChangeRequired(kernel/classes/user/exppasswordchangerequired.php): the mark is the user preferenceexp_password_change_required(ezpreferences), so no schema change and no update script on any engine. The check runs before every view inezpKernelWeb::dispatchLoop();site.ini [PasswordChangeRequiredSettings] ExemptViews[]keepsuser/password,user/logoutand the two-step sign-in (user2fa) open. Two-step sign-in comes first.- The REST interface answers
403{"error":"password_change_required"}for a marked account (expRestPasswordChangeRequiredStatus); WebDAV and SOAP refuse its sign-in. - Console command
exp:user-password-change-required status|set|clear --login=<login>|--user-id=<id> [--json]. - Audit events
access.user.password.change_required.set,.clearand.redirect. - The texts in the complete kernel catalogues (German, Spanish for Spain and Mexico, French, Hebrew, Arabic, Ukrainian, Latin, Klingon) and the source catalogue.
Updated
user/passwordclears the mark after a change that went through and shows why the user is there ($password_change_required;design/standard,design/admin,design/admin4).user/preferencesrefuses to write the preferenceexp_password_change_required.- Demo mode (
EXP_DEMO_MODE=1): the mark is ignored for administrators, whose passwords cannot change there, so it never locks them out.
Tests
expPasswordChangeRequiredTest(without a database): the mark's values, the decision (demo mode never locks an administrator out, a non-administrator still is sent), the views that stay open, the checkbox's default and who may choose it, the posted form and the draft, the reserved preference, the REST answer and its wiring, the kernel hook and its place, the audit names, the templates and the catalogues.
2026-10-23: Remember me is on by default and lasts the whole sign-in
Updated
- iOS Safari and Chrome drop cookies that end with the browser session on their own, and signed users out with them.
site.ini [Session] RememberMeTimeoutis now 31104000 (360 days, before: empty, which hid the box) and the Remember me box of the login forms (design/standard,design/admin,design/admin4) is checked by default.SessionTimeoutis 31104000 as well (before: 259200, three days), so the server keeps an idle session as long as the cookie can bring it back. A site that wants the old behaviour sets both insettings/override/site.ini.append.php. - Remember me works when a session already runs at sign-in, which is the usual case:
eZSession::setCookieLifetime()cannot change the cookie of a running session, so the box had no effect there. The login view now callseZSession::rememberCookieLifetime(), which keeps the lifetime in the session and sends the session cookie again with it, replacing the one of the response.eZSession::regenerate()sends every later new session id with the same lifetime, also on a later request: the code step of a two-factor sign-in no longer turns the cookie back into one that ends with the browser. The cookie sent with every response (CookieAlwaysAddToHttpResponse) and theis_logged_incookie last as long. The same code runs under Apache (PHP-FPM) and Velocity. A sign-in with the box cleared forgets a lifetime remembered before. - With PHP's file sessions (the default handler) the session directory holds about 360 days of sessions instead of three; each file is a few kilobytes. The Sessions guide says what the defaults are.
Tests
eZSessionRememberMeTest(no database): the lifetime kept in and forgotten from the session, the cookies that last as long, a persistent worker's next request not inheriting it, the session cookie replaced rather than sent twice, the new id ofregenerate()sent with it, the defaults and the checked box of every kernel login form.
2026-10-24: the installers write the cron job of the queue and the imports
Added
- New installations activate expqueue and sqliimport (and expai in the administration), but nothing ran
runcronjobs.phpfor them or for the kernel's parts: queued jobs, scheduled imports, notifications and the search index waited until somebody wrote a crontab by hand. The setup wizard, the kickstarter andexp:installnow write the cron job in their last step (eZStepFinal,kernel/classes/expcrontabsetup.php): one line per cronjob part ofcronjob.ini [CrontabSettings] Parts[]that the installation defines (global,frequent,infrequent, andexpqueue,sqliimport,expaiwhere their extension is active), with the PHP binary, the admin siteaccess forAdminParts[]and the schedules of[AdminSettings] CrontabSchedule_<part>(the queue and the imports every minute). The lines go into the crontab of the system user that owns the installation (its own crontab, orcrontab -u <owner>from root), never into root's crontab for a site, between# BEGIN Exponential cronjobs: <root>and# END Exponential cronjobs: <root>: a second run replaces the block or writes nothing, every other line stays as it was, and a part that a line outside the block runs already is left out. Without a crontab (a container, no crontab command, no permission, another user) the installation finishes and the installer prints the lines to add; the wizard shows them on its last page. [CrontabSettings] Install=disabled(orexp:install --no-crontab,EXP_INSTALL_CRONTAB=0) switches it off: the lines are printed instead.bin/php/installcrontab.php(--show,--add,--remove,--dry-run,--admin-siteaccess): the same on an installation that exists, with the lines that would go and come, and PASS/FAIL.runcronjobs.phprecords each part it starts and finishes invar/log/cronjob-lastrun.json. The dashboard has the block Cronjobs (dashboard.ini [DashboardBlock_cronjobs], forsetup/managecronjobs, fetchfetch( 'setup', 'cronjob_status' )): each part, its schedule, its last start, and a warning when no part has run yet or one is late (no start for twice its interval and five minutes, at least an hour).exp:syscardreads the record too. Upgrade note.
Tests
expCrontabSetupTest(no database, no crontab: the crontab command is a callable): the lines and how Setup > Cronjobs reads them back, the block appended, replaced in place and taken out with every other line kept, other installations' blocks, hand-written lines found and their parts left out, whose crontab (owner, root with-u, never root's or a third user's), the fallback without a crontab command and on a refused one, dry runs that write nothing, the parts of an installation's files (active extensions only, admin siteaccess, schedules), the switch, and the run record and its on time, late and never states.
2026-10-24: plain_site's Editor role holds single content functions instead of content/*
Changed
plain_sitekept the base data'scontent/*in its Editor role, which also let editors empty everybody's trash, change the installation's languages, its URL aliases and wildcards, and see everybody's content jobs (bin/php/addeditorpolicies.phpreported them asWARNlines). The setup step now replaces acontent/*without limitations by the content functions the other site packages give editors (create,edit,read,manage_locations,diff,versionread,versionremove,remove,translate,bookmark,pendinglist,dashboard,view_embed,hide,restore,reverserelatedlist), without limitations, so the role's assignment still limits them to Content and Media (expEditorPolicies::narrowPlan(),narrow()). The base data is unchanged; the other packages removecontent/*themselves and are not touched; a limitedcontent/*is kept.bin/php/addeditorpolicies.phpdoes the same on an installation that exists:--dry-runlistscontent/*to be removed and each function to be added, the run checks the stored role, a second run changes nothing.--keep-content-wildcardkeepscontent/*; with--onlyit is kept as well. Upgrade note.
Tests
expEditorPoliciesTest(no database): the base data'scontent/*narrowed with no over-grant left, the narrowed list covering every content function of the packages and of the everyday policies and no over-grant, the packages' role untouched, a limitedcontent/*kept, functions held already and skipped ones not added.
2026-10-24: the second step on the other sign-in paths
Two-step sign-in (sevenx_authentication_2fa) asks for its second step in the web login form only. The REST interface
with basic authentication, WebDAV, SOAP and the expservices login check a password themselves, so an account the site
requires a second step of (Enforce2FA, e.g. Administrator users and Editors) signs in there with its password
alone. The kernel now asks the two-step extension on those paths too
(feature page), and a site can opt each path in.
By default no path is guarded: their clients cannot answer a second step, so every path signs in as before. The
extension's side is in sevenx_authentication_2fa 1.0.17: [OtherSignInPaths] Guard[] (empty by default; one line
per path: expservices, webdav, rest, soap), and app passwords ([AppPasswords]) for the opted-in ones of
REST, WebDAV and SOAP.
Added
expSignInSecondFactor(kernel/classes/user/expsigninsecondfactor.php) and the interfaceexpSignInSecondFactorHandler: after a right password the kernel asks the classes ofsite.ini [SignInSecondFactorSettings] Handlers[]whether a password alone is enough, and before it whether the secret is an app password of the account (which signs in for that request only, without a session). Without a handler, or with the extension's handler and no path in itsGuard[](the default), nothing changes.CodeHeader(X-Exp-OTP) names the header of an authenticator code.- The answers of an opted-in path, in each protocol's own format: REST
401withWWW-Authenticateand{"error":"second_factor_required"|"second_factor_invalid","error_description":...},403second_factor_setup_required(expRestSecondFactorRequiredStatus); WebDAV401with the reason inX-Exp-Sign-In-Refused; SOAP401/403withWWW-Authenticateand a SOAP fault; the expservices login{"ok":false,"error":{"code":401,...,"reason":...}}. - Audit events
access.user.second_factor.refused,access.user.app_password.create,.revokeand.use, with their labels in the complete kernel catalogues.
Updated
expRestPasswordAuthFiltertries an app password before the password, so it never counts as a failed sign-in;ezpRestAuthConfigurationchecks a password alone before the request is signed in, never a personal API key, an OAuth token or an app password. The OAuth token end point still has no password grant.- WebDAV (
eZWebDAVContentBackendAuth) and SOAP (soap.php) take an app password first and check the password sign-in after it. - expservices:
expsession::logintakes the fieldotp(or the headerX-Exp-OTP) and, once a site guards it, refuses a password alone for such an account; app passwords are never taken there, because the login makes a session the website accepts. It also refuses an account marked "change password at next sign-in" (403, reasonpassword_change_required), like the other paths.expServiceExceptionand the error envelope carry an optionalreason.
Tests
expSignInSecondFactorTest(no database): the refusal and its status, the decision with handlers of the test, the kept refusal of a request, app passwords never for a disabled account, the code of a request, the answers of every protocol (REST status, WWW-Authenticate, SOAP fault), the wiring of every path and its order, the audit names, verbs, labels and documentation, the settings and the autoload.expAuditRecordTest: the catalogue has 180 names (130 on by default).
2026-10-24: a new installation after a failed one leaves maintenance mode
Fixed
- A kickstarter run that fails leaves maintenance mode on on purpose, so the half-installed site is not shown. The next
run counted that window as someone else's ("Maintenance mode was already on: kept as it is") and left it on after a
successful installation, so the new site kept answering with the maintenance page until
php bin/php/maintenance.php off.expMaintenance::beginRun()now takes over a window that an earlier setup run left (reasonsetup, a run id, no wizard token) and the successful run switches it off. A window opened by hand, and the setup wizard's own window, are still kept as they are.
Tests
MaintenanceRunTest::testAFailedRunsWindowIsTakenOverAndEndedByTheNextRunandtestAWizardSessionsWindowIsNotTakenOver(no database).
2026-10-24: the help dashboard answers in about a second
Fixed
- The help dashboard (
exphelp/dashboard) took 16 seconds on Apache and ran into Velocity's 30 second limit (504) on a site with 5,700 help objects: every request loaded the whole help tree with its data maps (expHelpContentRepository::load(), about 10 seconds) and then each object again to askcanRead()andcanEdit()(another 6), only to show six topics, a count and the manuals. The search for help topics, the topic view and the manual list paid the same, and the context help's list of released topics paid it every five minutes on whatever admin page came first. On the same data the dashboard's part now takes 0.5 to 2 seconds instead of 16 to 35, with the same content for every user.
Added
- The rows of help maintained as content are kept in the cache (
var/<site>/cache/exphelp/content/repository-*.cache) and checked against a stamp of the help subtree on every request (a few sums over the locations, objects and states below the help root); after a publish, translation, move, removal, state or section change only the objects that changed are read again.expHelpContentRepository::index()gives the rows without the topics' texts,load()with them,accessData()what the permissions are decided with. A cache itemexphelpcontent(site.ini [Cache_exphelpcontent], tagexphelp) clears it. expHelpContentAccess: who may see a help object, decided from the cached rows against the user'scontent/readandcontent/editpolicies aseZContentObject::checkAccess()does (Class, ParentClass, ParentDepth, Section, User_Section, Language, Owner, ParentOwner, State, StateGroup_*, Node, Subtree, User_Subtree); for a Group limitation, an extension's limitation, the anonymous "own session" owner or the member management guard the object is asked, as before.expHelpContentProvider::newestTopics()(the dashboard's list and count),topics( false )(without the texts),searchWordSets();expHelpContentRepository::searchWords()keeps the search words of every topic insearch-*.cache, computed again only for the topics whose texts changed (a search for help topics took 25 seconds)../console exp:help:content cache -s <siteaccess>: fills the cache after a deploy or after clearing it.
Updated
- Reading every help object again (an empty cache) asks for the attributes one version and language at a time with the attribute table first: the kernel's single query made SQLite go through every attribute of the help classes for each chunk (30 seconds for the help tree; about 1 second this way). The state of each object comes from one query for all of them.
- The list of released help_ids of the context help is kept in a static property, so a persistent Velocity worker no longer keeps it for its whole life.
Operator note
- After the deploy, run
./console exp:help:content cache -s <siteaccess>once for each siteaccess that shows the help (the cache is per language list), so the first visitor does not read the help tree.
Tests
expHelpContentAccessTest(no database): access words; Section, Class, State, StateGroup_, Subtree and User_Subtree, Node with Subtree, Owner, Language and ParentDepth ascheckAccess()compares them; what only the object can tell;decide();newestTopics()without loading an object; the cache file; the search words.
2026-10-24: a site package keeps the newer installed extensions
Fixed
- Installing a site package replaced every extension it carries, without asking, whatever was installed:
exp:install --package=ezwebin_sitedeleted the Composer copies of ezwebin, ezstarrating, ezwt and ezgmaplocation (6.x) and put the copies of 2012 from the package server in their place, and the old ezstarrating then stopped the admin's full view with a fatal error.eZExtensionPackageHandler::install()now compares the two copies (extension.xml<metadata><version>, elseezinfo.php'Version') and keeps the installed one unless the package's is newer; an installed copy without a release number stays when Composer or git manages it. A package without a release number (dev-master,//autogentag//) never replaces a numbered copy. The kept extension is activated as before and named invar/log/setup.logwith both versions. Doc:doc/eng-us/bc/6.0/package-install-keeps-newer-extensions.md.
Tests
ExtensionKeepNewerTest(no database): release numbers and what is not one, the version read fromextension.xmlthenezinfo.php, the keep-newer rule, Composer (installed.jsonformat 1 and 2) and git detection, and the published 6.0.15 extension packages against the Composer copies.
2026-10-24: the legacy site packages get the tables of the extensions they switch on
Fixed
- ezwebin_site, ezflow_site and ezdemo_site switch on extensions such as eztags and xrowextract without creating
their tables, so the setup log's checks ended in "6 of 144 declared tables missing" and "The site is installed but
not usable yet". After the site package's post-install, the setup now creates the tables that every active
extension's
share/db_schema.dbadeclares and the database lacks (expInstallDefaultExtensions::ensureExtensionTables(); nothing is dropped or changed, an installation that has them all is left as it is).
2026-10-24: WebDAV on PHP 8
Fixed
- PROPFIND after sign-in failed with a TypeError: the WebDAV component's lock plugin counted a list that is null
when every property is found (
count(null)since PHP 8.0). The component comes from the se7enxweb fork from 1.2.1 on (se7enxweb/zetacomponents-webdav, replacingzetacomponents/webdav1.2), which also fixes the lock discovery of allprop responses, a LOCK of a missing resource that recursed without end, display names cut at "&", an empty PROPFIND body (now allprop, as RFC 4918 says) and the deprecations of PHP 8.1 to 8.5. - WebDAV stores locks (LOCK, UNLOCK, a write without the lock token refused with 423) and reads back the files it
wrote on a siteaccess with a PathPrefix; names with spaces can be locked (
expWebDAVPathFactory).
Tests
WebDAVPathsTest(no database); the fork'stests/php8_regression_test.php.
2026-10-24: an emptied help cache is filled in the background, never by a visitor
Updated
- A web request no longer reads every help object when the help content cache is missing or cannot be updated by
reading only what changed (
ezcache --clear-all,--clear-id=exphelpcontent,--clear-tag=exphelp, a changed help class or section, more than 300 changed objects): that took about 35 seconds, more than Velocity's 30 second limit. It startsphp bin/php/exphelpcontent.php cache -s <siteaccess>detached in the background, once (a marker file next to the cache), and answers at once without help content: the dashboard leaves out its help content part, the search finds no help topics, a topic or manual is not found, the context help keeps its last list. The next request after the command finished has it all again. The same for a search that would compute the search words of every topic.expHelpContentRepository::isBuilding()andexpHelpContentProvider::isBuilding()tell a request that it is in that state. exp:velocity deployfills the help content cache after the cache clears, as one PASS/FAIL step per siteaccess that shows the help (exphelp.ini [ContentSettings] CacheSiteAccessList[]and the siteaccesses that have used the cache, recorded invar/<site>/cache/exphelp/content/siteaccesses.json); SKIP where help content is not installed../console exp:help:content cacheanswers SKIP (exit 0) where help content is not installed or switched off.- New settings
exphelp.ini [ContentSettings] CachePhpBinary(the command line PHP for the background command; empty: found next to the running PHP) andCacheSiteAccessList[]. - The generated pages of the command (
exp:help:content, English and German) and of[ContentSettings]list the actioncacheand the new settings.
Tests
expHelpContentAccessTest: the background command line, the command line PHP, the siteaccesses the deploy fills, the marker that keeps a second build from starting.
2026-10-25: passkeys and "Verify it's you"
Guides: Passkeys, Verify it's you. Upgrade note: Passkeys and "Verify it's you".
Added
- Added: Passkeys (WebAuthn) for user accounts: added, named, renamed and removed on My account > Passkeys
(
user/passkeys), used on the web sign-in form ofdesign/standard,design/adminand admin4 (a button and the browser's passkey autofill). The kernel checks WebAuthn itself (kernel/classes/webauthn: challenge per request kept in the session, origin, relying party id, user presence and verification, ES256/ES384/ES512/RS256/RS384/RS512/Ed25519 signatures, the signature counter; attestationnone). New tableexppasskey(share/db_schema.dba, the kernel schemas, the 6.0.0 to 6.0.15 update files,update/common/scripts/6.0/createpasskeytable.php), new settingssite.ini [PasskeySettings]. A passkey sign-in is the whole sign-in; REST, SOAP, WebDAV and the expservices login are unchanged. - Added: "Verify it's you": before the actions of five groups (own account security, users and roles, system settings
and setup actions, data exports: on; removing and publishing content: off) the signed-in user confirms with the
password, a passkey or an authenticator code, valid for
[ReauthSettings] Minutes(10). Only the web pages ask. Groups are switched on Setup > Verify it's you (setup/reauth), written tosettings/override/site.ini.append.php. On Your devices (user/sessions), switching off one's own "New sign-in" mail asks (account group) and signing another user out asks (users group); signing out one's own other devices does not. - Added: audit events
access.passkey.add,.rename,.remove,.use,.use.failedandaccess.session.reauth.ask;access.session.reauthand.failedare raised by "Verify it's you" as well.
Updated
- Updated: "Verify it's you" counts only a file actually chosen as an upload (
expReauth::hasUploads(): an entry of$_FILES, nested arrays included, whose error is notUPLOAD_ERR_NO_FILE). A form that merely has file inputs, such as the edit form of a user account with its image, counted as one: an editor creating a member account was told that the chosen files had to be chosen again, though none were. Such a form is now replayed whole without the notice, and the account is published after the answer as it was filled in. The rulecontent/edit other(users group) stays: creating or editing another user's account is user management and asks once. - Updated: after a real upload the form now opens again after the answer, as the guide always said. The check looked
at the answer's own request, which never carries files, so the POST was replayed without the file and, unless the
autosave had stored the file in the draft first, published the content without it; the page now sends the mark
ExpReauthReplayFileswith the replayed fields.
Tests
expWebAuthnTest(recorded vectors of real authenticators and of Chromium's virtual authenticator, each check refused field by field),expPasskeyTest,PasskeySchemaSqlTest,expReauthRulesTest; no database.expReauthRulesTestRA-12: file inputs left empty are no upload, a chosen file (or one that did not arrive) is one, also for inputs named as arrays.
2026-10-25: Your devices, the signed-in sessions of each user
Added
- Your devices (
user/sessions): every sign-in is a session of its user, listed with the browser, system and kind of device (from the User-Agent header), the approximate address (IPv4 /24, IPv6 /48), the sign-in method (password, password and two-step code, REST, SOAP, WebDAV, single sign-on), Remember me, when it started and was last used; this device first and marked. Sign out this session and Sign out all other sessions end sessions for good: the session is signed out at its next request on every session handler, also when it stays signed in with Remember me (360 days, unchanged). Recently ended sessions are listed with how they ended. user/sessions/<user id>for administrators with the new policy function user/sessions: a user's sessions with the full address, one signed out or all ("Sign out everywhere"). The account page (user/edit) has the card Your devices (own) or Sessions (another user); the profile page of the public site links to the page.- New sign-in to your account: an e-mail (category security) when an account signs in from a browser, system and
device kind it has not used within
RetentionDays; on by default, each user turns it off on Your devices (the E-mail preferences page points there). Optionally a note to the administrators (NewDeviceAdminMail). site.ini [UserSessionSettings]:Tracking,TouchInterval,RetentionDays,NewDeviceMail,NewDeviceAdminMail,AdminMailReceivers[],FirstSignInMail.- Table
expuser_sessioninshare/db_schema.dba, the MySQL, PostgreSQL and SQLite kernel schemas and the 6.0.0 to 6.0.15 update files (created only when missing);update/common/scripts/6.0/createusersessiontable.phpcreates it on any engine. Only the SHA-256 of a random token kept in the session is stored, never the session id. - Cronjob
usersessions.php(partinfrequent, and partusersessions): ends sessions not used for[Session] SessionTimeout, removes ended ones afterRetentionDaysand those of removed users. - Audit event
access.session.new_device;access.session.revokealso for sessions signed out from the page (scope, by user or admin, the rows);access.session.loginandaccess.session.logoutcarry the session's row id. The catalogue has 181 names. - Documentation: Your devices: signed-in sessions per user, the guide Your devices and sessions, in English and German.
Updated
- A password change with
[PasswordSettings] EndOtherSessionsalso ends the user's other session rows (reasonpassword_change).
Tests
expUserSessionTest: User-Agent parsing, approximate addresses, the new-device rule, the sign-in method, the table in every schema and update file (the SQLite update twice), revoke one, others and all on SQLite, retention and the page's list, the wiring.expUserAccountOverviewTest: the Sessions card.expAuditRecordTest: 181 names.
2026-10-25: administrators approve new editor accounts
Added
- The workflow event Approve user accounts (
event_expuserapprove, on content/publish before), modelled on the approval event: a user account whose new version places it in a group that requires approval waits until an approver accepts it in the collaboration system (an item of the handlerezapprove), and cannot sign in before. Its settings are the groups that require approval, the approving users and groups, the groups without approval and the e-mail to the approvers. Approvers never wait. - The approvers get the mail "New user account waiting for approval" with the one link to the approval item in the
administration (
[UserSettings] RegistrationAdminBaseURL); it carries no key and decides nothing. - After Approve or Deny the workflow continues at once: the account is published and signs in, or its version is a draft of its author again.
- Moving an account or a user group into a group that requires approval, or adding a location there, is refused for someone who would wait for the approval.
- Audit events
access.user.approval.request,.approve,.denyand.refused(on, channel access). - A new installation gets the workflow "Approve new editor accounts" (Editors wait for the Administrator users) and the
role "Editor account requests" (read the Editors group, create user accounts directly in it), assigned to Editors;
bin/php/adduserapproval.php(--dry-run,--required,--approvers,--requesters,--no-role) sets them up on an installation that exists and changes nothing on a second run. - Docs: technical note, feature page, guide, and their German counterparts.
Tests
expUserApprovalTest(no database);expAuditRecordTestcounts the four new events (184 names, 134 on by default).
2026-10-24: four kernel calls that did not work as written
Fixed
fetch( 'collaboration', 'tree_count' )namedeZContentFunctionCollection::fetchObjectTreeCount()(13 parameters) with 4 and ended in anArgumentCountError. It callseZCollaborationFunctionCollection::fetchObjectTreeCount()now, which answers a missing parent node with "not found" instead of a call on null.ezpContentPublishingQueue::initHooks()guarded itself withisset( $init )beforestatic $init = true;, which never held: everyadd()connected the INI hooks once more (three calls, each hook ran three times per signal). A static property guards it now.content/downloadannounced its event with an array with string keys, whichcall_user_func_array()passes as named arguments under PHP 8: a listener with other parameter names failed with "Unknown named parameter". The arguments are positional now (object ID, attribute ID), asezpEvent::notify()documents.- The object relation datatype (
ezobjectrelation) did not export its allowed classes to a package, so a class imported from a package allowed every class. They are written as<class-constraints>, the format ofezobjectrelationlist, and read back on import; a package without them keeps what the attribute had.
Tests
expKernelCallFixesTest(no database):tree_countreaches a method with one parameter per declared one and answers a missing node, the queue hooks run once after threeinit(), the download event's arguments.eZObjectRelationClassConstraintsTest(no database): export, import, and an older package.
2026-10-25: event arguments always reach listeners by position
Fixed
ezpEvent::notify()passes its arguments by position (array_values()): an extension that still announces an event with a hash, ascontent/downloaddid and thecontent/downloadview ofnxc_powercontentstill does, no longer stops a listener with other parameter names ("Unknown named parameter" under PHP 8).
Tests
expKernelCallFixesTestKC-03: a hash given tonotify()reaches the listener by position.
2026-10-24: an account without its ezuser_setting row is not enabled anywhere
Fixed
- Sign-in already refused an account without an
ezuser_settingrow (F-06 of the security hardening of August 2026), buteZUser::isEnabled()still answeredtruefor it, so a session, API key or OAuth token of such an account kept working.isEnabled()now agrees with the login: no row, not enabled. - The user hygiene and user deactivation pages (
expUserHygiene,expUserDeactivation) count such an account as disabled; the hygiene filter "disabled" lists it. eZUserOperationCollection::setSettings()creates the missing row, so enabling such an account in the administration repairs it; it did nothing before.
Upgrade notes
- Count the accounts concerned before updating; an empty result means nothing changes:
SELECT COUNT(*) FROM ezuser u LEFT JOIN ezuser_setting s ON s.user_id = u.contentobject_id WHERE s.user_id IS NULL.
Tests
eZUserIsEnabledWithoutSettingLiveTest(live installation, skipped without one): no row is not enabled, with a rowis_enableddecides,setSettings()creates the row.expUserHygieneTest: the account without a row is disabled. The live tests that create users (eZContentAccessForUserLiveTest,eZContentPermissionSQLOptimizationLiveTest,eZContentHistoryAccessLiveTest,eZContentObjectDraftAccessLiveTest,expOEDialogAccessTest) give them an enabled row;eZContentAccessForAnotherUserTestmarks its stand-in users enabled.
2026-10-25: an upgrade script creates the missing ezuser_setting rows
Added
update/common/scripts/6.0/createmissingusersettings.php(expUserSettingRepair): an account without itsezuser_settingrow is not enabled anywhere sinceeZUser::isEnabled()agrees with the sign-in, so its open session, API key or OAuth token stops working with the update. The script creates the rows of all such accounts on any engine: enabled, as these accounts counted everywhere but at the sign-in before, disabled for one waiting for its activation mail, or every one disabled with--disabled; an account whose content object is not published is left alone, rows that exist are never changed.--dry-runlists the ids first. Fresh installations and every way the kernel creates an account write the row, so only installations upgraded from old releases or filled by SQL are concerned.
Tests
expUserSettingRepairTest(no database): what each account gets,--disabled, unpublished accounts, the dry run.
2026-10-25: WebDAV checks what a request may do
Fixed
eZWebDAVContentBackendAuth::authorize()allowed every read before it looked at a policy; only the root (/, the list of sites) is read without a check now, everything below it needsuser/loginfor the siteaccess named by the first path element. A check compared with=instead of===($access = ACCESS_WRITE).- PUT, MKCOL, DELETE, COPY and MOVE answered 201/204 when the content layer refused them (no
content/edit,content/createorcontent/remove), so a user without the right lost changes silently. They answer 403 now. COPY or MOVE between two sites answers 403 instead of "no errors";performDelete()returnsarray()instead ofnullfor the first step of a MOVE (count(null)is a TypeError since PHP 8). - The site was matched by name prefix:
/site_admin/...ran as/site. It is matched as a whole path element. webdav.phpsets the authorizer for the root branch too.
Tests
expWebdavAuthorizeTest,expWebdavBackendRefusalTest(no database).
2026-10-25: WebDAV reads of a missing or unreadable file, and the permissions for administrators
Fixed
eZWebDAVContentBackend::getContentNodeData()read the node information before it checked the node: a GET of a path without a node ended in a call onfalse, and the details of a node the user may not read were fetched before it was refused. The node is checked first,content/readsecond, andgetResourceContents()answers a refused or missing file withfalsewithout reading an offset offalse.
Documentation
- WebDAV permissions for administrators (English,
German):
user/loginper siteaccess,content/readper node, 403 for refused writes, never "Verify it's you", a second sign-in step only withGuard[]=webdav. Proven on a SQLite installation: an unreadable folder 404, a readable one 207, a refused PUT, MKCOL and DELETE 403 with nothing changed, a refused COPY and MOVE 403 inside the 207 multistatus answer with nothing copied or moved, the administrator's MKCOL 201 and DELETE 204, an account without itsezuser_settingrow 401.
Tests
expWebdavReadRefusalTest(no database): a path without a node, a node the user may not read, the contents of both.
2026-10-24: the content job log line does not follow the locale
Fixed
expContentJobWorker::run()wrote its line per batch with%f, which followsLC_NUMERIC: with a German locale set inconfig.phpthe job log read "in 0,01 s, peak 30,0 MB". It uses%Fnow.
Tests
ContentJobEngineTest:testWorkerRunsToDoneEachItemOnce(CJ-12) failed with a German locale and passes. Every forked child now ends by SIGKILL in afinallyand in a shutdown function, and the parent waits at most 60 seconds: before, an exception in a child skipped its kill, the copy of the PHPUnit process ran the rest of the suite next to the parent, and its later kill ended runs at random places (exit 137).ContentJobEngineTest::testWorkerLogLineUsesADotUnderACommaLocale(CJ-12b) switchesLC_NUMERICto a German locale, runs the worker and checks every batch line carries a dot; it fails with%fand passes with%F. Skipped where no locale with a decimal comma is installed (CI's runners), so it guards the fix wherever one is.
2026-10-25: the database test group runs on PHPUnit 13, and the DFS cache generation works on PHP 8.1+
Fixed
eZDFSFileHandlerMySQLiBackend::_query()(dfsbackends/mysqli.php): with$reportError = falsea failed query returnsfalseagain instead of throwingmysqli_sql_exception(mysqli reports errors as exceptions by default since PHP 8.1)._startCacheGeneration()relies on the failedINSERT(duplicate key means "being generated"), sostartCacheGeneration()never returned the remaining generation time and a timed-out generation was never taken over._endCacheGeneration()rolls back and returnsfalsewhen itsSELECT ... FOR UPDATEfails, instead of aTypeErrorfrommysqli_num_rows( false )with the transaction left open.
Tests
- The tests of the
databasegroup, which had not been migrated to PHPUnit 13 (assertFileNotExists(),@dataProviderannotations,setMethods(),returnValue()), run again: 30 errors and 8 failures before. - Tests no longer expect the demo content or a fresh database per test: they create what they read, remove what they
created (also when they fail) and restore the INI settings and languages they change in
tearDown()(eZContentClassTest,eZURLAliasMLTest,eZRSSExportTestand others). The RSS fixtures name the locale and the site address through placeholders. ezpMobileDeviceDetectTestis declared once: the duplicate undertests/tests/kernel/private/is removed and itsisEnabled()cases are in thekernel/classestest.
2026-10-25: tests of the group database run on throwaway databases
Fixed
ezpLiveInstallationstarted the siteaccessadmin, which this distribution does not ship, so every live test was skipped ("Unable to connect to the database server") and the groupdatabasenever ran. The siteaccess is nowEXP_TEST_SITEACCESS, elseDefaultAccess.
Added
ezpThrowawayDatabase(tests/toolkit/ezpthrowawaydatabase.php): MySQL and MariaDB databasesexp_test_<pid>_<hex>for the tests. One seed per run gets the kernel schema and the clean data; each test class gets a copy made on the server and dropped when the next class starts, at the end of the run, and on a fatal error,SIGINTorSIGTERM. Only databases the process made itself are written to or dropped; connections are closed beforeDROPandlock_wait_timeoutis set; forked children never inherit the admin connection.- Live tests write only to a database with
testas a word of its name; otherwise to a throwaway database, and without one they are skipped with the siteaccess, the database and the reason. Other databases than MySQL and MariaDB skip with the reason;tests/runtests.php --db-per-test --dsn=…works as before. - Settings:
EXP_TEST_DB_HOST,EXP_TEST_DB_PORT,EXP_TEST_DB_SOCKET,EXP_TEST_DB_USER,EXP_TEST_DB_PASSWORD(default: the database settings of the siteaccess) andEXP_TEST_KEEP_DATABASES=1. The account needsCREATEandDROPonexp\_test\_%only, for exampleGRANT ALL ONexp_test_%.* TO 'exp_test'@'localhost'. See TESTING.md. Guard test:ezpThrowawayDatabaseGuardTest. ezpTestDatabaseHelper::create()andclean(), which drop every table of the database oftests/runtests.php --db-per-test --dsn=…, accept only a name withtest,testsortestdb(optionally numbered) as an underscore-separated word (testdb,exp_test_*), not any name containing "test": a demo site'sexpdemolatestor a reinstall target…pkgtestwas accepted before. For Oracle, wherecreate()empties the schema of the account, the account is checked (ezpTestDatabaseHelper::isSetUpDatabaseName(), TD-06).
2026-10-24: three unused Zeta Components are no longer required
Removed
zetacomponents/authentication-database-tieinandzetacomponents/mvc-authentication-tieinfromrequire,zetacomponents/php-generatorfromrequire-dev(composer.json, and the same entries incomposer.json.dist). No code of the kernel, of its designs or of the packages it requires uses a class of them; the two comments inkernel/private/rest/that name MvcAuthenticationTiein say where the kernel's own copy of that code came from.
Kept
zetacomponents/configurationstays inrequirefor now. 7x SQLI Import (se7enxweb/sqliimport ~2.0.0, required bycomposer.json) throws itsezcConfigurationNoConfigExceptionfromSQLIImportFactory::runImport()for a handler without its configuration section, in its releases up to 2.0.1; without the component that error path would end an import with "Class not found" instead of marking the item failed. The component goes once a release of 7x SQLI Import that throws its ownSQLIImportConfigExceptionis pinned. Its INI reader, which turns a quoted dimension key (key["..."]) into PHP code and runs it witheval()(src/ini/ini_parser.php), stays unused: the kernel reads its settings witheZINI, and nothing loadsezcConfigurationIniReader.tests/toolkit/extras/scripts/create-test-from-class.php, the only user ofzetacomponents/php-generator: it wrote a test skeleton whose constructor callsparent::__construct()without a name and thensetName(), which the PHPUnit versions the test suite runs on (10 and later) do not accept. No documentation, script or test calls it.
Upgrading
- Code of your own that uses one of the three removed components (
ezcAuthenticationDatabase*,ezcMvcAuthentication*,ezcPhpGenerator*) requires it in the project's owncomposer.json, e.g.composer require zetacomponents/php-generator.composer updateremoves them fromvendor/otherwise.
Tests
expComposerKernelRequirementsTest(no database): the three components are not required, no PHP file of the kernel, its libraries, designs, scripts or shipped extensions names one of their classes, andzetacomponents/configurationstays required as long as a required 7x SQLI Import still uses it.
2026-10-24: the setup wizard activates expimageeditor; ezie stays installed
expimageeditor (root pull request #302) replaces the image editor ezie in admin4.
Updated
- The setup wizard and the kickstarter activate
expimageeditorfor a new installation where they activatedezie(kernel/setup/steps/ezstep_create_sites.php; as before, only an extension that is present is activated, and only one of the two: both replace the same include of admin4's image edit template). - The admin4 hook for ezie (
content/datatype/edit/ezimage_editor.tpl, drawn only whenezmodule( 'ezie/prepare' )allows it) stays: a site that activates ezie keeps its Edit button in admin4. - The pages on ezie and expimageeditor say how ezie is installed and activated (English and German).
Kept
se7enxweb/ezieandzetacomponents/image-conversion(with its dependencyzetacomponents/image-analysis) stay in therequirelist ofcomposer.json(andezsystems/ezie-ls,zetacomponents/image-conversionincomposer.json.dist). Dropping them was proposed together with the wizard change, but thesevenx_multisitesite package activates ezie for the classic admin designs, which have no other image editor, and the released ezie versions do not declare the image conversion themselves: a new installation would have activated an extensioncomposer installno longer put there. The ImageMagick handler of image-conversion builds the file name of a composited image (the watermark) into its shell command withoutescapeshellarg()(ezcImageImagemagickBaseHandler::save()); ezie, its only user, composites only the watermark images of its own folder, resolved withrealpath(), so that name never comes from a request. The requirement can go once ezie declares the image conversion itself and the site packages activate expimageeditor.
Upgrading
- Nothing to do: ezie stays installed. To use expimageeditor in admin4:
ActiveExtensions[]=expimageeditorin place ofezie, thenphp bin/php/addeditorpolicies.php --apply --only=expimageeditor/editfor the Editor role (expimageeditor). - A project that builds its own
composer.jsoninstead of the root's requires ezie with the image conversion, since ezie does not declare it:composer require se7enxweb/ezie zetacomponents/image-conversion.
Tests
expComposerKernelRequirementsTest:composer.jsonrequires ezie and the image conversion while a site package of the distribution activates ezie, and the setup wizard activatesexpimageeditor, notezie.
2026-10-24: OAuth tokens stop working for disabled users, refresh tokens expire, no token in the address
Fixed
- A user disabled in the administration or locked by failed logins kept full REST access with an OAuth token issued
before, and kept getting new access tokens with the refresh token.
ezpOauthFilter::run()and both grants of the token endpoint now check the user (ezpOauthUtility::userMayUseToken(), the rule of personal API keys and password sign-in) and answer401orinvalid_grant. The grants refuse such a user with the description they give for a code or refresh token of another client, so the answer does not tell the token holder the account's state (ezpOauthUtility::checkGrantUser(); the reason goes to the debug output). - The token endpoint refused only a missing last parameter: a request without
client_idwent on toezpRestClient::fetchByClientId( null )with "Undefined array key" warnings. Every missing parameter is named now (ezpRestOauthTokenController::missingParams()). - A token endpoint parameter posted as a list (
client_id[]=...,refresh_token[]=...,code[]=...) reached the client and token lookups as an array and ended in an error page; it is refused asinvalid_request("Invalid client_id parameter.",ezpRestOauthTokenController::invalidParams()). Anoauth_token[]=...is no token, and a listcode_verifiercounts as none. - A client still sending
?oauth_token=whileAllowTokenInQueryis off is named in the debug output, so the401can be traced from the site's side.
Updated
- The client a code or refresh token was issued to is compared in constant time, and an empty client id never matches
(
ezpOauthUtility::clientMatches()). - Refresh tokens expire:
rest.ini [OAuthSettings] RefreshTokenTTL=2592000(30 days from the refresh that issued them,0= never). The issue time is derived from the access token's expiry, so no schema change. - The
oauth_tokenquery parameter is read only withrest.ini [OAuthSettings] AllowTokenInQuery=enabled(defaultdisabled); the form body parameter stays on (AllowTokenInBody=enabled). TheAuthorizationheader is always read.
Tests
ezpOauthGrantChecksTest(no database): disabled, locked and missing users and the description they are refused with, the client binding, the refresh token lifetime, every missing parameter named before any lookup.ezpOauthUtilityTokenTest: the token sources of the shipped settings, the query only withAllowTokenInQuery=enabled, header only.
2026-10-24: sendmail envelope sender, tip-a-friend sender, SMTP failures and random mail identifiers
Fixed
eZSendmailTransportpassed the sender as-f'<address>', andmail()undoes that quoting withescapeshellcmd(): a sender with a quoted local part such as"a' -X/tmp/x.php -OQueueDirectory=/tmp '"@example.com(valid foreZMail::validate()) gave sendmail options of its own.-fis now given only for a plain address (isShellSafeAddress()), without quoting; any other sender is left out with a debug warning.content/tipafriendwithout[TipAFriend] FromEmailsent with the visitor's address as sender and envelope sender; it now sends withEmailSenderorAdminEmailand puts the visitor intoReply-To; the display name is then[SiteSettings] SiteName, not the visitor's free-text name (which made "Bank support <site address>").- An SMTP server that offers only
AUTH NTLMmadeezcMailSmtpTransportthrowezcBaseExtensionNotFoundException('mcrypt')into the request;eZSMTPTransport::sendMail()now catches everyThrowable, logs it and returnsfalse. eZMail::senderText()of a mail without a sender raised "Trying to access array offset on false" and a PHP 8.1 deprecation inmb_encode_mimeheader(); it returns''.
Added
site.ini [MailSettings] TransportAuthMethod(empty by default: the library's choice).- A warning in the debug log when SMTP credentials go unencrypted to a host other than loopback.
Updated
- The
multipart/alternativeboundary of template mail and the generated Message-Id are random; the library's forms carried the PHP process id.
Tests
eZSendmailEnvelopeSenderTest,eZSMTPTransportTest(fake SMTP server on 127.0.0.1),eZMailTest,expMailTemplateBodyTest,ezpMailMessageIdTest,expTipafriendSenderTest(no database).
2026-10-24: package archives are scanned before they are read
Fixed
- A package archive in the PAX tar format (tar types
x/g; Python'starfilewrites it by default since 3.8,bsdtarfor names that are not ASCII) keptpackage/upload,ezpm importand the setup wizard busy untilmax_execution_time: the GNU header reader ofzetacomponents/archive1.5.1 moves on only afterLandKheaders and read the same block for ever. The same held for GNUD/M/N/S/Vheaders and for a regular file typed\0in an archive with theustarmagic. A long name header was read into memory in the size it declared (up to 8 GiB) before a single entry was checked. - A file packed as 04755 came out of the archive set-uid, a directory packed as 0777 world writable: ezcArchive applied the archive's modes.
Added
expPackageArchive(kernel/classes/exppackagearchive.php):scan()reads the tar headers 512 bytes at a time, skips the data by its size and stops atMaxEntries/MaxUnpackedSize;open()opens the archive only when the scan finds nothing, with the extract callbackexpPackageArchiveModes(files 0644, directories 0755, within the installation's file mode limits). Used byeZPackage::importUnaudited(),eZPackageUploadInspector::inspect()andexpPackageJson::extractArchive().- The refusal codes
tar_pax,tar_entry_typeandtar_long_name, with their messages in the source catalogue and the complete kernel catalogues (The .ezpkg file). A long name header whose size is a multiple of 512 bytes is refused, because the reader then skips the entry's real header and parses its data as the next one; GNU tar stores the name with a closing NUL, so a name of 511, 1023, ... or 4095 bytes cannot be imported. Thetar_paxmessage names the GNU format in the spelling of both tars:tar --format=gnufor GNU tar,tar --format=gnutarfor bsdtar (macOS, the BSDs), which answers--format=gnuwith "No such format". - A size field of 12 octal digits without terminator, as old writers fill it, is read like ezcArchive reads it.
Tests
expPackageArchiveTest(no database): a GNU archive with a long name and a V7 archive pass and extract, a PAX archive is refused quickly (in a child process with a deadline), unreadable header types, a huge long name, a long name of a block multiple and damaged headers are refused, a size of 12 octal digits is read while a base-256 or not octal size is refused, the limits are kept, and the extracted modes are masked. An archive written aseZPackage::exportToArchive()writes one (ezcArchive's ustar writer, a path over 100 bytes in the prefix field) passes the scan, the inspection and the extraction, so every package the kernel exports stays importable.
2026-10-24: package XML is read without network access, and the form token is compared in constant time
Fixed
eZPackage::fetchDOMFromFile()and the three XML readers ofeZPackageFileBrowser(documentText(),prettyPrintXML(),objectItemSummary()) loaded the XML of an uploaded package withoutLIBXML_NONET, so a DTD or an external entity named in it was fetched from any host. They passLIBXML_NONETnow, asexpPackageJsonandeZPackageUploadInspectoralready did.ezxFormToken::input()compared the posted token with!==, which stops at the first differing byte. It useshash_equals()now; a token posted as an array (ezxform_token[]=) is refused as wrong instead of aTypeError.
Tests
eZPackageXMLNoNetworkTest: everyload()/loadXML()of both files passesLIBXML_NONET, apackage.xmlwith aDOCTYPEnaming a DTD on another host is still read, the file browser still pretty prints, shows and summarises.ezxFormTokenCompareTest(no database): the right token from the field and from theX-CSRF-Tokenheader passes; wrong, truncated, longer and array tokens are refused.
2026-10-24: getDataByURL() fetches only http and https, and an error status is no data
Fixed
eZHTTPTool::getDataByURL()handed any address to curl orfopen(), both of which readfile://,php://,ftp://and more, so a configured or posted address (RSS import, static cache) could return local files; curl also followed a redirect tofile://orgopher://. Only absolutehttpandhttpsaddresses with a host are fetched now (eZHTTPTool::isHTTPURL()), and curl is limited to both protocols, also for redirects. An address without a scheme (www.example.com/feed, as RSS sources are often entered) is read ashttp(eZHTTPTool::withDefaultScheme()), as curl did before.- A response with a status of 400 or more returned the error page's body as the data. It is
falsenow, with curl and withfopen()(the status of the last response in the stream's headers,eZHTTPTool::responseStatus());$justCheckURLisfalsefor it as well. - The static cache fetches several pages at once with
curl_multi(eZStaticCache::fetchPages()), outsidegetDataByURL(); its handles follow redirects and are limited tohttpandhttpsthe same way now (eZHTTPTool::limitCurlToHTTP(), used by both).
Tests
eZHTTPToolGetDataByURLTest: refused schemes, paths, addresses without host, control characters and non-strings; 200, 404, 500 and redirects against PHP's built-in server on 127.0.0.1 (fixtures/http_status_router.php); a redirect tofile://not followed; the status of a real stream; addresses without a scheme; a curl handle limited bylimitCurlToHTTP()refusesfile://;eZStaticCache::fetchPages()answers 200 and redirects to 200 with the body and 404 and a redirect tofile://withfalse.
2026-10-24: views that send the user back to the page viewed last redirect again after the front page
Fixed
- The front page stores an empty
LastAccessesURIin the session (its requested URI is empty).eZRedirectManagerrefused''as unsafe ("empty"), soredirectTo( $module, false )had no target and views relying on it (shop/setusercountry,content/hide,user/preferences) did not redirect at all after the front page was the page viewed last. An empty page viewed last is now the site root/; an unsafe page or no page at all stays no target, andunsafeReason()itself is unchanged.
Tests
eZRedirectManagerSafeURITest: RED-01,redirectURI()with an empty or blank page viewed last returns/, a normal page is returned as before, an unsafe page and an empty session returnfalse.
2026-10-25: the front page is recorded as the site root, for every reader of the page viewed last
Updated
- The web kernel records the front page in
LastAccessesURIandLastAccessedModifyingURIas/, not as an empty page (eZRedirectManager::pageURI()). Every other reader of the page viewed last (returnURI()behind Cancel and Discard,user/login,user/logout,shop/basket, the "Verify it's you" step) got''before and fell back to its default or to nothing.returnURI()reads an empty page left in an older session as/too, asredirectURI()does.
Tests
eZRedirectManagerSafeURITest: RED-02pageURI(), RED-03returnURI()with an empty page viewed last returns/instead of the default, RED-04 the web kernel records the page throughpageURI().
2026-10-24: a copied object keeps the names of its translations, and the default language creates no language
Fixed
content/copynames each translation of the copy witheZContentObjectVersion::name( false, $languageCode ), but the method had a single parameter and cached its first answer for every language, so every translation of the copy got the name of the main language.name()takes the language from either position now and caches the name per language.eZContentObject::defaultLanguage()without a prioritized language (a CLI script without siteaccess) created[RegionalSettings] ContentObjectLocaleas a new content language, even in an installation that already had others. It uses that locale when it exists, else the existing language with the lowest ID, and creates it only when there is no language at all.
Tests
eZContentObjectVersionNamePerLanguageTest(no database): the name per language from both parameter positions, cached per language, kept apart from the name without language, with the object's name as fallback.eZContentObjectDefaultLanguageTest(no database): an existingContentObjectLocaleis used, a missing one falls back to the lowest existing ID and adds no language.
2026-10-24: the currency code and class attribute names are escaped in six templates
Fixed
shop/editcurrency.tpl(design/admin,design/admin4) printed$original_currency_coderaw in the heading and in the hiddenOriginalCurrencyCodefield. The code comes from the URL and reaches the template for an unknown currency as well. Both places use|washnow.- Class attribute names were printed raw in
design/standard(user/register.tpl,content/collectedinfo/poll.tpl,content/history.tpl) anddesign/base(override/templates/edit/comment.tpl). The name is editable by anyone who may edit classes and is shown to visitors; all seven places use|washnow.
Tests
AdminStoredValueEscapingTest: the case "edited currency code".StandardClassAttributeNameEscapingTest(source text): each of the four templates prints the class attribute name only escaped.
2026-10-25: more names escaped in the standard and base designs
Updated
- The "wait until date" workflow event of the standard design prints class names and class attribute names escaped, in its edit form (the lists and the entries) and in its view. The admin designs already did.
- The poll of the base design (
embed/poll.tpl,collectedinfo/poll_result.tpl) prints the question and the answers escaped, as the option datatype's own templates do; editors enter them and visitors see them.
Tests
StandardClassAttributeNameEscapingTest: the four templates above, each expression only in its escaped form.
2026-10-24: ezjscore never answers with an empty JSON body
Fixed
ezjscAjaxContent::autoEncode()andnodeEncode()calledjson_encode()without flags, which returnsfalsefor a string that is not valid UTF-8 (a truncated multibyte character in a title, a Latin-1 value from an old import):ezjscore/callanswered 200 with an empty body, which the client only saw as a JSONSyntaxError. The newezjscAjaxContent::jsonEncode()replaces invalid bytes with U+FFFD (JSON_INVALID_UTF8_SUBSTITUTE); a value that still cannot be encoded (INF,NAN) is logged and answered with the error objectezjscore/calluses for every other failure. Valid values are encoded exactly as before.
Tests
ezjscAjaxContentJsonTest(no kernel, no database): invalid UTF-8 through both encoders and throughezjscore/call, values that cannot be encoded, valid values unchanged.
2026-10-25: the json_encode template operator and the ajax uploader answer with valid JSON too
Updated
- The
json_encodetemplate operator of ezjscore and the ajax uploader's own JSON answers (the error carrying the exception message, the location form) encode throughezjscAjaxContent::jsonEncode(). A value that is not valid UTF-8 (a file name of an old import) gavefalsethere as well: an empty value in the page, an empty answer to the upload.
Tests
ezjscAjaxContentJsonTest: JSON-05 the template operator with invalid UTF-8, JSON-06 the uploader's answers.
2026-10-24: three leftovers around removing content
Fixed
content/actionwithActionRemoveand a node ID that no longer exists (the form sent again after the removal) ended in "Call to a member function attribute() on null". It is answered with "not available" now.content/removeeditversionleftDiscardConfirmin the session, so any later GET of the view discarded the version named inDiscardObjectID/DiscardObjectVersionwithout asking (a new draft can get the number of a discarded one). The value counts for the request that follows the Discard action only.eZContentObject::purge()asked only the workflow event types already loaded in the request to clean up once any type had been created; the others kept their rows for the purged object. It now asks every type ofworkflow.ini(eZWorkflowType::fetchRegisteredTypes()).
Tests
expContentRemoveLeftoversTest(no database): the node check ofActionRemove, the one-timeDiscardConfirm, and a purge that lets a type loaded before andevent_ezapprove(not loaded before) both clean up.
2026-10-25: the approval of user accounts after a purge and in persistent workers, Preview of a removed object
Updated
- The configurations of the "Approve user accounts" events (
expUserApproval::activeConfigs()) are read again in every request: a reset hook of the request scope (userapproval,expUserApproval::resetRequest()). A persistent worker kept the ones of its first request, so a changed workflow counted there only after a restart. A purge that takes a group or a user out of an event's lists forgets them at once too. content/actionwithActionPreviewfor an object that no longer exists answers "not available", asActionRemovedoes now, instead of ending in "Call to a member function attribute() on null".
Tests
expContentRemoveLeftoversTest: RL-01 forActionPreview, RL-03 the purge letsevent_expuserapproveclean up, RL-04resetRequest()forgets the request's configurations and keeps a test override.expKernelServicesTestlists the new reset hook.
2026-10-25: "Verify it's you" before approving or denying a new user account
Updated
- Updated: Approve and Deny of a new user account waiting for approval (an item of the workflow event Approve user
accounts) ask "Verify it's you" in the users group: the rule
collaboration/action other CollaborationAction_Accept CollaborationAction_Deny CollaborationAction_Deferinsite.ini [ReauthGroup_users], andexpReauth::addressedUser()reads the item of a POST tocollaboration/action(the pending account). The decision is held until the approver confirms and is then carried out as pressed. Ordinary content approvals address no account and never ask; comments and opening the item do not ask either. - Updated: The users group's description on Setup > Verify it's you names approving and denying new user accounts (eng-US, ger-DE, untranslated).
- Docs: Verify it's you, Approving new editor accounts, the upgrade notes Passkeys and "Verify it's you" and Approval of new user accounts, and their German counterparts.
Tests
expReauthRulesTestRA-11: Approve, Deny and Defer of an account approval ask in the users group; a content approval, a comment and opening the item do not, also with the content group on; the item id of the POST without a database.
2026-10-26: every mail as HTML with a plain-text part in the shared mail layout
Updated
- Updated: The mails of the forgotten password (the reset link, then the new password), "Your password was changed",
"New sign-in to your account" and its copy to the administrators, the e-mail preference mails (confirm a
subscription, confirm a new address, the link to the preferences, the note to the old address), tip a friend, the
mails of collected information and the order confirmation are HTML mails with a plain-text part in
design:mail/layout.tplanddesign:mail/layout_text.tpl, as the registration mails already were: a heading, a short text, one button with its address as text below it, the security notes, the site's name. Clumsy wording was rewritten; links use https. - Updated: Notifications, audit alerts and the reports of extensions, written as text, are sent as that text and as
HTML in the same layout (
expMailTemplateBody::fromText()); the first address on a line of its own is the button.notification.ini [MailSettings] HTMLLayout=disabledkeeps notifications text only. - Updated: The layout shows a code to type in (
mail_code) large and easy to copy, has a hidden preview text (mail_preheader), more paragraphs and small print (mail_paragraphs,mail_outro), and dark colors for the mail programs that ask for them. - Updated: The mail gate puts the footer of an optional mail (unsubscribe, manage the preferences) into both parts of a multipart/alternative mail, inside the card of the HTML part; before, it was appended after the last part, where no mail program shows it (no optional mail was multipart until now).
- New strings in the contexts of the mails and
design/standard/mail, translated in every administration catalogue. - Docs: The mail layout.
Tests
expMailLayoutMailsTestML-01 to ML-12: every mail template builds the layout with a text part, escapes typed values, carries no styles or markup of its own, uses https links given as text too;fromText(),applyRendered(), the gate's footer in both parts, the audit alert, a notification sent as text and HTML.
2026-10-26: the packages of every generation import
Fixed
- Packages made by eZ Publish 3.x, 4.x and 5.x and packages repacked by hand were refused on import. Measured against
1,429 distinct archives on disk: the 30 ezwebin 1.0 packages (V7 archives of the 3.x Archive_Tar port, files typed
\0) were refused asunsafe_type, because ezcArchive's V7 reader takes such a file for no file under PHP 8, and 5 uncompressed.ezpkgfiles of the 4.x era asnot_gzip. Archives built the way each tool writes them were refused too: a 3.x archive with a long name (Lwithout magic,tar_entry_type), every PAX archive (bsdtar on macOS, GNU tar--format=posix;tar_pax), a GNU tar repack (./package.xmlwas not found at the top,no_definition; with links,unsafe_type), a GNU volume label or a PAX global header (tar_entry_type,tar_pax). expPackageArchive::index()now reads V7, ustar, GNU and PAX headers (PAXpath,linkpath,size,mtime, bounded to 64 KiB), long namesL/Kof any size up to 4096 bytes, files typed0,\0and7, directories5andD, and normalises names (./,//). An archive ezcArchive cannot read as it is is rewritten into a plain GNU tar archive below the cache directory and opened from there (expPackageArchive::rewrite()); the copy is removed as soon as it is open. A hard or symbolic link to a file of the package is extracted as a copy of that file.- What a package cannot hold no longer refuses it: a link out of the package (
link_outside), to a directory (link_directory) or to nothing (link_missing), a device or fifo (special_file), a sparse file or an unknown type (unsupported_entry) is passed over with a notice, written to the debug log byeZPackage::importUnaudited(), returned byeZPackageUploadInspector::inspect()(notices) andexpPackageJson::extractArchive(), and listed among the validation warnings ofpackage/upload. An entry whose own path leaves the package (.., an absolute path) still refuses it, as do the size and entry limits; files still get 0644 and directories 0755. - A plain (uncompressed) tar archive passes the upload inspection again, as eZ Publish read it through
compress.zlib://. - A file or directory name longer than 255 bytes is refused as
tar_name_segmentbefore extraction: ezcArchive ended the request with a fatal error when it could not create the file. - Package names with upper case letters or a hyphen import:
eZPackage::isValidName()accepted only what theidentifiertransformation leaves unchanged (lower case letters, digits,_), which refused 4 packages of the 4.x era on disk asinvalid_name(RSSFeed_content_class,Classi_helper_classi,infoProf1.0 and 1.1). A valid name is now letters of either case, digits,_and-, not starting with-, at most 200 characters: still no/, back slash, dot, space or control character. The name stays the package's directory as it is; the creation wizards still offer lower case names.
Removed
- The refusal codes
tar_paxandtar_entry_typeand their messages; every administration catalogue (71) that had translated them carries the six new messages translated instead, so the complete ones stay complete.
Tests
expPackageArchiveLegacyFormatsTest(no database): twelve fixtures intests/tests/kernel/classes/packages/fixtures/legacy/, built bybuild-legacy-package-fixtures.phpthe way each tool writes (3.x Archive_Tar V7, the 4.0 to 4.2 handler, ezcArchive ustar, PEAR Archive_Tar, GNU tar gnu, posix, ustar and v7, bsdtar on macOS, GNU and ustar oddities), each inspected, extracted and imported: content, copies of links, modes, notices, and nothing but plain files and directories inside the target.expPackageArchiveTestandeZPackageUploadInspectorTestfollow: PAX archives, unusual types, long names of a block multiple and links are read; paths out of the package are refused.
2026-10-28: orders record the siteaccess they were placed in
Added
- Every order records the siteaccess (store or site) it was placed in, in the new column
ezorder.siteaccess(varchar(255) NOT NULL default ''), set byeZBasket::createOrder()and, for an order that has none yet, byeZOrder::activate(). The order view, the order list, the archive list and the customer order view ofadminandadmin4show it; the order lists filter by it ((siteaccess)/<name>,-for unknown) and the order list sorts by it. The customer's order view shows the store's name. Orders placed before keep''and show as unknown;bin/php/expbackfillordersiteaccess.phpinfers their siteaccess from the content tree each siteaccess serves (a dry run unless--apply). Run the 6.0.0 to 6.0.15 database update (update/database/<engine>/6.0/dbupdate-6.0.0-6.0.15.sql) to add the column; until then the shop works as before. Feature.
Tests
expOrderSiteaccessTest(no database): the definition with and without the column, the recording, the filter parameter and the SQL and document conditions built from it, the schemas, the update SQL and the templates.expOrderSiteaccessBackfillTest(no database): the inference of the backfill from path strings and siteaccess roots (one, none, several, a removed product, the deepest root).
2026-10-28: site packages keep Composer's extensions and find the content root
Fixed
- An extension that Composer installed (an install path in
vendor/composer/installed.json) or that is a git checkout is now always kept when a site package carries a copy of it, also when the package's copy states a higher release number: Composer updates it, and a package that replaced it leftinstalled.jsonandcomposer.lockdescribing files that were no longer there. The 6.0.15ezwt_extensionpackage (6.0.17) replaced a Composer ezwt 6.0.16 before. Uninstalling an extension package no longer deletes such a directory either; only its activation is undone. Both are named invar/log/setup.log. A plain copy is still replaced by a newer package, as before. - The legacy site packages (ezwebin_site, ezflow_site, ezdemo_site and their
_cleanvariants, in every release before the rebuilt 6.0.15 ones) swap the content root with their "Home" page and name the root "eZ Publish", a name the clean data of Exponential 6 no longer uses: the installation stopped at post-install step 8 with "Can't fetch node ''".eZSiteInstaller::contentObjectByName()now takes aremote_idfirst, then the name (with theclass_idthe step gives, which was dropped before), and for a name the content root has carried ("eZ Publish", "Exponential", "Websites", "eZ Platform") the object with the root's remote id (eZSiteInstaller::CONTENT_ROOT_REMOTE_ID).removeContentObject()never purges an object whose main node is a top-level node, so a removal of the old root cannot remove the content root itself when the swap did not happen.swapNodes()names the object it could not find instead of an empty node id. Theezwebininstallcommand names the root by its remote id. Doc:doc/eng-us/bc/6.0/package-install-keeps-newer-extensions.md.
Tests
ExtensionKeepNewerTest(no database): a Composer or git copy is kept against a newer package.SiteInstallerContentRootLookupTest(no database): the former root names, the lookup order, the messages, the top-level guard and the command's remote id.
2026-10-28: the user audit log
Added
- The user audit log: the table
expuser_auditkeeps one row per security event about a user account or a user group: who acted, when (to the millisecond), where (siteaccess, host, module and view, the address asaudit.ini [AuditPrivacySettings] Field[actor.ip]leaves it), whom (the account or group, id and login), what (the event name, result and reason, refused and failed attempts included) and the fields before and after. Passwords, hashes and other secrets are never stored. The rows are copied from the audit's access channel by the new sinkuseraudit(expAuditUserAuditSink,[AuditChannel_access] Sinks[]), so every path the audit records is covered: user/edit and content edits of user objects, registration and its approval, enable, disable, move, removal, member management, password changes and resets, role assignments, sign-ins. Settings:audit.ini [UserAuditSettings](UserAudit,Events[],RecordIP,RetentionDays,MaxValueLength). - Users > User audit log (
user/audit, new policy functionuser/audit): the events newest first, filtered by user (id or login, the account or the user who acted), action, result and days, paged, and exported as CSV (a POST, recorded asdata.export.csv). The same template inadminandadmin4, built from the shared components only. - The cronjob part
useraudit(also ininfrequent) removes the rows older thanRetentionDays. - The table is in
share/db_schema.dba(MySQL and MariaDB, PostgreSQL, SQLite, Oracle through ezoracle), the kernel schemas and the 6.0.0 to 6.0.15 update files;update/common/scripts/6.0/createuseraudittable.php(--dry-run) creates it on any engine. Not on MongoDB. Feature. - The member management page in German (
doc/ger-de/bc/6.0/mitgliederverwaltung.md).
Tests
expUserAuditTest(no database): which records are kept, the row, no secrets, the filters and their SQL, the CSV cells, the schema in every file and the SQLite update run twice, the wiring (sink, view, policy, cronjob, menu, help).
2026-10-28: addeditorpolicies and addmembermanagement only show what they would change unless --apply is given
Updated
bin/php/addeditorpolicies.phpandbin/php/addmembermanagement.phpare a dry run by default and write only with--apply; they wrote by default and took--dry-run. Both change roles and assignments of an installation that exists, and a run without options now lists the changes instead of making them, asbin/php/expbackfillordersiteaccess.phpdoes.--dry-runis still accepted and means the same as no option, so scripts and notes that pass it keep working;--applytogether with--dry-runis a usage error (exit 2). The dry run ends withPASS: ... (dry run, nothing changed; --apply writes). The setup step of a new installation callsexpEditorPolicies::ensure()andexpMemberManagement::ensure()directly and is not affected.Exponential\Runnable\Command::applyRequested()reads the two options for any command that works this way. Behavior change.
Tests
expEditorPoliciesTestandexpMemberManagementTest(no database): no option and--dry-rundo not write,--applywrites, every write call of both commands sits behind the dry-run check, and both options together end the command with exit code 2.
2026-10-29: exp:upgrade, the upgrade from the console
Added
./console exp:upgradeupgrades an installation in one call: a dry run by default that lists, step by step, what would change; with--applya backup, then every pending step in order, each recorded, stopping at the first failure, thenexp:velocity deploy --kerneland a request to the front page. The stages run in a fixed order: code, libraries, database, scripts, finish. Every stage is its own command:exp:upgrade:database,exp:upgrade:scripts,exp:upgrade:finish; with themexp:upgrade:status,exp:upgrade:steps,exp:upgrade:verify(drift: a step recorded as done whose effect is missing) andexp:upgrade:backup. Machine output--format=json(exp-upgrade/1); exit codes 0 to 4. The names, options, exit codes and the output are kept for good. Guide.- 32 steps for 6.0.15 (
exp:upgrade:steps): the 6.0.0 to 6.0.15 database update in 21 steps written from the kernel schema on every engine (MySQL and MariaDB, PostgreSQL, SQLite, Oracle through ezoracle; MongoDB gets the collections), the update scripts ofupdate/common/scripts/6.0/and the setup commands through their classes, and the finish. Each step looks at the installation itself, so what was done by hand counts asnot-needed. - The record of what
exp:upgradeapplied: since 31 October the tableexp_migration; the first pre-release kept it in a tableexpupgrade_stepof its own, which no release ships (dropped). - Extensions add their own steps with
upgrade.ini [UpgradeSettings] ExtensionStepList[]. Guide.
Fixed
update/database/mysql/6.0/dbupdate-6.0.0-6.0.15.sqlstopped with a syntax error after the tableexppasskey(its closing line was missing), so the help content tables, the user audit table andezorder.siteaccesswere never created by it; the PKCE columns are now added only when missing, so the file can run again.
Tests
expUpgradeRegistryTestandexpUpgradeCommandContractTest(no database): the order of the steps, dependencies and their problems, the states, the options, every step registered and its tables, columns and indexes in the kernel schema, the record table in every schema file, and the contract snapshot of the commands.- On throwaway SQLite, MariaDB and PostgreSQL databases: a new installation is
not-needed; a 6.0.14 database is pending, the dry run changes nothing,--applymakes a backup and applies every step, a second run changes nothing, the schema then equals a new installation's, a dropped table is reported as drift, a half-applied database is completed, and after the SQL update file run by hand every database step isnot-needed.
2026-10-29: exp:upgrade moves the code and the libraries, and rolls back
Added
exp:upgrade:code(stage 1): the installation's git checkout, and each--reposcheckout, moved to--to(a tag, a branch or a commit) or to its upstream through git_manager, fast-forward only, the submodules following. Refused over local changes, over commits the target lacks and for a process that is not the owner of the checkout. git_manager's backup comes first on MySQL and MariaDB; the commits before the move are kept incode.json.exp:upgrade:libraries(stage 2):composer installfromcomposer.lock, or--updateof named packages first, withcomposer auditbefore and after. Refused over local changes invendor/, over a package kept as a git checkout of its own (an extension updated by git: Composer would replace it), and for a process that is not the owner (--allow-vendor-changesfor the first two).exp:upgraderuns the stages after the code stage in a new PHP process when the code moved, so they use the new code; the reports of both processes are one.exp:upgrade:rollback: the backups, and with a name and--applythe database,settings/override, the libraries and the checkouts restored, part by part (--parts=). Never automatic.--no-backupis refused on production ([AdminEnvironment] Level, orautowithEXP_ENV) without--force-no-backup.- Audit events
system.upgrade.step,system.upgrade.backup,system.upgrade.rollback;system.upgrade.runalso for the end ofexp:upgrade --apply. - The backup also keeps
composer.json,composer.lock,installed.jsonand the list of tables.
Fixed
- The upgrade backup of MySQL and MariaDB wrote
mysqldump's warnings into the dump; PostgreSQL's dump now cleans the objects it restores (pg_dump --clean --if-exists).
Tests
expUpgradeStagesTest(no database): the lock againstinstalled.json, git checkouts found, merged reports, the options of the new process, production, the rollback's usage errors, the audit events.- On throwaway git repositories, a throwaway Composer project and throwaway SQLite, MariaDB and PostgreSQL databases:
the code stage (dry run fetches only,
--to, upstream, refusals,--repos), the libraries stage (install, a second run,--update, refusals), the rollback of each part, the production rule; on a throwaway installation cloned from the branch,exp:upgrade --apply --stages=code,finishmoves the code and runs the finish stage with the new code in a new process.
2026-10-28: approval of new staff accounts by section, collaboration mail links to the administration
Added
- Added: The section based approval of new staff accounts, an optional alternative to the event Approve user
accounts (which stays the default): a section Staff for Administrator users and Editors and the plain approval
event (
ezapprove) on content/publish before, with Administrator users approving and needing no approval, for new objects only.bin/php/adduserapproval.php --mode=section(a dry run unless--apply;--staff,--approvers,--exempt) sets it up and refuses while the event Approve user accounts runs on that trigger, as both together would ask twice for one account;site.ini [UserSettings] AccountApprovalMode=group|section|nonechooses what the setup step creates (defaultgroup).expUserApprovalSection.
Updated
- Updated: The collaboration notification mails (
notification/handler/ezcollaboration/view/plain.tpl,ezapprove/approve.tpl,ezapprove/author.tpl) link to the item in the administration ({$collaboration_item.admin_url},eZCollaborationItem::adminURL():[UserSettings] RegistrationAdminBaseURL, else the administration siteaccess), not to the SiteURL of the siteaccess that ran the notification cronjob. - Updated: The plain approval event, for a user account or user group: every approver gets the mail with the link to
the item, the request, approval and denial are recorded (
access.user.approval.*withmode: section, also in the user audit log), "Verify it's you" asks before Approve and Deny, and the decision is carried out at once. - Docs: Approval of new user accounts ("Two ways", examples), Approving new editor accounts (step 9, screenshots), the feature page, and their German counterparts.
Tests
expUserApprovalSectionTest(no database): the default groups, the approval event's fields aseZApproveTypereads them, the policy warnings, the modes, the command's dry run, the collaboration mail links, the plain event's hooks.
2026-10-28: sign-in sessions with their place, host and second step
Added
- Each signed-in session keeps the host it was signed in on, its second step (an authenticator app, a code by e-mail,
a recovery code, a passkey) and, with a local GeoIP database, its country and approximate place: the new table
expuser_session_info(expUserSessionInfo), created by the 6.0.0 to 6.0.15 update files andupdate/common/scripts/6.0/createusersessiontable.php. Your devices shows them. [UserSessionSettings] GeoIPDatabaseandGeoIPLanguage: a MaxMind DB file (.mmdb) on the server, read withMaxMind\Db\Readerwhen it is installed; nothing is asked of another server and private addresses are not looked up (expUserGeoIP). A sign-in from a country the account has not used withinRetentionDaysisaccess.session.new_locationand goes into the "New sign-in" e-mail, which now also links to the security activity.expUserSessionTracker::exportForUser()anderaseForUser()for a data export and erasure.
Updated
- The addresses of sessions are stored as
[UserSessionSettings] RecordIPsays:audit(default, as the audit keeps them: the network by default),full,truncatedordisabled. - A session's last use is written at most once a minute (
TouchInterval=60, was 300). - A session kept by Remember me is no longer ended as expired before its Remember me date, also when
[Session] SessionTimeoutis shorter. - The method hint of a sign-in and the row of the current session are kept for their own request only, so a persistent Velocity worker never hands them to the next request.
Tests
expUserSessionInfoTest(no database): the address modes, the GeoIP record and lookup, the new-location rule, Remember me and the timeout, the method hint per request, the schema in every file, and on SQLite in memory the details with the list, the retention, the export and the erasure.
2026-10-28: security activity per account
Added
- Security activity (
user/security,user/security/<user id>with the policyuser/sessions): the account's recent security events as a timeline, newest first, filtered (all, sign-ins and sessions, account changes, needs attention) and paged, with the sign-ins, failed attempts and changes of the last 30 days on top (expUserSecurityActivity, read from the user audit logexpuser_audit; nothing is recorded twice). Templates inadmin(alsoadmin3),admin4(alsoadmin4l) andstandard(the public site), shared components only. - Linked from Your devices, the account page (a card with the failed sign-ins of the last 30 days), the user menu of the header (also on the phone) and the "Current user" box, with "Your devices" next to it.
- Audit events of two-step sign-in:
access.user.two_factor.enroll,.remove,.recovery_used,.recovery_issued,.verify.failed, recorded by the two-step extension throughexpSignInSecondFactor::audit(). - The help of
user/sessionsanduser/security.
Tests
expUserSecurityActivityTest(no database): every shown event is in the taxonomy with an icon of the administration and reaches the user audit log, the filters, the description of a row (who acted, no secrets), the counts, and on SQLite in memory one account's events newest first, paged, counted and exported; the wiring.expUserAccountOverviewTest: the card of the own account and of another account with and without the policy.
2026-10-28: the sign-in report and signing a user out everywhere
Added
- Users > Sign-in report (
user/sessionreport, new policy functionuser/sessions_manage): the sessions running and the users they belong to, today's sign-ins and failed attempts, the last 14 days with bars (sign-ins and failures from the user audit log, new sessions fromexpuser_session), the running sessions of every user with a button to sign one out, and Sign a user out everywhere by user ID, username or e-mail address (expUserSessionReport). It readsexpuser_session, so it works with every session handler; Setup > Sessions links to it when its handler keeps no table. Left menu and mega menu of the Users tab; the help of the view.
Updated
- "Verify it's you" now asks before Sign out all other sessions on Your devices (
[ReauthGroup_account]user/sessions self SaveMailButton RevokeOthersButton) and before signing sessions out on the sign-in report ([ReauthGroup_users]); with two-step sign-in it takes the code of the authenticator app. - The generated example pages of the settings, module views and cronjobs are current again.
Tests
expUserSessionReportTest(no database, and on SQLite in memory): counts per day, bar widths, the running sessions of every user (Remember me kept, timed out and ended left out), the wiring;expReauthRulesTest,AdminMegaMenuTest.
2026-10-28: security alerts, data export and erasure of sign-in data
Added
- Security alert e-mails to the owner of an account (
expUserSecurityAlerts, templatedesign:user/security_alert_mail.tplin the shared mail layout, categorysecurity, eng-US and ger-DE): two-step sign-in turned on or off, a recovery code used,FailedSignInAlertThreshold(5) failed sign-ins in a row, the account locked; each with "Secure your account" (the security activity) and Your devices. Sent from the rows the user audit log stores for the first time, so no event alerts twice and a late spool alerts nobody. A user cannot turn them off;[UserSessionSettings] SecurityAlertMail=disableddoes for the site. - Download my sign-in data on the security activity: the account's sessions with their details and its security
events as JSON ("Verify it's you" first,
[ReauthGroup_export]), recorded as the new eventdata.export.json.
Updated
- Removing a user account erases its sessions and their details (
expUserSessionTracker::eraseForUser()). - "Verify it's you" also asks before the CSV export of the user audit log (
[ReauthGroup_export]user/audit ExportButton).
Tests
expUserSecurityAlertsTest(no database, no mail): which rows alert, when none does, the wiring;expMailLayoutMailsTestrenders the alerts and the "new place" sign-in mail in the shared layout.
2026-10-28: commands only change things with --apply
Updated
- Updated: The setup and policy commands only show what they would change unless
--applyis given:addanonymouspublicpolicies,addexphelpreadpolicy,addoauthauthorizepolicy,adduserapproval(both modes),cleanuppoliciesandadddefaultstates, likeaddeditorpoliciesandaddmembermanagementbefore them. Each dry run ends withPASS: ... (dry run, nothing changed; --apply writes);--applywrites and checks the result;--dry-runis still accepted;--applywith--dry-runends with exit code 2 before the database is read.addexphelpreadpolicyandaddoauthauthorizepolicynow end with PASS or FAIL.cleanuppolicieslists each policy it would remove and reads them all before removing any; its paging no longer rereads pages.adddefaultstateshas a dry run, per state group. - Updated: The maintenance commands work the same way: the writing actions of
audit,explogs,contentcheckandexphelpcontent, andexploremipsumfill,collaborationsampledata,cleanuprss,clusterpurge,ezsubtreecopy,ezsubtreeremove,trashpurgeandcleanupversions(whose removal loop skipped every second batch and could fail to end). - Updated: The mail and notification commands:
mailbounces,mailconsent cleanup,notificationevents cleanup,notificationsubscriptions remove-missingandnotificationrun(a dry run never sends mail);installcrontab --addand--removeshow the block and the lines that would change. The button Run now on the notification status page passes--apply. - Updated: The installers:
exp:installandexp:kickstarter runinstall only with--apply; the dry run checkskickstart.ini, the database and the remote packages and shows the plan (also whenDatabaseAction=removewould drop the tables).--forcealone is a usage error; it is accepted together with--apply. The demo rebuild examples (doc/eng-us/examples/demo-mode/) pass--applyand stop when nothing was installed. - Updated:
preload, the writing subcommands ofvelocity(config setandunset,cache clear,layout migrate,site,confandmodenableanddisable,ssl renew,watchdog installandremove, the FrankenPHPinstall),webserver configureandfrankenphp install. Process control (start,stop,restart...) andexp:velocity deployare as before; the maintenance mode clears Velocity's cache with--apply. - Updated:
ezpgenerateautoloads,exp:cacheandezcache.phpstay as they are, on purpose: they regenerate what can always be regenerated, and composer, the deploy and CI run them. - Updated: The administration's hints that name such a command show
--apply(notification status, content check storage, audit dashboard, cronjob dashboard, the setup page of an installed site), in English and German. - Docs: Behavior change with every command and the ones left as they are, and why; the generated command pages; every guide, upgrade note and example that runs one of the commands, in English and German.
Tests
expApplyFlagCommandsTest,expApplyFlagMaintenanceCommandsTest,expApplyFlagCronCommandsTest,expApplyFlagInstallerCommandsTestandexpApplyFlagDeployCommandsTest(no database): every write call of a command sits behind the dry-run check, the dry run ends with the PASS line,--applywith--dry-runends eachbin/php/<command>.phpwith exit code 2, and the callers (the notification job, the maintenance mode, the preload job, the demo scripts) pass--apply;ezpgenerateautoloadsandcachestill write without it.
2026-10-29: the SOAP entry script is index_soap.php
Renamed
- The SOAP server
soap.phpis nowindex_soap.php, named like the other front controllers (index.php,index_rest.php,index_treemenu.php), so one rule pattern describes every script the web server may run. Settings, extensions (soap.ini [ExtensionSettings] SOAPExtensions[]), sign-in and answers are unchanged; a second sign-in step still never applies to SOAP (an app password is used).eZSys::init()gets the name the request used, so URIs and the siteaccess match are read relative to it.
Updated
soap.phpstays as a shim that only includesindex_soap.php. Once a day a request through it writes a notice tovar/log/warning.log(the day is kept as the date ofvar/log/soap-php-deprecated.stamp); without a writablevar/log/it writes nothing and still answers. It will be removed in a later major version.expUserSessionTracker::detectMethod()counts a sign-in through either name assoap..htaccess_root,.htaccess_root_staticand the server examples (doc/eng-us/examples/htaccess,ezpublish.conf,web-servers/nginx.conf,web-servers/openlitespeed-vhost.conf) carry the rule forindex_soap.php, commented out as SOAP is off by default.- Documentation, English and German: the behavior-change page
The SOAP entry script is now index_soap.php (why, the lines
to change for Apache
.htaccessand the HTTPS virtual host, Plesk, nginx, LiteSpeed, Caddy, IIS and Velocity, the SOAP clients, acurltest of a GET and of a SOAP envelope, the log check for clients that still callsoap.php), and every page that namedsoap.php: WebDAV and SOAP, syndication between installations, integration surfaces, integrators FAQ, the source tree tour, getting the code, API standards, extension directories, multi-site log paths, the second step on other sign-in paths, the syndication feature page, the Apache, virtual host, nginx and Velocity examples, the hosting server overview, hardening for hosts, Plesk and the storage layout. syndication1.3.14 showsindex_soap.phpas the SOAP address when the kernel has it and uses it as the example in its import wizard.
Tests
expSoapEntryScriptTest(no database):index_soap.phpis the server, the shim only forwards and declares nothing, run in a PHP process of its own it forwards, writes one notice a day and still answers withoutvar/log/, and a sign-in through either name is a SOAP sign-in.expSignInSecondFactorTest,expPasswordChangeRequiredTestand the file-mode scan readindex_soap.php.
2026-10-29: the WebDAV entry script is index_webdav.php
Renamed
- The WebDAV entry script
webdav.phpisindex_webdav.php, named likeindex_rest.php,index_treemenu.phpandindex_cluster.php, so one pattern (index(_[a-z]+)?\.php) covers every web entry point at the root. Administrators who use WebDAV must change their virtual host (HTTP and HTTPS) and rewrite rules; the rules for Apache, Plesk, nginx and IIS and a test procedure are on The WebDAV entry script is now index_webdav.php.
Updated
webdav.phpstays as a forwarding shim: it definesEXP_WEBDAV_ENTRY_SCRIPTand includesindex_webdav.php, so existing rules and mounted drives keep working. The first request through it on a day writes a warning towarning.log(stamp filewebdav-entry-script-deprecated.stampin the log directory). It will be removed in a later major version.index_webdav.phppasses the name of the script actually run toeZSys::init()and treats/index_webdav.php[/]and/webdav.php[/]as the server root (new classexpWebDAVEntryScript).- A sign-in through
index_webdav.phporwebdav.phpis recorded as a WebDAV session (expUserSessionTracker::detectMethod()). .htaccess_rootand.htaccess_root_staticcarry a commented rule#RewriteRule ^index_webdav\.php - [L]with an explanation; the shipped rules still send WebDAV toindex.phpuntil it is uncommented.- The documentation names
index_webdav.phpeverywhere, in English and German: the WebDAV help, the integration pages, the installation chapters 3 and 8, the web server examples (Apache.htaccessand virtual host, nginx, IIS, Velocity), the hosting pages, the upgrading guide and the behaviour changes of October 2026.
Tests
expWebDAVEntryScriptTest(no database):index_webdav.phpis the server, the shim only names itself and includes it, both names are the entry script and the root, the warning is written once a day, a sign-in through either name is a WebDAV session, the shipped rewrite rules nameindex_webdav.php.
2026-10-29: PHP 7.4 is usable again, without support
Supported: PHP 8.0 to 8.5. Usable without support: PHP 7.4 (Exponential 6.0.15 and later). Exponential 6.0.15 installs and runs on PHP 7.4 again, without support (no bug-fix or security commitment specific to PHP 7.4), for sites that cannot change the PHP of their servers yet. Velocity, Exponential Layouts and a few extensions that need PHP 8 are left out on 7.4. The reasons, a table per feature, the security posture and the end-of-life stance: Why Exponential supports PHP 7.4 (Deutsch).
Added
- PHP 8.0 installs Exponential Layouts (explayouts and its packages), expquery-translator, the expsite packages and the
media site theme again: the metapackage for PHP 8.0 (
se7enxweb/exponential-php-floor1.1.4) requires them, as every one of them runs on PHP 8.0. 1.1.0 to 1.1.3 left them out; the rootcomposer.jsonaccepts 1.1.4 and later on PHP 8.0. - The metapackage
se7enxweb/exponential-php-floor(1.0.3 for PHP 7.4, 1.1.4 for 8.0, 1.2.3 for 8.1 to 8.3, 1.3.3 for 8.4 and later), required by the rootcomposer.jsonat~1.0.3 || ~1.1.4 || ~1.2.3 || ~1.3.3: Composer installs the version for the PHP it runs with, and with it the extension releases that run there. 1.1.x, 1.2.x and 1.3.x require at least the extension releases the rootcomposer.jsonrequired itself before (Exponential Velocity ~0.0.4.57 and the releases of 29 October 2026). 1.0.x adds ezodf ~7.0.4 and eztags ~2.4.31 on PHP 7.4, so the ezodf extension that Exponential activates is installed there too. The x.y.3 versions also bring Twig 3 for the Twig template engine:se7enxweb/twig~3.11.4 on PHP 7.4 and ~3.19.1 on 8.0,twig/twig^3.27 on 8.1 and later. The rootcomposer.jsondoes not requiretwig/twig: upstream Twig up to 3.26 is blocked by Composer's security advisories and 3.27 needs PHP 8.1, so atwig/twigrequirement in the kernel made installs on PHP 7.4 and 8.0 fail. The earlier floor versions (1.0.0 to 1.0.2, 1.1.0 to 1.1.2, 1.2.0 to 1.2.2, 1.3.0 to 1.3.2) allowed older extension releases or install no Twig and are left out.symfony/polyfill-php80andsymfony/polyfill-php81. make lint74andmake lint74-all, and the Quality workflow's jobphp74: PHP 7.4 syntax of every file, a production install with Composer on PHP 7.4 and the boot smoke test.expVelocity::phpRefusal():exp:velocity startrefuses the Velocity engine on PHP 7.4 with a message that names what does run there (Apache or nginx with PHP-FPM, or--engine=php).- The help page "Why Exponential supports PHP 7.4" (EN and DE), and a question in the project FAQ.
Updated
composer.json:"php": "^7.4 || ^8.0 || ... || ^8.8"; the 38 packages whose current releases need PHP 8 are required through the metapackage instead of directly (on PHP 8.4 and later the installed set is the same as before).- The kernel parses on PHP 7.4: nullable parameters are written
?Type, three nullsafe operators became plain checks, a test passes arguments by position. - The kernel runs on PHP 7.4: the query cache's session map falls back from
WeakMaptoSplObjectStorage,eZPersistentObject's magic methods have nomixedtype, the GD handler accepts an image resource,eZHTTPToolkeeps its cURL handle open until its last use, the log viewer's ZIP download sets file times only whereZipArchive::setMtimeName()exists, the sign-in integrity check callssetAccessible()before PHP 8.1, host matching usesstr_starts_with()/str_contains()(PHP 8 semantics for an empty host on every version), the HTTP benchmark keys cURL handles that are resources, and the template edit link falls back whensprintf()returns false. settings/setup.ini[phpversion] MinimumVersion=7.4.0, also the system report's default.- The tests run on PHPUnit 9.6 as well (the PHPUnit for PHP 7.4 and 8.0).
expPhpSupportandsettings/setup.ini[phpversion] SupportedVersion=8.0.0: the setup check, the kickstarter andexp:installaccept PHP 7.4 with an "unsupported" notice and refuse older PHP; the system report shows both versions.- The documentation (EN and DE) states "Supported: PHP 8.0 to 8.5. Usable without support: PHP 7.4 (Exponential 6.0.15 and later)." wherever it states a PHP requirement of Exponential: the requirements and install chapters, the hosting and panel pages, the upgrade guides, the business, press, training and developer pages, the README; Velocity and Layouts keep their own minimum (8.0 and 8.1).
Renamed
- The namespace of the class module's views
Exponential\View\Kernel\ClassisExponential\View\Kernel\ContentClass("class" is a reserved word before PHP 8.0). The old names keep working:autoload.phploads the new class for them, and each class registers the old name withclass_alias.
Tests
expVelocityPhpRefusalTest,expRenamedClassViewNamespaceTest. The DB-less suites were run on PHP 7.4 and PHP 8.0 with the same PHPUnit (9.6); every test that failed only on 7.4 was traced to a PHP difference and fixed.
2026-10-29: exp:upgrade captions, the verification after an upgrade, rollback to any point in time
Added
Before an upgrade writes anything,
exp:upgrade --applytakes a caption, git_manager's full backup (the database dump and thevar/storage) labelledupgrade <from>→<to> <time> pre. It adds the upgrade's own part: settings overrides,composer.json,composer.lock,installed.json, the git commits of every checkout, and the SHA-256 of every file. The caption is recorded in the report and inexp_migration. Without git_manager, or with one that cannot dump the engine,exp:upgrademakes the caption itself and says why.--no-caption(the same as--no-backup) is refused on production without--force-no-caption. Guide.exp:upgrade:verify --after, run in a new process at the end ofexp:upgrade --apply(--no-verifyskips it):- the steps: no drift, none failed;
- the smoke addresses (
[UpgradeSettings] SmokeURL[], else velocity.iniWarmUpURL[]for Apache and Velocity, else the front page and the sign-in page); - the new PHPUnit test suite
upgrade-smoke; - no new fatal line in the error logs;
- the autoloads.
A failure ends the run with exit 1 and lists the captions with their rollback commands.
--rollback-on-failrolls back to the pre caption;--post-captiontakes a caption of the verified state.exp:upgrade:captions: every caption, git_manager's own included.--verify=<id>checks the checksums and restores the dump into a throwaway database.--prune [--apply]appliesCaptionKeep,CaptionKeepDaysandCaptionKeepUpgradesofupgrade.ini [UpgradeSettings].exp:upgrade:rollback <caption>works with any caption, and has a new part,storage(var/). With--applyit first takes a safety caption of the current state, so a rollback can be undone, and it verifies afterwards.Audit events
system.upgrade.captionandsystem.upgrade.verify.git_manager: captions dump PostgreSQL, SQLite and MongoDB as well as MySQL and MariaDB, and describe their dump in
dump.json(git_manager, next release).
Tests
expUpgradeCaptionTest(no database): prune by count, age and the last upgrades, checksums, caption ids never paths, the fatal lines of an error log since a moment, the smoke addresses.- On throwaway SQLite, MariaDB and PostgreSQL databases with git_manager's every-engine dump: the pre caption and its
record, the list,
--verify(checksums and a restore into a throwaway database) and a changed file found,--no-captionrefused on production, rollback with its safety caption and back again, the storage part, prune. On SQLite also without git_manager, and with a git_manager that dumps MySQL only. - On a throwaway installation through the console: a failed verification ends
exp:upgrade --applywith exit 1, lists the captions and, with--rollback-on-fail, restores the pre caption. A passed one (with the test suite) takes the post caption.exp:upgrade:captions,--verifyand the rollback dry run work on the result.
2026-10-29: "Verify it's you" with the password and a passkey, a passkey policy, a passkey as the second step
Guides: Passkeys, Verify it's you. Upgrade note: Passkeys and "Verify it's you".
Updated
- Updated: "Verify it's you" asks for the password AND a passkey, without a passkey for the password and the code of
the authenticator app (
[ReauthSettings] Factors[]); a way of the same strength is offered beside it, never a weaker one. A web sign-in that asked for as much counts as a confirmation. - Updated: a group
delete(on): emptying the trash, removing from it, removing several items at once (rule tokenbulk,BulkCount). Another user's devices in full and the sign-in report ask before the page opens. The optional content group also asks when a draft is stored. Groups can nameUserGroups[]; Setup > Verify it's you edits every siteaccess or one (setup/reauth/<siteaccess>) and picks the user groups of the content group.
Added
- Added: a passkey policy,
[PasskeySettings] Policy(optional, preferred, required) andPasswordSignIn, per site, user group and account, withexpPasskeyPolicyHandlerclasses for extensions. The last passkey cannot be removed while the account could not sign in without it. Web pages only. - Added: administrators with the policy
user/passkeys_managesee and revoke another account's passkeys (user/passkeys/<user id>). The list names each passkey's authenticator by its model id (AAGUID);Attestation,RelyingPartyIDMap[],AuthenticatorNames[]andConsoleSlots[]are new settings. - Added: security mails when a passkey is added, removed or revoked; audit events
access.passkey.revoke,.password_refused,.required,.suggested. - Added:
expReauthTriggerHandler([ReauthSettings] TriggerHandlers[]) andexpReauth::requireFresh()for extensions. - Added (sevenx_authentication_2fa): the second step of signing in offers a passkey beside the app's code; an account that must have a second step and has a passkey needs no app.
Tests
expReauthFactorsTest,expReauthGroupsTest,expPasskeyPolicyTest(no database): the combinations and that none weaker is offered, the delete and content groups,bulk,UserGroups[], trigger handlers, the settings page's scope, the policy decision and gate, the last passkey, authenticator names, the relying party per host, attestation, the passkey alerts, a sign-in that counts as a confirmation.
2026-10-29: console commands copy .htaccess and config.php into place and read and set config.php values
Added
./console exp:setup:htaccess [--static]copies.htaccess_root(or.htaccess_root_static) to.htaccess,./console exp:setup:configcopiesconfig.php-RECOMMENDEDtoconfig.php, and./console exp:setup:statussays which template each file comes from (bin/php/setupfiles.php). They replace thecp -vaof the installation chapters. Nothing is written without--apply; an existing file that differs is shown as a diff and kept unless--force; the old file is backed up tovar/backup/setup/<time>/first (mode and owner kept, directories 0700), and the new one is written to a temporary file and renamed into place. A new file gets the template's mode and, run as root, the template's owner, ascp -adoes;--mode,--owner=user[:group]and-v(prints each copy likecp -v) are there too../console exp:config:list,exp:config:get <NAME>,exp:config:set <NAME> <value>andexp:config:unset <NAME>read and change thedefine()settings ofconfig.php(bin/php/configphp.php). The file is read as PHP tokens and never run (expConfigPhpEditor): only the value of onedefine()changes, a documented//define( ... );line ofconfig.php-RECOMMENDEDis uncommented in place, any other name goes into a marked block; comments, order and formatting stay. Values are typed (bool, int, octal, float, string, null) from--type, the current or the documented value.config.php-RECOMMENDEDis the catalogue of known settings, with their sections and descriptions (expConfigPhpCatalogue); other names need--allow-unknown. Every write is a dry run without--apply, backed up tovar/backup/config/<time>/, checked withphp -land read back before it is renamed into place; aconfig.phpthat does not compile is refused. Values of names with PASS, SECRET, KEY, TOKEN, CREDENTIAL or PWD are masked unless--show-secrets.--format=jsonfor list and get.- Guide: Setting up .htaccess and config.php from the console; feature page, generated command pages, and the installation, upgrade, hosting, web server, security and developer pages name the commands, in English and German.
Updated
autoload.phpreads aconfig.phpthat does not compile as before (the parse error stops the request), except for the scripts that defineEXP_CONFIG_PHP_TOLERATEfirst:bin/php/console,bin/php/setupfiles.phpandbin/php/configphp.php. For them the file is checked with the PHP parser and left unread when it does not compile, so./console exp:setup:config --force --applycan replace a brokenconfig.php; the console prints a warning.config.php-RECOMMENDED: the file permission settings have a heading of their own, FILE PERMISSIONS.
Tests
expSetupFilesAndConfigPhpCommandsTest(no database): the editor finds set, commented and conditionaldefine()s, reads every value type, changes only the value, uncomments the documented line, adds to and removes from the block (an addition and its removal give the file back byte for byte), refuses conditions and duplicates; values are typed; strings with quotes and backslashes read back; every setting of the shippedconfig.php-RECOMMENDEDcan be set, read back and unset; copies, modes, backups, a failed check that changes nothing; the commands run in a child process against a throwaway root (both flags exit 2, dry run writes nothing, refusal without--force, masked secrets, a brokenconfig.phprefused).
2026-10-29: one copyright notice, Exponential Foundation and 7x
A historical shift. On 29 October 2026 the copyright of Exponential moved from 7x alone to the Exponential Foundation and 7x together, and the project replaced every notice of its own work with one notice in two forms:
- shown to users: Exponential Copyright © 1998-2026 Exponential Foundation and 7x
- in files and metadata: Copyright (C) 1998 - 2026 Exponential Foundation and 7x. All rights reserved.
The Foundation is named first on purpose: the nonprofit that operates the project is the steward meant to carry the
software's rights, its GPL license and its history through the twenty-year support window of the 6.x line,
independent of the fate of any one company; 7x, which leads the development, is the second, full holder. Until this
date the project's own notice was written as "7x and others", "7x / Exponential Foundation", "7x & Exponential
Foundation", "7x and the Exponential Foundation" or "7x" alone, depending on the file. The license did not change:
Exponential stays under the GNU General Public License, version 2 or any later version, and its text is untouched.
The full story: Copyright and copyleft
(German), and COPYRIGHT.md.
Updated
- Every place a user sees the notice shows the visible form, never translated: the footers of the admin and admin4
designs (and of admin3, admin4l and the editor, which include them) and their sign-in pages, the standard design
footer (plain, print, popup, full screen, login and OAuth layouts), the base design footers, the setup wizard, the
page of an undefined module, the fatal error page,
./console --version, the /ezinfo/copyright page, the help's documentation notice (also in its PDFs) and the system card. - Catalogues: in all 156
translation.tscatalogues the footer message (contextdesign/admin/page_copyright) is the new notice, and its translation is the notice itself; the three dead variants of the old "7x and others" notice that no template used any more are removed.translation-identical-allowlist.jsonlists the notice as kept in every language. settings/exphelp.ini[License]:CopyrightYears=1998-2026,CopyrightHolder=Exponential Foundation and 7x; the documentation notice (doc/eng-us/README-LICENSE.md) names the same two holders and no others. There is no separate "Exponential contributors" copyright: every contributor assigns the copyright in a contribution to the Exponential Foundation and 7x through the CLA; contributors are credited (changelogs, commit authorship, the recognition pages), and credit is not a copyright claim../console --versionsays "Written by the Exponential Foundation and 7x (se7enx.com)."; the about page says that its list of contributors is credit.- File headers: about 3,630 files carry "Copyright (C) 1998 - 2026 Exponential Foundation and 7x. All rights
reserved." in their
@copyright,// COPYRIGHT NOTICE:or comment header line. Only that line changed in each file;php -lpasses on every changed PHP file. LICENSEandLICENSE.mdname the new holders above the license text, which is unchanged;COPYRIGHT.mdrecords the notice, the holders, this history, the contributors and the third-party notices;README.mdhas a Copyright section;composer.jsonlists the Exponential Foundation and 7x as authors.- The metadata of the extensions shipped in the kernel (
ezinfo.phpandextension.xmlof exphelpsite, expservices, ezformtoken, ezjscore and ezoe) and the files./console exp:ini:move --create-extensionwrites use the header form. - Documentation, English and German: a new page, Copyright and copyleft, listed in the help index; every page that quoted or described the notice (licensing, legal, about, contributing, partner, press, trainer, procurement and FAQ pages, the extension developer guide, the coding standards, the extension metadata specification) states the new holders, and about 35 of them link to the new page.
- The contributor agreement (CLA) assigns the copyright in a kernel contribution to the Exponential Foundation
and 7x together, which become its owners, the Foundation named first as in the notice: the Exponential Foundation and 7x Contributor Licensing Agreement,
version 1.1.0, live on the CLA page since the same day, is the first version that assigns the copyright (section
4.1; section 4.3 keeps a license as a fallback). Version 1.0.1 had granted 7x a non-exclusive copyright license
(its section 4, from the Apache ICLA 2.0), not an assignment; contributions under a signed 1.0.1 stay licensed to
7x under it (section 12 of 1.1.0).
CONTRIBUTING.md("Copyright assignment (CLA)"), the checklist of.github/PULL_REQUEST_TEMPLATE.md,README.md,CODE_OF_CONDUCT.mdand every page that describes the CLA (the CLA explained, the CLA for students, the FAQs, the glossaries, the contributing, partner, supporter, press, trainer and procurement pages, in English and German) say so and point to version 1.1.0 (Exponential Foundation and 7x CLA v1.1.0.pdf).
Kept unchanged
- Every notice of another holder, as the GPL requires: eZ Systems AS (the original developer of the discontinued
eZ Publish, "Copyright (C) 1999 - 2014 eZ Systems AS" and about 1,390
@copyrightlines, below the project's line), the joint notice "7x / Brookins Consulting / eZ Systems AS" of the role operator, CJW Network, Mugo Web (LoveStack), Moxiecode and Tiny Technologies (TinyMCE), the OpenJS Foundation, Unicode (CLDR), Nokia and Trolltech (Qt Linguist sources), the KDE Artists, the Tango icons, the Free Software Foundation's notice in the license text, and the notices of the books. The list with the reason for each:tests/tests/bin/copyright-notice-allowlist.json. - History: the changelogs, the history pages and the release tables keep the notices as they read on their date.
Tests
CopyrightNoticeTest(testsuitequality, no database) reads every file of the repository and fails on any notice that names 7x or the Exponential Foundation in a form other than the two approved ones, unless the allowlist gives a reason; its own cases cover the approved forms, every old spelling and third-party notices.CopyrightFooterNoticeTest(testsuitedesign) checks the footers, sign-in pages, setup wizard, undefined module and error pages, the console, the copyright page, the help notice and the system card, and that no catalogue translates the notice.expHelpLicenseTestchecks the help's notice;IniMoveTestthe header the extension generator writes.
Operator note
Nothing to run. The extensions in repositories of their own (the se7enxweb extensions and their -ezpackage twins,
Exponential Velocity, the installers and site packages) follow in their own releases.
2026-10-29: the site packages no longer carry ezpaypal or bcwebshop
Removed
- The
sevenx_extensionpackage no longer carries bcwebshop: its files, file list and install items are gone (sevenx-ezpackage). The kernel's shop covers what bcwebshop added; its shop module and kernel override conflicted with it. bcwebshop 0.8.2 stays available on GitHub and Packagist for sites that keep it.
Updated
ezwebin_site,ezflow_site,ezdemo_site,sevenx_siteand their_cleanvariants no longer require theezpaypal_extensionpackage: Composer installs ezpaypal, and the package server's copy was written over it. The installers still activate ezpaypal and no longer activate bcwebshop.ezpaypal_extensionstays on the package server (and in its index) for older installations. The multisite installers (sevenx_multisite,cjw_multisite) never installed either and still activate ezpaypal.- Upgrading: item 23 of the list above. Existing installations are not changed.
2026-10-29: the documentation recommends rewrite rules in the virtual host over .htaccess
Updated
- Every page of the documentation that mentions
.htaccess(155 in English, 156 in German; not the generated reference pages and not the history) opens with the same warning:.htaccessfiles, once enabled withAllowOverride, are looked for, read and evaluated on every request, and the fix isAllowOverride Nonein the HTTPS virtual host with the rules in its configuration. Each box links to the measurement, the example virtual host for Apache, the configurations for nginx, LiteSpeed, Caddy, IIS and Velocity, and the control panel pages for Plesk, cPanel, DirectAdmin and ISPConfig, and says how to switch over. - Setting up .htaccess and config.php from the console
has the measurement: two throwaway Apache 2.4 instances with the rules of
.htaccess_root, without PHP, 9 rounds, medians. A static image four directories deep: 988 requests/s with.htaccessandAllowOverride All, 1,419 with the rules in the configuration andAllowOverride None(44% more). A page address rewritten toindex.php: 1,079 against 1,303 (21% more). exp:setup:htaccessprints one line naming the faster alternative and the example virtual host.
2026-10-29: debug comments no longer break a start tag
Fixed
- With
[TemplateSettings] Debug=enabledorShowTemplatePathComments=enabled, the administration showed a stray>before the title of the full view, before every attribute of the preview and the edit form, and<Name>in the title of the edit form (admin and admin4).parts/exp_content_direction.tplprints into a start tag (<h1 class="context-title"{include ...}>), and the engine wrapped it in its debug comments: the browser ended the tag at the first>of the comment and showed the rest as text. The templates that test its output withset-blockfor''got the comment, never an empty string, and added their right-to-left wrapper always. Clear the template and content caches after the update: templates cached with the comments keep them.
Added
site.ini [TemplateSettings] TemplatesWithoutDebugComments[]: templates that neitherDebugnorShowTemplatePathCommentswrap in comments, by their path belowtemplates/(a file of that path in any design or extension). The default listsparts/exp_content_direction.tpl. Add a template of your own that prints into a start tag.eZTemplate::templateAllowsDebugComments()answers for a template file; the interpreter and compiled templates ask it.
Tests
eZTemplateDebugCommentsTest(no database): a part printed into a start tag or captured withset-blockgets no comments withDebugorShowTemplatePathComments, interpreted and compiled; other templates keep theirs; the setting matches the path belowtemplates/in any design.
Merged pull request #342 (contributed by ekked).
2026-10-29: debug comments are left out wherever they break the output, system wide
Updated
- Every rendered output (
eZTemplate::fetch()) is checked once whileDebugorShowTemplatePathCommentsis on: a debug comment inside a start or end tag, an attribute value,<script>,<style>,<title>,<textarea>or another comment is taken out, and output that is not HTML (XML, where a comment before<?xmlis an error, JSON, plain-text mail, CSV, iCal) gets none (eZTemplate::removeMisplacedDebugComments()). No list is needed for those places; production, with both keys off, never runs the check. - A
set-blockorappend-blockthat captures a value rather than HTML (a name, a URL, attributes, plain text, nothing) gets no debug comments (eZTemplate::capturedText(), interpreted and compiled); captured HTML keeps them. With the comments on, the admin4l left menu asked for the templateparts/<!-- START ...(the navigation part, captured fromparts/a4_navigation_part.tpl), the Import/Export button and the menu zones were drawn although empty (their|trim|ne( '' )tests saw the comments), the plain-text part of the HTML mails carried them, and exp_adminui's avatarsrcwas a comment.parts/a4_preference_attributes.tpl(in the<html>tag of admin4 and admin4l) andparts/language_badge.tpl(in a script string) are now covered by the rule as well, soTemplatesWithoutDebugComments[]keepsparts/exp_content_direction.tplas its only default. eZTemplate::templateAllowsDebugComments()reads the list once into a lookup array and keeps each template's verdict: the check on every include is a key lookup, however long the list.- Templates compiled with
Debug=enabled(which compiles its comments into the template) get compiled files of their own, named with the state ofDebug,ShowXHTMLCodeand the list: switchingDebugor changing the list no longer needs a template cache clear, and production's compiled file names do not change.
Tests
eZTemplateDebugCommentsTest: a comment in a tag, an attribute value, a script, a style, a title, a textarea or another comment is taken out, comments between elements stay, XML, JSON, plain text and iCal get none, an unlisted part in a start tag loses its comments in the rendered page (Debug and path comments, interpreted and compiled), the verdict is a key lookup, the site.ini default, the compiled file name of Debug, a captured value (a name, nothing) has no comments and captured HTML keeps them.- Checked in a browser with path comments on (a sandbox, throwaway administrator): node views, the preview and the
edit form in admin4, admin4l, the classic admin design, admin2 and exp_adminui, light and dark, without a comment in
a tag, an attribute, a script, a style, the title or a textarea and without a stray
>.
2026-10-29: admin4 node view tabs switch and package compare marks (pull request #341, follow-up)
Updated
- The node view's tabs switch stands at the end of the meta line after the languages
(
parts/a4_tabs_switch.tpl;window_controls.tplleaves its own out withtabs_switch_elsewhere), the dark mode has more contrast and the side columns' headings clear the rounded corner. - Package compare's insert and delete marks take a quarter of the radius token, so a mark on one character stays a box.
Merged the follow-up commits of pull request #341 (contributed by ekked).
2026-10-29: page caching with LiteSpeed and OpenLiteSpeed, without a plugin
Added
.htaccess_rootcarries a commented block,## >>> LiteSpeed page cacheto## <<< LiteSpeed page cache, directly before the front controller rule. Uncommented (onesedline, given in the block), LiteSpeed Web Server (Enterprise) and OpenLiteSpeed keep a page formax-age(120) seconds and answer it without PHP, for aGETorHEADrequest without a query string, without a session cookie (eZSESSID...orPHPSESSID, after[Session] SessionNameHandler) oris_logged_incookie, and outside the administration,user/,shop/,notification/,layout/,ezjscore/,setup/and everycontent/view other thancontent/view/. It sits in<IfModule LiteSpeed>, so Apache and nginx never read it. The verbatim copies of the file (doc/eng-us/examples/htaccess, the Apache.htaccessexample pages) have it too.litespeed-page-cache.conf: the same block for a LiteSpeed Enterprise virtual host, Plesk's "Additional directives for HTTPS", cPanel's include files and DirectAdmin's custom HTTPD configuration. The OpenLiteSpeed virtual host example has a commentedmodule cacheblock and the commented cache rules before its last rewrite rule.- The guide Page caching with LiteSpeed and OpenLiteSpeed (German:
Seiten-Caching mit LiteSpeed und OpenLiteSpeed): when it
helps, what the kernel does that the cache must know (when a session starts, the two session cookie names,
is_logged_in, the defaultno-cacheheaders), every line of the block, LiteSpeed Enterprise, Plesk (and why a hand-madevhost_ssl.confis lost at the next regeneration), cPanel, DirectAdmin, OpenLiteSpeed and CyberPanel, checks withX-LiteSpeed-Cache, how it sits with the kernel's caches, choosingmax-age, purging (what exists today, and an example listener oncontent/cachethat sendsX-LiteSpeed-Purge, not part of the kernel), what must never be cached, troubleshooting, security, measuring and crawlers.
Updated
- Linked from the help index, the guides' table of contents and the operators' road, the caches guide, the hosting
pages for LiteSpeed, performance, caching layers and the slow site runbook (a new section on slowness under
crawlers), the Plesk, cPanel and DirectAdmin pages, the web server examples, and every
.htaccessspeed box (156 English, 155 German pages), in English and German.
Tests
- The address condition was checked with PCRE against sample addresses (content pages and
content/view/cached; the administration,user/,shop/,content/edit,content/search,content/treemenu,layout/,ezjscore/,setup/andnotification/excluded, also behind a siteaccess name), and thesedline was run on a copy of.htaccess_root. Not run on a LiteSpeed or OpenLiteSpeed server: there is none on the machine the documentation is checked on.
2026-10-29: Twig documentation: native templates, recipes, debugging, editors, compatibility and speed
Added
- Writing Twig-native templates for Exponential (German:
Twig-Templates für Exponential nativ schreiben): the two
ways a
.twigis found (the design resource with override rules, Twig's own loader with the design fallback), template inheritance together with override rules,include,embedandexpinclude, macros, filters, functions and tests,wash, autoescape and|raw(and whywashunder autoescape escapes twice), whitespace control, what the sandbox allows and refuses, the variables each kind of template sees, the design keys throughexpvar(). - Twig templates: recipes (German: Rezepte): a page layout, a line view, a block of Exponential Layouts, a mail template whose subject the module reads back, cache blocks per role and per user, a form with the form token, texts with a count, images in an image alias.
- Debugging Twig templates (German:
Fehlersuche):
exp:template:checkwith every Twig rule, the message in the page read part by part, the messages and their fixes, template path comments and the debug bar, thetwig profilenotice row by row, Twig's debug mode, a change that does not show. - Twig templates in your editor (German:
im Editor): highlighting and file endings, what an editor's
Twig checks cannot know, the checker in Visual Studio Code and JetBrains IDEs, the ignore comment, formatting,
.editorconfig, the conversion marker, snippets. - Twig templates: versions, upgrades and compatibility (German:
Versionen, Upgrades und Kompatibilität):
the Twig package the PHP floor metapackage installs on each PHP line (
twig/twig^3.27on PHP 8.1 and later,se7enxweb/twigon 8.0 and 7.4), whytwig/twigcannot be in the kernel's owncomposer.json(advisories up to 3.26, 3.27 and later need PHP 8.1) and why an installation on PHP 7.4 or 8.0 must not add it, what happens when Twig is missing, upgrading Twig, PHP and Exponential, Velocity (one environment per worker, class names, the cache permissions of compiled templates). - Speed of the three template engines (German:
Geschwindigkeit der drei Template-Engines): Twig,
Smarty and Exponential templates measured on 29 October 2026, cached (600 requests per page, engine and server),
uncached, side by side in the same rounds and as the debug render, each with its date, load and data file; why cached
pages are engine-independent and why
.tplis slower in a full render; a profile of where a request spends its time; what is in progress and not shipped; how to repeat the measurements withexp:benchmark. - Templates side by side: Exponential templates, Twig and Smarty (German: Templates nebeneinander): the same template in the three engines, every row converted by the kernel's converters and rendered with identical output, and how every operator and template function is written in Twig, the operators of the fast path included.
- Glossary entries for Smarty templates, template engine and Twig templates (English and German).
Updated
- The Twig performance guide (English and German): the direct static calls for every bridged operator and function, the quieter cached run of the afternoon, links to the comparison of the three engines.
- The three template engines compared, the Twig feature page, the guides index, the features index, the template
language index, the Smarty guides and side-by-side page, the moving-to-Twig chapter and the benchmarking guide link the
new pages; German indexes and
.paths.jsonlikewise.
2026-10-29: the GraphQL API, phase 1: reading content
Added
- Added: The GraphQL API of the kernel at
/api/graphqlin every siteaccess where it is switched on (settings/graphql.ini [GraphQLSettings] Enabled=enabled, optionallySiteAccessList[]; off by default). The schema is generated from the content classes: a type per class implementing the interfaceContent, a field per attribute, the root queriesnode(by node id, remote id, path or URL alias),content,children,currentUser,siteaccessandtranslations, lists withlimit/offsetand withfirst/aftercursors, images in any image alias withvariations, rich text ashtml,text,xmlandexcerpt, relations, alanguageargument. The datatypes of the kernel have types of their own; every other datatype is aString. Every query runs with the rights of its user: an OAuth token, a personal API key with the new scopegraphql:read, the session, or the anonymous user. Guide: The GraphQL API (German: Die GraphQL-API). - Added: Naming styles for the schema (
NamingStyle=identifier,pascalormapwith[GraphQLNaming]), the suffix_fieldfor an attribute named like a field ofContent, the suffixContentfor a class named like a type of the schema, and a schema hash in every answer (extensions.schemaHash, the headerX-GraphQL-Schema). - Added: Limits checked before a query runs (
MaxDepth,MaxComplexity,MaxListLimit,DefaultListLimit), introspection for signed-in requests only by default (Introspection), classes, attributes and datatypes kept out of the schema (ClassList[],ExcludedClassList[],HiddenFieldList[],HiddenDatatypeList[]: user accounts, user groups, settings classes,ezuserandezpasswordby default), errors withextensions.codeandextensions.requestId, andDebugfor a development siteaccess. - Added: Setup > GraphQL (module
graphql, policygraphql/playground): the end points, the library, the schema and a playground that runs queries with the rights of the signed-in user through the session end pointgraphql/query(policygraphql/query,POSTwith the form token inX-CSRF-Token). - Added: The console commands
exp:graphql:status,exp:graphql:schema(sdl,json,summary) andexp:graphql:query(a query, a.graphqlfile or standard input,--variables,--as,--format=table), with names, options, exit codes and machine output kept as a stable contract. - Added:
webonyx/graphql-php ^15in the rootcomposer.json(installs on PHP 7.4 through 8.5), and[GraphQLSettings] LibraryPathor the environment variableGRAPHQL_LIBRARY_PATHfor an installation that keeps the library in a directory of its own. - Added: The generated reference pages of
graphql.ini, the three commands, the views and policies of the modulegraphql, the newrest.inisettings and the extension points ofgraphql.ini(English and German), with their.paths.jsonentries; glossary entries for GraphQL (English and German).
Updated
- Updated:
rest.ini:ProviderClass[graphql]=expGraphQLRestProvider, the scopegraphql:readwith its route,[RouteSettings] OptionalAuthentication[](a route authenticated only when the request presents credentials; a wrong or expired token is still refused) and[expGraphQLRestController_CacheSettings](no application cache for an answer). - Updated: The REST prefix filter takes the address of a provider without a slash after it (
/api/graphql) whenProviderClass[]registers a provider of that name; any other single element stays the default provider's route. - Updated: Personal API key scopes may carry a namespace (
graphql:read). - Updated: The features index (English and German) links the GraphQL page.
2026-10-30: the GraphQL API, phases 2 and 3: speed, caching, limits; search, tags, layouts, more datatypes
Added
- Added: Persisted queries for the GraphQL API: queries stored on the server (
var/<site>/graphql/persisted/) and called by name (GET /api/graphql?id=articles) or by SHA-256, Apollo's automatic persisted queries (extensions.persistedQuery.sha256Hash, stored for signed-in requests by default:AutomaticPersistedQueries,MaxPersistedQueries), queries shipped in directories (PersistedQueryDirectoryList[]), and a mode that runs persisted queries only, for anonymous requests or all (PersistedQueriesOnly). The new commandexp:graphql:persistchecks.graphqlfiles against the schema and stores them (--apply), lists (--list) and removes them (--remove);exp:graphql:query --idruns one. New error codesPERSISTED_QUERY_NOT_FOUND,PERSISTED_QUERY_NOT_SUPPORTED,PERSISTED_QUERY_REQUIRED; the longest query is now a setting (MaxQueryLength). - Added: Rate limits for the GraphQL API:
RateLimitrequests per signed-in user or personal API key (600) andRateLimitAnonymousper anonymous IP address (120) inRateLimitWindowseconds (60), counted in files undervar/cache/graphql/rate/by every web server worker alike; over the limit the answer is429withRATE_LIMITEDandRetry-After, and every answer carriesX-RateLimit-*headers.exp:graphql:statusshows the limits and the persisted queries. - Added: A response cache for the GraphQL API (
ResponseCache,ResponseCacheTtl): the answers of persisted queries, kept per siteaccess, schema, user rights, variables and address invar/cache/graphql/responses/. An anonymousGETof a persisted query is answered at the start ofindex_rest.phpbefore the kernel starts, still counted against the anonymous rate limit. The cache expires whenevereZContentCacheManagerclears view caches (publishing), when a class changes, when persisted queries change, and with the new cache itemgraphql(ezcache --clear-id=graphql, tagscontentandini). Every answer has anETagandIf-None-Matchgets304;X-GraphQL-CachesaysHIT,MISSorBYPASS. A signed-inGETis answered early as well once the kernel has checked its token or key: the rights of the credentials are rememberedCredentialCacheTtlseconds (60) and forgotten when an API key is revoked or removed, a role changes or the user caches are cleaned up; a CORSGETkeeps theAccess-Control-*headers of its origin. Measured with PHP-FPM 8.5 and OPcache and alpha's extensions (50 articles with image, owner and address, 3 x 250 requests): cachedGETanswers median 1.1-1.7 ms, p95 2.1-3.2 ms, at most 6.6 ms; a cached answer through the kernel (POST) median 33.0 ms; uncached median 93.0 ms. - Added: The GraphQL API's error messages in the language the request asks for with
Accept-Language(a catalogue of the administration, by full code or language), else in the siteaccess's language (expGraphQLMessage, contextkernel/graphql); the depth, complexity and introspection refusals of the library in the kernel's own words.extensions.codestays the same in every language. - Added: GraphQL types for the datatypes of extensions and the shop:
ezgmaplocation(GeoLocation),ezsrrating(Rating),xrowmetadata(Metadata),ngenhancedlink(EnhancedLink, with the linked content),sckenhancedselectionandngclasslist(lists of identifiers),ezoption(OptionList),ezmultioption(OptionGroup),ezrangeoption(Range),ezmultiprice(CurrencyPrice),ezenum(EnumValue),ezsubtreesubscription,ezproductcategory. - Added: Schema extensions for the GraphQL API (
[GraphQLSettings] SchemaExtensionList[],expGraphQLSchemaExtension): an extension adds root queries, fields ofLocationand the types of its datatypes where it is available; its name is part of the schema hash, and a batch loader of its own is one call away ($ctx->loader( kind, language, batch )). - Added: The root query
searchof the GraphQL API: the search engine of the installation (the kernel's, or Solr through ezfind) bytext,classes,subtree,sections,publishedAfterandpublishedBefore, with the pagination of every list,totalCount, theenginethat answered and, from Solr,facetswith their values (typeSearchResult). - Added: Tags in the GraphQL API while eztags is active (the schema extension
expGraphQLTagsExtension, inSchemaExtensionList[]by default): the root queriestag(by id, remote id or path of keywords) andtags(below a tag, or by keyword), the typeTagwith its keyword in a translation, parent, path, children, synonyms, main tag, address and the content tagged with it (eztags' attribute filter, as the user may read it), and theeztagsfields of content as[Tag!]!. The root queries need the policytags/read. - Added: Layouts of the page builder in the GraphQL API while explayouts is active (the schema extension
expGraphQLLayoutsExtension, inSchemaExtensionList[]by default): the root querylayout(by id, identifier, or the URL alias of a page through the rules of explayouts) andLocation.layout, withLayout,LayoutZone(a linked zone shows the shared zone's blocks),LayoutBlock(definition, view types, parameters, collection type, the content placed in a manual collection as the user may read it, the blocks of a container) andLayoutBlockParameter. Only published layouts of the site; parameters and collections of a level in one batch each.
Updated
- Updated: The GraphQL API reads a page in groups (the DataLoader pattern,
expGraphQLBatchLoader): the owners, the parents and the related objects of every item of a level are read in one query each, the URL aliases of a page in two (eZURLAliasML::primePathsForNodes()), and a value once read is answered from memory for the rest of the request. 50 articles with their image, owner and address cost 11 SQL statements instead of 249. The content classes of the schema come from a cache file per siteaccess (var/<site>/cache/graphql/), renewed when a class is stored, and the name of an item is read from its own row instead of one query per item. - Updated: The GraphQL API's caches, rate counters and persisted queries are written with directories 0777 and files 0666, so PHP-FPM and Velocity, which run as different users, can both renew and count them.
2026-10-30: the GraphQL API, phase 4: mutations
Added
- Added: Mutations of the GraphQL API, on together with the end point (
[GraphQLSettings] MutationsEnabled=enabled, the default;disabledper siteaccess for a read-only API):createContent,updateContent(a new version, published or left a draft withpublish: false),translateContent,publishDraft,deleteContent(one location or every location; to the trash by default),moveContent,hideContent,revealContent,addLocation,uploadImage,uploadFromUrlandcreatePreviewToken, each answering aContentPayload(status,contentId,nodeId,version,content). Each checks the policies of the user on the content it changes (NOT_FOUNDfor content the user may not read,FORBIDDENfor a change it may not make), runs the kernel's operations so their workflows run (PENDINGfor a publish an approval holds,CANCELLEDfor one a workflow cancels), and never writes a class the schema does not show, a hidden field or datatype, or a top-level location. Guide: Mutations. - Added: Who may send a mutation:
POSTonly; signed in (AnonymousMutations=enabledlets anonymous requests through to the Anonymous role's policies); a personal API key with the new scopegraphql:write(rest.ini Scopes[graphql:write]=content/edit); the session end point with the form token inX-CSRF-Token. - Added: The audit names
content.graphql.mutation(the mutation, how it was signed in, the request id, the key, the field identifiers, never their values; the kernel's own events of the change are its children) andcontent.graphql.mutation.failed(refusals and errors, with the reason), both on by default. - Added: Files in mutations by the GraphQL multipart request specification (the scalar
Upload, also inside aFieldInput):MaxUploadSize, the checks ofUploadCheckList[](expGraphQLUploadCheck, a virus scanner's hook; a missing check refuses every file), andFileDatatypeList[]; a file field never takes a path as its value.uploadFromUrlfetches overhttpsfrom the hosts ofUploadFromUrlHostList[]only (empty by default: off). - Added:
FieldInputwithformat: TEXTandformat: HTMLfor rich text, checked against the class before anything is written (unknown, hidden or doubled fields, required fields of new content). - Added: Drafts and preview tokens:
content(id:, version:)for a draft of one's own or withcontent/versionread;createPreviewTokenand the querypreview(token:), a token signed with the installation's secret key, validPreviewTokenTtlseconds (at mostMaxPreviewTokenTtl), readable without signing in.
Updated
- Updated: A mutation that changed something expires the GraphQL response cache, and the answer of a mutation is never
kept. The error codes
FORBIDDENandNOT_FOUND, reserved until now, are sent by mutations and preview tokens. - Updated: The GraphQL API no longer reads an object in the trash as content (
content(id:)and relations). - Updated: The schema version of the GraphQL API is 3; the schema hash changes when mutations are switched on.
2026-10-30: the GraphQL API, phase 5: switching it on in one command, keys, Setup > GraphQL
Added
- Added:
exp:graphql:enable: the GraphQL API switched on in one command. It checks that the schema builds, setsEnabled=enabledandSiteAccessList[](--siteaccess) insettings/override/graphql.ini.append.php(the mutations come on with it;--mutationsswitches them on again where they were switched off and gives a keygraphql:write), lets a front end's origin call it from a browser (--origin,rest.ini [CorsSettings]), makes a personal API key for a user (--key-for,--key-name,--scope,--days; the token is shown once), and prints the end point and acurlline. A dry run (the diff of every file) unless--apply; the files are written by the kernel's INI editor (a backup first, the audit'ssystem.setting.write), and the INI cache is cleared. Guide: Quick start. - Added:
exp:graphql:disable: the API, or only its mutations (--mutations), switched off everywhere or in the siteaccesses of--siteaccess(taken out ofSiteAccessList[], since settings/override wins over a siteaccess's file); a dry run unless--apply. - Added:
exp:graphql:keys: the personal API keys with a GraphQL scope, listed (--format=json, schemaexp-graphql-keys/1), made for a user (--create --for=, checked against the user's policies and the Scope limitation ofapikey/create; recorded asaccess.apikey.create) or revoked (--revoke=,access.apikey.revoke); a dry run unless--apply. - Added: Setup > GraphQL shows whether the API and its mutations answer in every siteaccess (with switches), the limits,
the response cache (with its answers, bytes and last expiry, and a button to clear it), the persisted queries (with
removal of a stored one) and the API keys with a GraphQL scope. The switches need the new policy
graphql/administrate, which also shows every user's keys. admin4 components and tokens only, light and dark.
Updated
- Updated:
exp:graphql:statustells whether the mutations are on; its status comes fromexpGraphQLSetup::status(), shared with Setup > GraphQL.
2026-10-30: the GraphQL API, phase 6: starter kits, recipes, a walkthrough
Added
- Added:
exp:graphql:starter: a starter front end generated into a new directory (--kit=html|next|nuxt|astro|expo,--to=,--endpoint=,--list) from templates shipped inshare/graphql/starters/and the live schema: the kit's files with the end point filled in, and the queriesMenu,Node(a page by its URL alias) and one per content class with every field of the class (images in the alias large, rich text as HTML, relations by name and URL alias), as a module of the kit and as.graphqlfiles forexp:graphql:persist. Every generated query is checked against the schema and its limits before anything is written; nothing is installed or deployed. A dry run unless--apply. - Added: The kits: plain HTML and JavaScript (no build step), Next.js (App Router, server components; the key stays on the server), Nuxt 3 (its own server route adds the key), Astro (a static build that walks the content tree) and React Native with Expo.
- Added: The guides A front end on the GraphQL API, step by step and GraphQL recipes (a blog, a mobile app, a static build, an AI agent; the queries run against the demo content), in English and German, with the generated page of the command.
2026-10-30: content jobs wait out a busy database
Updated
- Updated: The content job worker retries a batch that meets a busy or locked database (SQLite busy or locked, the
transaction start that waited
SQLiteTransactionWaitin vain; MySQL lock wait timeout and deadlock): the batch is rolled back, the job read again from its last checkpoint, the type'sprepare()run again, and the batch tried again after a pause of 1, 2, 4 ... seconds while the heartbeat is written. New settingscontent.ini [ContentJobSettings] BusyRetries=8andBusyBackoffMax=60. Before, the start of the batch's transaction was outside the error handling: on a busy SQLite database the worker process ended and left the jobrunningwithout a worker, which is how the help manual builds on the demonstration servers stopped at 77% and 5%. - Updated: A content job whose busy retries ran out fails with a temporary error, and the cronjob part
contentjobsresumes it by itself, at mostMaxAttemptstimes in a row (counted again from 0 after every batch done). Any other error of a worker fails the job with "the worker stopped: <reason>" instead of ending the process unannounced. - Updated: The help manuals' content job builds its manual sync again after a rolled-back try
(
expHelpManualSync::abandon()), so a retried batch makes nothing twice. - Updated:
bin/php/expcontentjob.php listshows the age of each job's heartbeat and "stalled" for a job that waits without a worker;list --resumeresumes every stalled or failed job in the background.
2026-10-30: the GraphQL API's mutations come on together with the end point
Updated
- Updated:
settings/graphql.ini [GraphQLSettings] MutationsEnabledshipsenabled: the mutations come on together with the end point, and every fallback in the code agrees. A read-only API switches them off per siteaccess (./console exp:graphql:disable --siteaccess=<name> --mutations --apply, the button Mutations off of Setup > GraphQL, orMutationsEnabled=disabledin the siteaccess'sgraphql.ini.append.php);exp:graphql:enable --mutationsswitches them on again and gives a keygraphql:write. Every guard stays:POSTonly, a signed-in user (AnonymousMutations=disabled), a personal API key withgraphql:writeor the session's form token, the user's policies on the content changed, the workflows and the audit. The GraphQL pages, the walkthrough and the generated pages of the setting and the commands say so, in English and German. - Updated: Setup > GraphQL names the siteaccess whose settings it shows: the facts of the end point say "For the siteaccess admin, which shows this page", and where the API is off there but answers in another siteaccess the notice says so and points to the table, instead of reading as if the API were off everywhere. The Mutations fact shows on only where the API answers too. Two new texts in all 155 catalogues.
- Updated: The Remove button of a persisted query on Setup > GraphQL no longer has an empty bordered box beside it:
the in-place confirmation (
.a4-confirm) in a row of actions (.a4-actions) is as wide as its summary until it opens, as it already was in a toolbar. - Updated: The Spanish texts of Setup > GraphQL (esl-ES, esl-MX) address the user with tú, as their catalogues' guides require.
2026-10-30: the SQL query cache no longer misses tables of a statement
Fixed
eZDBQueryCache::readTables()stored a result for too few tables, so a write to one it missed left the result stale untilMaxAge: a derived table inFROM(FROM ( SELECT ... FROM a ) x, b JOIN cwas read asconly), an index hint orPARTITION (p)before a comma list,LATERALandONLY, a parenthesised join, a comma list after a join (ON ...,USING (i), an alias),STRAIGHT_JOIN, a quoted name with a space, the secondSELECTof Oracle'sMINUSand ofINTERSECTandEXCEPT, any comment, Oracle'sq''and PostgreSQL'sE''literals, MySQL's double-quoted strings, and a backslash at the end of a literal on Oracle, PostgreSQL and SQLite, where it does not escape ('C:\'). Such statements are now read correctly or not cached, and a regular expression that fails (PCRE limits) makes the statement not cacheable instead of reading only part of it.writtenTables()uses the same literal and comment rules (unreadable: everything stale). NeweZDBQueryCache::backslashEscapes( $db )tells from the driver'sdatabaseName()how its literals end;readTables()andwrittenTables()take it as an optional second argument. Oracle'sSAMPLE (n)is volatile.
Tests
eZDBQueryCacheTestQC-38.
2026-10-30: the SQL query cache's state file and key, robust
Fixed
- A state file (
var/<site>/cache/querycache/state.ser) that was deleted, with anrm -rof the cache directory, or could not be read made every result still in APCu or in the result files current again, although the writes the file knew were lost. The state now carries anid, set when a file is written where there was none; results are answered only under the id they were stored under. The first write of a new installation therefore makes the results stored before it stale, once; a miss now reads the state. - A write that could not open or take the state lock went on without it, so another process's change could be lost. It
now makes everything stale, which narrows that window without closing it, and logs it once per request; the lock
must work (writable
state.lock, NFSv4 orlockdon NFS). A failedchmod()of the new state file no longer counts as a failed write (the check was always true), and a temporary file that could not be renamed is removed. - The key of a statement carries the database user, port and socket: two sites on one server with one APCu and the same database name (two Oracle schemas on one service) answered each other's rows. Results stored before the update are not answered again (new keys).
- The request's memo keeps at most
eZDBQueryCache::MEMO_MAX(1,000) results, the least recently used going first; a long script (an import, a cronjob) kept every result it read, and every write walked all of them.
Tests
eZDBQueryCacheTestQC-39 to QC-42; QC-04 and QC-22 start from an existing state file.
2026-10-30: the SQL query cache on PostgreSQL and SQLite, in a persistent worker, and its lock failures
Builds on pull requests #343 and #344 (contributed by fwoldt).
Updated
- Updated:
eZDBQueryCache::readTables()andwrittenTables()read the statements of PostgreSQL and SQLite explicitly: a backslash does not escape there, so'C:\'is a whole literal and the statement stays cacheable;"..."quotes a name, which is kept ("public"."ezuser"isezuser), and#is no comment (PostgreSQL: an operator). Not cached any more on any database: a quote left unpaired (a literal that does not end) and a quoted name that holds a quote ("it's",`it's`, SQLite's[it's]), whose apostrophe paired with the next literal's quote and hid the tables in between; PostgreSQL's dollar quoting ($$...$$,$tag$...$tag$, not$1). A PostgreSQL connection withstandard_conforming_stringsoff escapes with a backslash as MySQL does: neweZPostgreSQLDB::standardConformingStrings()reads the setting from the connection (no statement), andeZDBQueryCache::backslashEscapes()asks it. - Updated: Writes on PostgreSQL and SQLite:
UPDATE ONLY a,DELETE FROM ONLY aandALTER TABLE ONLY amarked a table calledonlyand leftacurrent; they markanow.TRUNCATE a, bandDROP TABLE a, bmark every table of the list (only the first before),ALTER TABLE a RENAME TO bboth names;TRUNCATE ... CASCADEandDROP ... CASCADE, which reach tables the statement does not name, make everything stale. A name in a write's table list that cannot be read makes the whole write unreadable (everything stale): before, it was left out and its table never became stale. - Updated: Not cached either, as not a function of their tables: PostgreSQL's
clock_timestamp(),statement_timestamp(),transaction_timestamp(),timeofday(),txid_current(),setval(),gen_random_uuid(),current_setting(), everypg_*()function,FOR NO KEY UPDATEandFOR KEY SHARE; SQLite'schanges(),total_changes(),last_insert_rowid(),randomblob()and its date functions without an argument; the literal'now'(and'today','tomorrow','yesterday','localtime'). - Updated: The state lock is taken on the file at the path: a process that waited for
state.lockwhile the cache directory was deleted got the lock on the deleted file while the next process locked a new one, and both wrote the state. The file held is now compared with the one at the path (inode and device) and opened again when they differ. - Updated: A write that cannot take the state lock is shown to the admin, not only logged: the time of the last
failure is kept in the state file (
lockFailed), so Setup > System information shows it in red for any process's failure (admin4 and admin,.a4-notice.is-bad, no inline CSS), with the lock file to check; the debug output's query cache report has a row State lock, and the Exp Debug bar's QC chip turns red. Clearing the query cache forgets it. One new text in all 155 catalogues. - Updated: The persistent worker (Velocity) was checked: the per-request reset drops the worker's copy of the state, the memo, the settings and the "lock failed" note, so every request reads the state file again and another process's write or a replaced state file counts from the next request on. The SQL query cache pages (English and German, BC note and help page) say so, and the German pages carry everything #343 and #344 added.
Tests
eZDBQueryCacheTestQC-43 to QC-47 (QC-45 forks a second process and needs pcntl);expDebugBarQueryCacheTestDQ-01 checks the red chip.
2026-10-30: "Was this you?", Secure your account and suspicious activity
- Added: The security activity (
user/security) asks Was this you? about every event of the last 30 days that needs attention on the own account: Yes, it was me remembers the answer (user preferenceexpsecurity_confirmed, recorded asaccess.user.activity.confirm), No, secure my account opens the new card Secure your account. - Added: Secure your account on the security activity: after "Verify it's you"
(
[ReauthGroup_account]Rules[]=user/security self SecureButton) every other session of the account is signed out, also those kept by Remember me, and the page leads on to Change password. The link Secure your account of the New sign-in mail and the security alerts isuser/security/(secure)/1, which opens the page with that card first. In admin, admin4 (and so admin4l and editor) and the standard design of the public sites. - Added: Suspicious activity in the last 24 hours on the sign-in report (
user/sessionreport): one network failing on three or more accounts, accounts locked, accounts withFailedSignInAlertThresholdfailed sign-ins, and accounts signed in from several countries at once, each linking to its security activity and sessions. - Updated: The texts of these pages in all 155 catalogues; the guides and feature pages in English and German.
2026-10-30: the Editor role gets the interface choices and the media site's classes, without duplicates
Updated
- Updated: The setup step,
bin/php/addeditorpolicies.phpand the upgrade step6.0.15-editor-policiesalso adduiswitch/languageanduiswitch/designto the Editor role where they are missing (each while its provider is inuiswitch.ini ProviderList[]). - Updated: The class sets of the new
settings/editorpolicies.ini: for the classes of a set that the Editor role cannot create, onecontent/createlimited byClassandParentClassand onewebsitetoolbar/uselimited byClassare added. The shipped setmedia_sitecovers the media site'sng_*classes below a folder, the front page and the set's own containers; an installation without them is not touched.--skip-class-set=<name>leaves a set out. - Updated: A policy the Editor role holds twice with the same limitations (such as
ezoe/*) is removed once, the oldest kept ([EditorPolicySettings] RemoveDuplicates,--keep-duplicates). Upgrade note: The everyday policies of the Editor role.
2026-10-30: the GraphQL API, phase 7: real time, signed webhooks and subscriptions
Added
- Added: The GraphQL event journal (
var/<site>/graphql/events/,graphql.ini [GraphQLEvents]): the kernel notes publish, update, trash, delete, move, hide, reveal and successful GraphQL mutations wherever content changes, and writes them at the end of the request, after its transaction, while the API is on and somebody listens. - Added: Signed outgoing webhooks: end points in
graphql.ini [GraphQLWebhooks] EndpointList[]and[GraphQLWebhook_<name>](events, siteaccesses, classes, subtrees,Payload=eventor the answer of a persisted query), POSTed by the new cronjob partgraphqlwebhooks(in the frequent group) withX-Exponential-Signature: sha256=<HMAC-SHA-256(secret, timestamp + "." + body)>andX-Exponential-Timestampagainst replays, retried with a doubling back-off, given up afterMaxAttempts, every attempt in a delivery log. The signing secrets live in the secret store, never in a settings file or a log. - Added: The console command
exp:graphql:webhooks(list,--add,--remove,--test,--deliver,--log; a dry run unless--apply) and a Webhooks section in Setup > GraphQL (add, test, remove, the recent deliveries). - Added: GraphQL subscriptions (
Subscription.contentEvents,contentPublished,contentUpdated,contentDeleted,contentMoved,contentHidden, the typesContentEventandContentEventKind) at/api/graphql/subscribe: an event stream in the protocol of graphql-sse with PHP-FPM and Velocity, and a WebSocket with graphql-transport-ws on Velocity (velocity.ini [ServerSettings] GraphQLWebSocketPaths[], Velocity after v0.0.4.58). Every event is checked against the subscriber's rights when it happens; a stream holds no database connection while it waits; limits per installation, user and address, a heartbeat, a maximum length and an idle timeout (Subscriptions,AnonymousSubscriptions,MaxSubscriptions*,Subscription*), and the error codeSUBSCRIPTION_LIMIT. - Added: The documentation of the real-time API, its security notes and a recipe (a webhook for a shop and a live list in the browser), in English and German; the generated pages of the command and the new settings.
Updated
- Updated:
/api/graphqlrefuses a subscription and names/api/graphql/subscribe; the schema hash changes with the subscriptions;ContentEventandContentEventKindare reserved type names. The REST response writer sends nothing more for a response that was streamed.
2026-10-30: the Editor role gets ezie/* where ezie is active, and only active extensions count in the setup step
Updated
- Updated:
expEditorPoliciesknows ezie's module: where a site keeps ezie active, the Editor role getsezie/*(ezie'smodule.phphas no policy functions, so every view asks for it; each view still checks the draft), andbin/php/addeditorpolicies.php --apply --only=ezie/*adds it on an installation that exists. Where ezie is not active it is skipped ("no module ezie on this installation"). - Updated: The setup step (
expEditorPolicies::ensure(), newsetupModules()) counts an extension's module only when the new installation activates the extension, not when it is merely inextension/: ezie is installed with every installation, soezie/*is not granted where expimageeditor takes its place. - Updated: The Editor role note explains why
ezie/...answers "module not found" (kernel error 20) for every user on a site that activates expimageeditor in place of ezie: an inactive extension'smodule.iniis not read, so the module is not on the module path. No page of such a site asks for ezie (Editor role).
2026-10-31: Your devices and Security activity follow a design's own light and dark switch
Updated
- Updated:
design/standard/stylesheets/user_sessions.css(Your devices, Security activity and the link to them on the profile) follows the mode a site design marks on<html>--data-theme,data-bs-theme(Bootstrap 5.3) ordata-exp-theme(the media design) -- and the system'sprefers-color-schemeonly where there is no such mark. A site with its own switch set to light on a dark device showed these pages dark. Documented as the integration point for theme developers (Dark mode and design tokens in themes, German too) and in the problems of Your devices.
2026-10-31: a stored GraphQL query with variables is checked for its worst call
Updated
- Updated:
exp:graphql:persistandexp:graphql:startercheck a query's complexity with the values of its variables that make it highest (expGraphQLServer::worstCaseVariables(): an Int atMaxListLimit, an@includethat runs, an@skipthat does not skip), as no request gives them values yet. Every query with a required variable was refused with "Variable "$path" of required type "String!" was not provided":exp:graphql:starterwrote no kit (itsNodeandContent_*queries), and step 8 of the walkthrough could not store a kit'sgraphql/. A query that one call could take pastMaxComplexityis still refused, and a request still checks the values it sends. - Updated:
/api/graphql/subscribecomputes a subscription's complexity with the variables of the request.
2026-10-31: the GraphQL library is optional again, so the platforms install
Removed
- Removed:
webonyx/graphql-phpfrom therequireof the kernel'scomposer.json(added on 2026-10-29). Installations that pull the kernel'sdev-mainthrough a platform could not be resolved any more: Exponential Platform Legacy 3.x (ibexa/graphql-php0.13, which replaceswebonyx/graphql-php, on PHP 8.0 and later) and Exponential Platform Legacy 4.6.x on PHP 7.4 (se7enxweb/graphql-php14.11 throughexponential-platform-dxp-php-floor). The library is now listed undersuggestandrequire-dev(^15.32.3, the releases no security advisory affects): the kernel's own Composer set-up keeps installing it, a platform keeps its own.
Updated
- Updated:
expGraphQL::loadLibrary()accepts onlywebonyx/graphql-php15 (it checks forGraphQL\Validator\QueryValidationContext): with a platform's 0.13 or 14 loaded, the API reports the library as incompatible instead of running on it. New:expGraphQL::libraryState()(available,missing,incompatible),libraryProblem()andfoundLibrary();exp:graphql:statusand its JSON (libraryState,libraryProblem, added fields) say why the API cannot run (FAIL GraphQL API unavailable: ..., exit code 3),/api/graphqlanswers503. - Updated: The boot smoke test (
tests/bin/bootsmoke.php, run by CI with PHP 7.4 and 8.0 after a--no-devinstall, so without the library) checks that the GraphQL status degrades; the new testexpGraphQLLibraryMissingTestchecks the missing and the incompatible library, the status and the end point's answer. - Updated: The GraphQL API (German: Die GraphQL-API), the requirements (Requirements, German: Voraussetzungen) and the GraphQL walkthrough say which installations have the library and why the kernel does not require it.
- Updated: Twig templates: versions, upgrades and compatibility
(German: Versionen und Kompatibilität)
names exponential-php-floor 1.2.5 and 1.3.5, which accept
twig/twig^3.27 || ~3.19.1, so a platform that keepsse7enxweb/twig3.19.1 on PHP 8.1 and later (Exponential Platform Legacy 3.x, 4.6.x) installs the kernel's current code. The kernel's own constraint on the floor (~1.0.3 || ~1.1.4 || ~1.2.3 || ~1.3.3) is unchanged.
2026-10-31: the move from ezwt to expwt grants each expwt policy once and leaves one expwt active
Updated
- Updated:
exp:wt:migrate-from-ezwtand the upgrade step6.0.15-expwt-enableplan one expwt policy per function for a role with severalwebsitetoolbar/usepolicies: identical limitations give one grant, limitations that differ in one limitation's values only are joined (twoClasslists become one), and sets that cannot be joined without granting more stay one grant each. A policy the role holds already, the same or broader, is not granted again, so a second run plans nothing. Each line names the limitation values ("limited to Class 1, 2, 5") and the source policies, in place of the limitation's name and the source function (the website sidebar). - Updated: Where a settings file activates expwt already, before or after ezwt, the ezwt line is removed instead of
replaced ("remove ezwt (expwt already active)"), and an ezwt listed twice gives one expwt; the file is still kept as
<file>.before-expwt.
2026-10-31: migrations in the kernel
A site's changes to its database, content, classes, roles and settings can be written once as migrations, files under version control that every installation of the site applies in the same order, with a record of what ran where, a caption before every run, an automatic restore after a failure and a commit, tag and push of every run's record. Guide: Migrations (German: Migrationen), starting with the tutorial; the feature: Migrations in the kernel.
Upgrading: ./console exp:upgrade --apply runs the new step 6.0.15-migration-tables, which creates exp_migration
and exp_migration_run (after it, 6.0.15-upgrade-record-table retires the pre-release table expupgrade_step);
from then on exp:upgrade records its steps in exp_migration. Commits, tags and pushes of migration runs need git_manager; without it a run commits nothing and
says so.
The parts of migrations in the kernel, each with a section of its own: the engine, the record, captions, commands and the administration, YAML migrations, content and locations, the upgrade steps of exp:upgrade, one record table, exp:migration:check, classes, users, roles, sections, languages, object states, tags, URL aliases, the trash, files, images, caches, search index, autoloads, layouts, HTTP, mail, console commands, SQL and INI migrations, GraphQL and the APIs, the engine matrix and performance.
Added
Added: Migration files
YYYYMMDDHHMMSS_name.<type>in the site'smigrations/, inextension/<name>/migrations/of active extensions and in installed packages, with a header ofkey: valuecomment lines (description,requires,environments,engines,maintenance,author), ordered by id andrequires, with a SHA-256 checksum (expMigrationDefinition,expMigrationLocator,expMigrationGraph; the file format).Added: PHP migrations: a namespaced class that extends
expMigrationPhpwithplan(),up()and optionallydown()anddetect()(PHP migrations).Added: The record: tables
exp_migration(one row per migration; also the record ofexp:upgrade, which replacesexpupgrade_step) andexp_migration_run, mirrored intomigrations/status/<environment>.jsonandmigrations/runs/<run-id>.log(expMigrationRecord,expMigrationStatusMirror).Added: The runner: one installation lock with
exp:upgrade(expMigrationLock;expUpgradeLockis now this lock), a transaction per migration where the engine has one, the run's user ([RunSettings] RunAsUser), maintenance mode per migration, a guard against changed files (--force --reason), the eventsmigration/run/before|afterandmigration/step/before|after, and the audit eventssystem.migration.run,.apply,.fail,.rollback,.caption,.push.Added: Captions of migration runs (kinds
migration-pre,migration-post,migration-failureofexp:upgrade's captions), reused while nothing has changed, and the restore of the pre caption after a failure; a run, one migration (down()) or any caption taken back by hand (Rollback and captions).Added: The migrations repository: the record of every run committed, tagged
migrations/<environment>/<run-id>and pushed; failed pushes retried by the new cronjob partmigrationpush(The migrations repository).Added: The commands
exp:migration:status,:migrate,:dry-run(with--simulate --diffon PostgreSQL and SQLite),:rollback,:resume,:caption,:push,:checkand:generate --skeleton, with the machine outputexp-migration/1and the exit codes ofexp:upgrade(the commands).Added:
settings/migration.iniwith[MigrationSettings],[RunSettings],[CaptionSettings]and[RepositorySettings](the settings).Added: The documentation in English and German: the guide, a tutorial, PHP migrations, rollback and captions, the migrations repository, troubleshooting, the feature page and the file format.
Added: Setup > Migrations, the module
migration: the status of every migration, the history of the runs (filtered by state), the page of a run with its record, its migrations and the end of its run log, the dry run with a simulated run, the run form (the migrations chosen one by one, the switches of the caption flow, changed files accepted with a reason), resume, and the rollback of a run, a migration or (expert) an earlier caption. Policiesmigration/read,run,rollbackandedit(Administrator only in a new installation); a run, a resume, a simulation and a rollback ask "Verify it's you" (site.ini [ReauthGroup_system]). Every string is in the 155 administration catalogues (Migrations in the administration).Added: Runs started in the administration run in the background: a console process of its own as the signed-in user (
expMigrationJob, files invar/<site>/migration/jobs/), followed live by the run's page throughmigration/stream, and going on when the page is closed.exp:migration:migrateand:resumetake--run-idand--progress,:rollbacktakes--progress;migration.ini [AdminSettings]hasPhpCliPath,JobHistoryandLogBytes(migration.ini [AdminSettings]).Added: The dashboard block
migrations(pending, failed, orphans, the run going on, the last run) and the fetch functionmigration/summary; the website sidebar (expwt) shows the same read-only in its Migrations panel.Added: The help pages of Setup > Migrations in English and German: the status and the history, run, dry run and rollback, and the website sidebar's panel, opened by the ? button of each view; the generated pages of the module's views, its policies, its fetch function and the new settings.
Added: Recording changes as migrations: while a recording runs, what a user changes (a session recording) or what every user changes (a site-wide window) is kept at the kernel's operation level, through the audit's hook points, as the steps of a YAML migration with remote ids and identifiers instead of numeric ids. Users' accounts and personal fields are never recorded. Tables
exp_migration_recordingandexp_migration_recording_event(upgrade step6.0.15-migration-recording-tables, every engine), the commandsexp:migration:record:start,:stop,:status,:list,:showand:discard,exp:migration:generate --from-recording(it lists the steps and waits for the YAML step types), Setup > Migrations > Recording with a Recording bar on every page of the administration (policymigration/edit; the site-wide window asks "Verify it's you"),migration.ini [RecordingSettings]and the audit eventsystem.migration.record(Generators and recording changes, Recording changes in the administration).Upgrading:
./console exp:upgrade --applyruns the new step6.0.15-migration-recording-tables.
Updated
- Updated: Upgrading with the console, Upgrade steps for extensions, Schema changes and migrations and the architecture, evaluation, scaling and technical debt pages no longer say that Exponential has no migration runner, and name the shared lock, record and captions.
2026-10-31: fixes from the Opencontent and Mugo Web forks, and the items their review left open
Merges pull request #345, reported and contributed by Ekkehard Dörre (CJW): the fixes that Opencontent
(Opencontent/ezpublish-legacy) and Mugo Web (mugoweb/ezpublish-legacy), the two active forks of the kernel
Exponential grew from, made and Exponential still lacked, each written anew for this tree. With thanks to Ekke, to
Opencontent and to Mugo Web. Feature page: Fixes from the Opencontent and Mugo Web forks
(Deutsch); upgrade notes:
What to check (Deutsch).
Added
- Added:
site.ini [URLTranslator] MaxPathDepth(default30):eZURLAliasML::translate()andfetchByPath()look up at most that many path elements, each one a self-join ofezurlalias_ml; a deeper URL is not found without a query. A crawler requesting endless paths kept the database busy (Opencontent). NeweZURLAliasML::maxPathDepth(). - Added:
eZLog::oneLine(), which replaces every line break of a text with a space, for code that writes log lines. - Added:
eZURLAliasML::stripURLOrigin(), the path of an absolute URL. - Added: database tests of
eZContentOperationCollection::publishNode()for a new object, a new version, a new location and a copied node assignment (eZContentOperationCollectionPublishNodeTest).
Updated
- Updated: An
eZLog::write()entry stays on one line: a line break in a logged value (a user name, a URL) could forge further entries (Opencontent).eZLog::writeStorageLog()writesstorage.logthe same way. - Updated: The REST kernel registers the listeners of
site.ini [Event] Listeners[], as the web kernel does; none of them ran for REST requests (Opencontent). - Updated: Iterating the fields of a content object in a language it has no translation in gives no fields instead
of a warning and a
TypeError(ezpContentFieldSet, Opencontent). - Updated:
eZPersistentObjectreads a limit with an offset and no length without an undefined array key (Mugo Web). - Updated:
clusterize.phpskips binary file, media and image rows without a file name instead of storing a directory (Mugo Web). - Updated:
index_cluster.phpwithout a cluster configuration answers with its error page instead of anErrorabout the undefinedCLUSTER_ENABLE_DEBUG(Mugo Web). - Updated: The Online Editor's browse answers an empty folder with a count of 0 instead of a
TypeErrorfromcount( false )(Mugo Web). - Updated:
texttoimagetakes a null or numeric input as a string (Mugo Web). - Updated:
eZINI::parameterSet()without a root directory looks insettings/; it set the file name tosettingsinstead (Mugo Web). - Updated:
eZSessiondoes not try to rename a session that is already active (Mugo Web). - Updated: The PDF of an embedded object the reader may not view no longer shows the object's name (Mugo Web).
- Updated: The benchmark CLI runner loads again;
display()was declared incompatibly with its parent (Mugo Web). - Updated:
publishNode()takes the node found by a node assignment's remote ID only when it is the object's node below the assignment's parent. The code readif ( !$existingNode );with an empty statement, sofindNode()always decided; removing only the semicolon, as Mugo Web did, would have let a copied object's first publication take over the original's node, becausecloneNodeAssignment()keeps the remote ID. The same emptyifin the search engine's handling ofwww.addresses is fixed, and a test finds anyifwith an empty body inkernel/andlib/. - Updated:
eZURLAliasML::storePath()stores an absolute URL given as an alias (https://www.example.com/news) as its path (news) instead of nested placeholder aliases; Opencontent refuses it instead, which the alias form would answer without a message. - Updated:
ezjscore.ini [ezjscServer_ezjscnode] HardLimitis500instead offalse, soezjscnode::subtreeandexpsubitems::rowsreturn at most 500 items per request, the largest page the sub-items list offers (Mugo Web ships100). ezjscore's changelog lists it under 1.8.8 (unreleased). - Updated:
file.ini [FileSettings] FileExtensionBlackList[]also refusesphp4,php5,php7,php8,phps,shtml,cgi,pl,asp,aspx,jspandhtaccess(Mugo Web's list is the shorter one Exponential shipped). - Updated: The features index, Security hardening and the security engineering overview for software engineers name the changes, in English and German.
Removed
- Removed:
moxieplayer.swf, the Flash video player of the TinyMCE 3 media plugin bundled with ezoe; the plugin'sflash_video_player_urldefaults tofalse, so it adds no Flash fallback (Mugo Web removed Flash files for the same reason).
Not adopted, and proposals
- Not adopted: skipping a re-parenting
UPDATEofezurlalias_mlthat changes no row, and atrim()of the CSS packer's output; neither fixes a defect. - Proposals, not in 6.0.15: INI values from environment variables,
i18n.iniOverrideDefaultforeng-GB, a Greek flagell-GR.gifand the localesmoc-ITandlad-IT(Opencontent); see the feature page.
Tests
expLegacyForkFixesTest(DB-less, 17 tests): each fix and follow-up fails without its change.eZContentOperationCollectionPublishNodeTest(database).
2026-10-31: four problems a downstream installation reported (Redmine #16709)
Reported by Ekke (CJW) after merging the kernel's main branch into a downstream fork, on PHP 8.5 and macOS. Thank you.
Updated
- Updated: A zstd audit archive is read to its end with every version of the PHP extension zstd. With zstd 0.18.0 a
compress.zstd://stream never reported its end, so reading an archive back (the read-back after archiving,verifyArchives(),restore()) ran at 100 % CPU until it was killed, andexpAuditArchiveTest::testEachFormathung.expAuditZstdFormat::open()no longer reads through the stream wrapper: it decompresses the archive in chunks (zstd_uncompress_init()/zstd_uncompress_add(), elsezstd_uncompress()) into a temporary stream. The zstd binary is used as before where the extension is missing.expAuditFormatBase::digestStream()also stops after 1,000 reads in a row that return nothing, so a stream that never ends makes the archive fail verification instead of hanging. The test file guards its long tests with a time limit (pcntl) and checks both cases. - Updated:
expPackageArchiveTest::testPaxArchiveDoesNotHangruns its child process with-d display_errors=stderr: on PHP 8.4 and later zetacomponents/archive 1.5.1 prints deprecations, which landed on stdout in front of the child's answer, so the test failed where the PHP configuration displays errors. The child's stderr is read while it runs, so many notices can never block it.expLogActionsTest::testDownloadgets the same option, since a notice on stdout would break the gzip data it reads. - Updated:
expPackageArchiveLegacyFormatsTest::testPackageWithUpperCaseNameIsImportedListedAndRemovedno longer assumes a case-sensitive file system. It creates a probe file in the package repository and looks for it under its lower-case name: on a case-sensitive file system (Linux) the lower-case package name must still not be found, on a case-insensitive one (macOS, Windows) the test checks that both spellings reach the same directory instead of failing. - Updated: The first administration page without the interface language caches no longer runs into
max_execution_time. The language choice in the toolbar, on every page, showed each language's translation coverage and built the masks of all 156 translations for it at once, about 30 seconds. Its catalogue now has a time budget of 2 seconds per request (expUISwitchLanguageCatalogue::setTimeBudget(),expUISwitchLanguageProvider::COVERAGE_TIME_BUDGET): what is not built in that time follows in the next requests, those languages are listed without a percentage meanwhile (pendingin their entries), and the complete catalogue is cached as before once every mask is there. Measured on a copy of an installation with 156 translations: the sign-in after the caches were cleared took 30.1 seconds before (the limit), 11 to 13 seconds after (the other cold caches), each following page about 2.5 seconds until the masks were complete after about ten pages, then under one second. Setup > Interface languages and./console exp:i18n fallbacksstill build everything at once (The interface language list, German: Die Liste der Oberflächensprachen).
2026-11-01: YAML migrations
A migration can be written as YAML: a list of steps, each of a step type, with references between them, conditions, loops and the id map of the environment, checked against each step type's schema before anything runs. Guide: Migrations (German: Migrationen); the rules: the file format.
Upgrading: YAML migrations need symfony/yaml (^5.4 || ^6.4 || ^7.0), which composer.json now requires; run
composer update symfony/yaml (or keep a copy elsewhere and name it in migration.ini [YamlSettings] LibraryPath).
Without it a YAML migration is refused with that hint, and nothing else changes.
Added
- Added: YAML migrations (
YYYYMMDDHHMMSS_name.yml) run, check and roll back like PHP migrations, and the two kinds require each other and share references in a run (expMigrationYaml, read byexpMigrationYamlParserthrough symfony/yaml: no PHP tags, duplicate keys refused). The document is the list of steps, or a map withsteps,down(the steps that undo it) andtransactional. - Added: The step model:
type,mode,if,environments,engines,referencesand the step type's own keys, checked against its schema (expMigrationStepSchema); loopsforeach(as,key,${loop.index}) andwhilewith a maximum ([StepSettings] MaxLoopIterations); conditionsall,any,not,ref/set,environment,engineandvaluewithequals,not_equals,in,not_in,gt,gte,lt,lte,matches,empty(expMigrationCondition); placeholders${ref.*},${loop.*},${idmap.*.*},${run.*}(expMigrationReferences). - Added: The id map of an environment,
migrations/idmap/<environment>.json(schemaexp-migration-idmap/1,expMigrationIdMap), and identifiers of nodes and objects by id,remote:,object-remote:andpath:(expMigrationIdentifier). - Added: The step plugin API:
expMigrationStepType(withexpMigrationStepTypeBaseandexpMigrationRevertibleStepType), registered inmigration.ini [StepSettings] StepTypeList[<name>]=<class>by the kernel and by extensions (expMigrationStepRegistry); the kernel's step typesreferenceandassert. - Added:
exp:migration:generate --skeleton <name> --type=ymlwrites an empty YAML migration;exp:migration:checkchecks YAML migrations (the YAML, every step, its keys, conditions, loops, references and placeholders) instead of refusing them; the dry run plans loops and conditions as far as they can be told before the run. - Added:
migration.ini [YamlSettings] LibraryPathand[StepSettings](the settings). - Added: Tests of the YAML engine without a database, and runs of YAML migrations through the runner on SQLite,
PostgreSQL, MySQL/MariaDB and MongoDB (
expMigrationYaml*Test).
Updated
- Updated: The file format, the migrations guide and the tutorial (a YAML migration, section 12), in English and German.
2026-11-01: YAML migration steps for content and locations
YAML migrations can now create and change content: the step types content, location and content_version, each
running the kernel's own API and operations the way the administration does. Guide:
YAML migration steps (German:
Schritte von YAML-Migrationen); every key of every step type:
the step types (German:
die Schritttypen).
Added
- Added: The step type
contentwith the modes create, update, ensure (create when missing byremote_id, else change only what differs) and delete;fields:in one language andtranslations:with one block per further language, each published as a version of its own;in_place: truechanges the published version without a new one;section,always_available,missing: skip,trash. - Added: The step type
locationwith the modes create (a further location), update (priority, sorting, main location), delete, move, copy, hide and unhide, through the content operations of the administration. - Added: The step type
content_versionwith the modes restore (publish a copy of an earlier version) and cleanup (remove archived versions butkeep:, and drafts withdrafts: true). - Added: Workflows of the operations' triggers run only with
workflows: true, notification events only withnotify: true; a workflow that cancels or defers an operation fails the step. - Added:
expMigrationPartlyRevertibleStepType: a step type that can undo only some of its steps says which withrevertible(). A content create with aremote_id, a location create, hide and unhide undo themselves; a migration with any other content step needsdown:. - Added: Identifiers of classes, sections, languages, users, roles and object states (
expMigrationIdentifier): by id, by name (identifier, locale, login, role name,<group>/<state>), classes and users also byremote:. - Added: Generated reference pages of the step types,
examples/migration-steps/(Germanbeispiele/migrationsschritte/), made from each type's schema by./console exp:help:examples --area=migration-steps. - Added: Tests of the step types without a database, and runs through the runner on SQLite, PostgreSQL,
MySQL/MariaDB and MongoDB (
expMigrationContentSteps*Test).
Updated
- Updated: On MongoDB, a URL alias entry that was made the original of its path stored no action type
(
eZURLAliasML::storePath()used it before it was set). - Updated: The migrations guide and the tutorial name the new step types.
2026-11-01: static publishing, the build engine
A front siteaccess can be built as a folder of static files that GitHub Pages or any other static host serves: the
pages crawled as the anonymous user, cleaned of debug output, form tokens, session ids and nonces, their links
rewritten (relative, or below a base path such as /my-site/), the files they need copied, and .nojekyll,
404.html, robots.txt, sitemap.xml and a manifest written. This is the first part of static publishing; pushing
builds to git, the administration screens and the GitHub setup follow. Guide:
Host static sites on GitHub Pages with Exponential 6.0.15+
(German: Statische Websites auf GitHub Pages hosten);
the feature: Static publishing; the format:
The static publishing build format.
Upgrading: nothing to do. Static publishing is switched off ([StaticPublishSettings] Enabled=disabled) until you
switch it on and name a target.
Added
- Added: Targets of static publishing in
staticcache.ini:[StaticPublishSettings](Enabled,TargetList[],WorkDir,MaxSiteBytes,MaxFileBytes) and one[Target_<id>]block per target, with the template[Target_example]; read and checked byexpStaticTarget(front siteaccesses only, base path, addresses, no secret inRemote, credential names). - Added: The build (
expStaticBuild): a crawl as the anonymous user, the pages stripped (expStaticPostProcessor) and refused when a session cookie, debug output or a form token remains, links rewritten (expStaticLinkRewriter), the files collected from the installation (var/through the cluster file handler) or from the site (expStaticAssetCollector), the storage images of content the anonymous user may read, the extras (expStaticExtras), the manifest.static-publish/manifest.json(expStaticManifest), the diff against the last build, a link check (expStaticLinkChecker) and an update of the target's folder that touches only the files a manifest lists. Everything is built in a staging folder first. - Added: The commands
exp:static:targets,exp:static:build <target>(--dry-run,--paths,--nodes,--full,--report,--crawl-url,--max-pages,--keep-staging),exp:static:diff <target>(--verify,--check-links,--mount) andexp:static:serve <target>(a preview below the base path with PHP's built-in web server, throughexpStaticServe), with the machine outputexp-static/1and the exit codes ofexp:upgrade(the commands).
Updated
- Updated:
expStaticCacheRunnerhas an export mode: a callable receives every page with its path in the site instead of the static cache handler, and the crawl can be pointed at another address.expPreloadRunnerhands the store callback the response's status, type and the names of the cookies it set, takes a request function for tests (fetcher), and its request is public asexpPreloadRunner::request().
2026-11-01: the upgrade steps among the migrations
exp:upgrade and migrations are one engine now: every upgrade step (the kernel's and those of extensions) is also a
migration of the source kernel. Guide: The steps among the migrations
(German: Die Schritte unter den Migrationen).
Added
- Added:
expMigrationUpgradeSourceandexpMigrationUpgradeSteppresent each step ofexpUpgradeRegistryas a migration of the sourcekernel, with the stateexp:upgradegives it (detect()and the record).exp:migration:status, Setup > Migrations, its dashboard block and the expwt Migrations panel list the steps first, before the site's and the extensions' migrations. - Added:
exp:migration:migrate --apply(and Run in Setup > Migrations) applies the pending steps of the stagesdatabaseandscriptsbefore the migrations, in the same run, under the same lock and caption, and records them inexp_migrationwith their stage and version, whereexp:upgradereads them. A migration may name a step inrequires.
Updated
- Updated: A pending step of the stages
code,librariesandfinish, or an optional step, is listed by a migration run asdeferred(a new action ofexp-migration/1) with a warning that names theexp:upgradecommand that runs it; a migration that requires it waits, and the run form of the administration leaves it out. After a run that applied steps a warning names./console exp:upgrade:finish --apply. - Updated: The entries of
exp-migration/1have astage(migrationfor a migration file); a migration whose state is detected (an upgrade step) has no checksum guard and no transaction around it. - Updated:
exp:upgradekeeps its commands, stages and options; its record isexp_migration, made by6.0.15-migration-tables(the pre-release tableexpupgrade_stepis copied over and dropped by6.0.15-upgrade-record-table, see the next section). - Updated: Upgrading with the console, Upgrade steps for extensions, Migrations, Migrations in the kernel and Schema changes and migrations, with their German pages.
2026-11-01: the table expupgrade_step is dropped
The record of exp:upgrade is exp_migration (see the two sections above). The table expupgrade_step, which kept
that record in a pre-release of 6.0.15, is no longer created anywhere: no released version had it, so a new
installation and an upgrade from 6.0.14 or older never see it. An installation that ran the pre-release still has it,
and the step 6.0.15-upgrade-record-table retires it. Guide:
The older table expupgrade_step
(German: Die ältere Tabelle expupgrade_step).
Upgrading: on an installation with expupgrade_step, run ./console exp:upgrade:database first (the dry run lists
each row it copies and the DROP TABLE), then ./console exp:upgrade:database --apply. Nothing is lost: the rows
exp_migration does not have are copied, a newer row of exp_migration is kept, and the table is dropped only when
every row of it is in exp_migration. A second run does nothing. Oracle: ezoracle carries the same change for its
schema and its 6.0.15 update file.
Removed
- Removed: The table
expupgrade_stepfromshare/db_schema.dba,kernel/sql/mysql/kernel_schema.sql,kernel/sql/postgresql/kernel_schema.sqlandkernel/sql/sqlite/schema.sql(and so from MongoDB, whose collections come fromshare/db_schema.dba). - Removed:
expUpgradeRecordno longer reads or writesexpupgrade_step, andexpUpgradeRecord::LEGACY_TABLEis gone. Untilexp_migrationexists (the first steps of a first run) nothing is recorded and a write returns false. - Removed:
expupgrade_stepfrommigration.ini [CaptionSettings] ReuseIgnoreTableList[]and the defaults ofexpMigrationCaption.
Updated
- Updated:
6.0.15-migration-tablesis step 0 of the database stage and only makesexp_migrationandexp_migration_run. - Updated:
6.0.15-upgrade-record-table(now after6.0.15-migration-tables) is pending only whileexpupgrade_stepexists; it copies the rowsexp_migrationdoes not have (expMigrationRecord::copyLegacy()), checks that none is left out, and drops the table (MongoDB: the collection; Oracle:DROP TABLE ... PURGE). It refuses to drop whileexp_migrationis missing or a row is not copied. - Updated: The 6.0.0 to 6.0.15 update files of MySQL, PostgreSQL and SQLite create
expupgrade_stepempty only so the copy can read it, copy its rows and drop it (DROP TABLE IF EXISTS), so they run on every installation and twice. - Updated: Upgrading with the console, Migrations and CaptionSettings, with their German pages.
2026-11-01: static publishing, publishing to git
A static build is published to any git host from the console: exp:static:publish <target> commits only the build's
files in the target's own repository (var/<site>/static-publish/<target>/, with Folder=docs the build in its
docs/), tags the commit static/<target>/<n> and pushes the branch and the tags, never forced. A push that fails
waits and is sent again by exp:static:push or the cronjob part staticpublish; a rollback is a new commit with the
files of an earlier tag. The push signs in with a token from the secret store or the environment (through
GIT_ASKPASS), an SSH deploy key made per target, or the host's own credentials; a credential is never on a command
line, in a remote address or in a log. Guides:
Repositories and credentials,
History and rollback,
SSH deploy keys (German:
Repositorys und Zugangsdaten,
Verlauf und Zurücknehmen,
SSH-Deploy-Keys).
Upgrading: ./console exp:upgrade --apply runs the step 6.0.15-static-publish-tables (tables exp_static_run and
exp_static_page, every engine). Publishing works without them, but nothing is recorded.
Added
- Added:
expStaticRepository: the target's folder as a git repository on its branch (an existing remote branch is taken over, its own files kept), commits of the build's files only, annotated tagsstatic/<target>/<n>that are never moved, pushes that are never forced (a guard refuses--force,+refspecs and deletions), the push statespushed,pending,disabled,no-remote,none, and the rollback as a new commit; git throughexpUpgradeGit::git()(git_manager) where no credential is needed, without a shell everywhere. - Added:
expStaticCredentials:token:<secret>,token:env:<VARIABLE>,sshkey:<secret>andnone, with a hook forapp:<installation>(expStaticCredentials::provider()); the askpass helperbin/shell/expstaticaskpass.sh; every git output redacted. - Added:
expStaticPublisher(publish, rollback, retry),expStaticLock(one build, publish, rollback or push of a target at a time;exp:static:buildtakes it too) andexpStaticStatus(tablesexp_static_runandexp_static_page, upgrade step6.0.15-static-publish-tables, kernel schemas and 6.0.15 update files of MySQL, PostgreSQL and SQLite). - Added: The commands
exp:static:publish <target>(--build,--message,--no-push),exp:static:rollback <target> <n|tag>(--apply,--message,--no-push),exp:static:push [<target>],exp:static:keys <target>(--generate,--replace) andexp:static:status <target>(--limit); the cronjob partstaticpublish. - Added:
[StaticPublishSettings] Push,CommitName,CommitEmail; the audit eventssystem.staticpublish.publish,system.staticpublish.rollbackandsystem.staticpublish.credential.
Updated
- Updated:
Folder=docswrites the build to<folder>/docs(expStaticTarget::outputDirectory()), forexp:static:build,diff,serveandtargets. - Updated: Host static sites on GitHub Pages and
Static publishing publish with
exp:static:publish, with their German pages; Upgrading with the console lists the new step.
2026-11-01: SQLite tables keyed on more than one column
The hand-written SQLite schema kernel/sql/sqlite/schema.sql keyed ezcontentobject_attribute on id alone, where
share/db_schema.dba keys it on id and version. A SQLite installation made from that file could not store a
second version of an object: the new rows replaced the old ones. Eleven more tables had the same fault
(ezcontentclass, ezcontentclass_attribute, ezworkflow, ezworkflow_event, ezpdf_export, ezrss_export,
ezrss_export_item, ezrss_export_opml_item, ezrss_import, ezoperation_memento, ezwaituntildatevalue), and
ezenumvalue and ezenumobjectvalue had a column text in place of enumelement and enumvalue. An installation
made by the setup wizard, the kickstarter or the installers reads share/db_schema.dba and was never affected.
Upgrading: SQLite only. ./console exp:upgrade:database lists every table whose key differs from the .dba under the
step 6.0.15-sqlite-composite-keys; --apply rebuilds each in one transaction (rows, columns and indexes kept). On a
database with the right keys the step is done and does nothing.
Updated
- Updated:
kernel/sql/sqlite/schema.sqlkeys every table asshare/db_schema.dbadoes, andezpdf_exporthasshow_footerandfooter_text.
Added
- Added: The upgrade step
6.0.15-sqlite-composite-keys(expUpgradeStepSqliteCompositeKeys, SQLite only): a table keyed differently fromshare/db_schema.dbais rebuilt with the key of the .dba (new table, rows copied, old table dropped, new one renamed, indexes created again, in one transaction). Pending only while a key is wrong.
2026-11-01: exp:migration:check reports a method PHP would refuse
A PHP migration with a helper method named definition() or file(), or with a hook such as plan() declared with
another signature, made exp:migration:check (and a run) stop with a PHP fatal error while the file loaded, without
naming the migration. The class is now read before the file is loaded, and the method is reported as an error of that
migration; the other migrations are checked as usual.
Updated
- Updated:
expMigrationPhp::load()refuses, before loading the file, a class that extendsexpMigrationPhpand declares a method of the base's own (now final:load(),classesIn(),definition(),setDefinition(),id(),source(),environments(),requires(),maintenance(),engines(),checksum(),file(),state()), a hook (plan(),up(),down(),detect(),description(),transactional(),reversible()) that is not public, static, takes fewer parameters, requires more or types$context, or a class withoutup()(expMigrationPhp::methodClashes(), which only tokenizes the code).exp:migration:checklists the message. - Updated: PHP migrations, with its German page.
2026-11-01: YAML migration steps for classes, users, roles, sections, languages and object states
YAML migrations can now change the structure of an installation: content classes and class groups, users and user groups, roles with their policies, limitations and assignments, sections, languages and object state groups. Each step type goes through the kernel's own helpers and operations, as the setup wizard, the site packages and the administration do. Guide: YAML migration steps (German: Schritte von YAML-Migrationen); every key: the step types (German: die Schritttypen).
Added
- Added: The step type
content_class(create, update, ensure, delete): a class with its names and descriptions in several languages, name pattern, URL alias pattern, container, sorting and class groups, and its attributes with their flags, category, placement and datatype settings (settings:, the columnsdata_int1...data_text5).updateadds attributes (add_attributes:, througheZSiteInstaller::addClassAttributes(), so every object gets them), changes them (update_attributes:) and removes them with their data (remove_attributes:,eZSiteInstaller::removeClassAttribute()); a class with objects is deleted only withwith_objects: true. - Added: The step type
content_class_group(create, update, ensure, delete); a group with classes is not deleted. - Added: The step types
user(create, update, ensure, delete: login, e-mail, password hashed as the kernel hashes it or a given hash, enabled, groups as locations, fields) anduser_group(the content step with the keys of a group). No plan, log or record shows a password. - Added: The step type
role(create, update, ensure, delete, assign, unassign): policies with limitations whose values may be identifiers (class and section identifiers, nodes and subtrees, siteaccess names, states), written througheZSiteInstaller::addPoliciesForRole(); assignments to users and groups, limited to a subtree or a section. - Added: The step types
section(create, update, ensure, delete, assign with the scopes subtree, inherited and object),language(create, update, ensure, delete) andobject_state_group(create, update, ensure, delete, with the states and their translations). - Added: Creates undo themselves when their migration is rolled back without
down:(a class only while it has no objects), and so do role assignments. - Added: The identifier kinds
class_group(by name) andstate_group(by identifier). - Added: Generated reference pages of the eight step types, English and German.
- Added: Tests without a database (
expMigrationStructureStepsTest) and runs through the runner on SQLite, PostgreSQL, MySQL/MariaDB and MongoDB (expMigrationStructureSteps*Test).
Updated
- Updated: A
contentdelete checks that the object is gone and fails when the delete operation left it (it does so without a word when the user the migration runs as may not remove it), and a delete that removes a user account keeps the user the migration runs as signed in. - Updated: The migrations guide, the file format and the YAML steps guide, with their German pages: the step types
that come later are those of tags, URL aliases, the trash, files, caches, layouts,
http,mailandcommand.
2026-11-01: YAML migration steps for tags, URL aliases, the trash, files and images
YAML migrations can now change the tags of eztags, the custom URL aliases and wildcards, the trash, and the files and images of content: a file or image is read from the folder of the migration, so it travels with the migration in its repository. Guide: YAML migration steps (German: Schritte von YAML-Migrationen); every key: the step types.
Added
- Added: The step type
tag(create, update, ensure, delete, move, merge): a tag of eztags with its keyword in several languages, its main language, always available and synonyms, made as the tags pages of eztags make it. Without eztags the step fails and says so;missing: skiplets it pass. - Added: The step type
url_alias(create, ensure, delete): a custom URL alias of a node (redirecting or not, from the root or below the parent's address), a global URL alias to a module URL or another address, and URL wildcards; a delete removes one address, every custom address of a node, or a wildcard, never the address of a node's name. - Added: The step type
trash(move, restore, purge): an object with all its locations to the trash, back below the parent it had or another, and purged for good or the whole trash emptied, each checked afterwards. - Added: The step types
file(anezbinaryfileorezmediaattribute) andimage(anezimageattribute with its alternative text, focal point and crop rectangle), each a new version or in place. The file is a path below the folder of the migration file;.., absolute paths, URLs and links out of the folder are refused. - Added: Creates of
tagandurl_alias, andtrashmove and restore, undo themselves when their migration is rolled back withoutdown:. - Added: The identifier kinds
tag(by id,remote:orpath:<keyword path>) andurl_alias(by its address). - Added: Generated reference pages of the five step types, English and German (with the German descriptions).
- Added: Tests without a database (
expMigrationTagAliasTrashFileStepsTest) and runs through the runner on SQLite, PostgreSQL, MySQL/MariaDB and MongoDB (expMigrationTagAliasTrashFileSteps*Test; the tags part with eztags where it can be loaded).
Updated
- Updated: On MongoDB a URL alias that does not redirect is stored as an alias (it was stored as a history entry of
the node's name, so it neither showed in the administration nor could be removed), an alias removed in its last
language is deleted (the filter of
eZURLAliasML::removeSingleEntry()used a negative bitmask, which matched nothing), andeZURLAliasQuerywithout a limit reads every item. - Updated: The migrations guide, the file format and the YAML steps guide and the migrations help hub, with their
German pages: the step types that come later are those of caches, the search index, layouts,
http,mailandcommand.
2026-11-01: Caches, the search index and autoloads after a migration run
A migration run now clears exactly the caches its steps touched and reindexes the objects they changed, and three
step types cover what that does not: cache, search and autoload. The report of a run names the caches with the
deploy that clears exactly them on the server too. Guide: YAML migration steps
(German: Schritte von YAML-Migrationen).
Added
- Added: After a run that applied migrations, or a rollback that undid some, the caches the steps of those
migrations declare are cleared once through ezcache, and only those (
migration.ini [RunSettings] ClearCaches). The report lists them (caches) with the deploy that clears them on a server with PHP-FPM and Velocity (deploy:./console exp:velocity deploy --caches=<list> --apply). - Added: The objects the steps created or changed are reindexed after the run: queued for the cronjob
indexcontent(the default), indexed at once, or left alone ([RunSettings] Reindex=queue|now|disabled); the report says how many (reindex). - Added: The step type
cache: caches of ezcache by id (caches:) or tag (tags:), and the view caches of objects; cleared with the run's list after the run (when: end) or at once (when: now). - Added: The step type
search(reindex, full): named objects or those the run changed so far, or every published object (of some classes), queued or indexed at once. - Added: The step type
autoload: the autoload array of the extensions or of the kernel overrides, regenerated by the generator in a process of its own and checked withphp -l; never the kernel's own array. - Added:
exp:velocity deploy --caches=<list>clears the caches named (ids or tags of ezcache,tag:<name>for a tag) whatever files changed. - Added: Generated reference pages of the three step types, English and German (with the German descriptions); a step
type whose caches are the ones a step names (
cache) shows those of its example. - Added: Tests without a database (
expMigrationCacheSearchAutoloadStepsTest) and runs through the runner on SQLite, PostgreSQL, MySQL/MariaDB and MongoDB (expMigrationCacheSearchAutoloadSteps*Test).
Updated
- Updated: The migrations guide, the file format, the YAML steps guide, the migrations help hub and the Velocity
deploy page, with their German pages: the step types that come later are those of layouts,
http,mailandcommand.
2026-11-01: YAML migration steps for layouts, HTTP requests, mails and console commands
YAML migrations can now build the layouts of Exponential Layouts (layouts, their zones and links to shared layouts, blocks and rules) and reach out of the installation: one HTTP request, one e-mail or one allowlisted console command per step. With these the YAML steps of the plan for 6.0.15 are complete. Guide: YAML migration steps (German: Schritte von YAML-Migrationen).
Added
- Added: The step types
layout,layout_blockandlayout_rule(create, update, ensure, delete) for Exponential Layouts (the extensionsexplayoutsandexplayouts_core), made through its services as the layout editor makes them: a change in the layout's draft, published at the end of the step (publish: falsekeeps the draft). A layout is named by its identifier, shared or not, its zones linked to a zone of a shared layout or not; a block by its name in a zone, with its definition, view types, place and parameters; a rule by its layout and targets (a node target takes any identifier of a node), with its conditions, priority and switch. Without the extensions the steps fail and say so;missing: skiplets them pass. A create of each undoes itself on a rollback withoutdown:. - Added: The step type
http: one request (GET, POST, PUT, PATCH, DELETE, HEAD) to an http or https URL without a user or password, headers, secrets by name (bearer:,secret_headers:), a text or JSON body, a signature as webhooks of Exponential carry it (sign:), the status codes that pass, and the answer kept for references (status, body, json). No redirect is followed. - Added: The step type
mail: one e-mail through the mail transport of the installation (the mail gate applies), to, cc and bcc, plain or HTML, from and reply-to, and a transport for this mail only. - Added: The step type
command: oneexp:*console command ofmigration.ini [StepSettings] CommandAllowList[](a name, or a group ending in*), neverexp:migration:*orexp:upgrade*, handed to PHP as a list (no shell), with a time limit and the exit code that passes. The default list:exp:services:compile,exp:graphql:persist,exp:help:content,exp:static:build. - Added: The audit events
system.migration.http,system.migration.mailandsystem.migration.command(on by default): method, host and path and status of a request, the number of recipients of a mail, the name and exit code of a command; never a header, an address, a text, an argument or an output. - Added: Generated reference pages of the six step types, English and German (with the German descriptions).
- Added: Tests without a database (
expMigrationLayoutHttpMailCommandStepsTest; requests, mails and processes go to a transport, a folder and a runner of the test) and runs through the runner on SQLite, PostgreSQL, MySQL/MariaDB and MongoDB (expMigrationLayoutHttpMailCommandSteps*Test; the layouts part with Exponential Layouts where it can be loaded).
Updated
- Updated: The migrations guide, the file format, the YAML steps guide, the migrations help hub and the help page of the migration policies and events, with their German pages: every YAML step type of the plan is in 6.0.15; SQL and INI migrations come later.
2026-11-01: SQL migrations, INI migrations, and the schema, data, sql and settings steps
Migrations can now change the database schema and its rows in a portable way, run SQL written per engine, and change settings. Guide: Migrations: SQL and settings (German: Migrationen: SQL und Einstellungen).
Added
- Added: The step type
schema(create_table, drop_table, add_column, drop_column, add_index, drop_index): a table, a column or an index written once in the.dbaformat and made on MySQL/MariaDB, PostgreSQL, SQLite, Oracle and MongoDB by the kernel's schema handlers. Each mode looks in the engine's catalogue first and does nothing when its work is done; create_table, add_column and add_index undo themselves on a rollback. SQLite before 3.35 drops a column by rebuilding the table. - Added: The step type
data(insert, ensure, update, delete): rows of a table, the values quoted by the connection on the SQL engines and stored as they are on MongoDB. - Added: The step type
sql: statements for every SQL engine or per engine, MongoDB commands, or a file next to the migration (the engine's own file wins);missing: skipfor an engine it has nothing for. - Added: SQL migrations:
.sql,.<engine>.sql(pgsqlfor PostgreSQL too) and.mongodb.jsonfiles of one id, the engine's own file run, a-- downpart that undoes them. A portable.sqlfile runs on the SQL engines only. - Added: The step type
settingsand INI migrations (.ini, each block naming its file and siteaccess): changes tosettings/overrideandsettings/siteaccess/<name>only, through the INI editor ofexp:ini, with a backup first, owner, group and mode kept, and each change in the audit log (system.setting.write). - Added: The header key
transactional: nofor a migration without a transaction. - Added: Generated reference pages of the four step types, English and German, and the guide Migrations: SQL and settings with its German page.
- Added: Tests without a database (
expMigrationSqlSettingsStepsTest) and through the runner on SQLite, MySQL/MariaDB, PostgreSQL and MongoDB (expMigrationSqlSettingsSteps*Test), with schema changes that are and are not undone by a failure.
Updated
- Updated: The runner and
exp:migration:checkrun and check SQL and INI migrations instead of refusing them. - Updated: The migrations guide, the file format, the YAML steps guide and the guide index, with their German pages.
2026-11-01: static publishing, the parts that need a server
A static copy cannot search, take a form or sign anybody in. Each build now finds those parts and gives them a fallback: the search gets a page and index of its own, information collectors post to the live site through a signed endpoint (or show a short text), comments, the basket and the login are left out, and the report lists all of it. Old addresses get redirect pages, the files can be minified, precompressed and fingerprinted, and a target can publish itself after content publishes or on a schedule, with an e-mail and a webhook afterwards.
Upgrading: nothing to do. Every new behavior is off until a target asks for it; the form endpoint answers only targets
with Forms=endpoint. To publish on content publish or on a schedule, run the cronjob part staticpublish every few
minutes.
Added
- Added:
expStaticDegrader, the reportdegradedof every build, and the fallbacks for search, information collectors, comments, the basket, the login and server calls;[StaticPublishSettings] FormFallbackTextandFormFallbackLink. - Added:
expStaticSearchIndex:search.html,static-search/search.jsand an index per language (exp-static-search/1) withSearch=enabled;[StaticPublishSettings] SearchMaxBytes. - Added: The form endpoint
staticpublish/form(expStaticFormEndpoint,expStaticFormToken): origin, signed token, rate limit, then the normal information collection;[StaticPublishSettings] FormEndpoint,FormTokenDays,FormRateLimit,FormRateWindow;PolicyOmitList[]=staticpublish/form. - Added:
expStaticRedirects: redirect pages for the history and custom aliases of the pages and for URL wildcards without*(Extras[]=redirects);README.mdandLICENSE(Extras[]=readme,license, target settingLicense). - Added:
PostProcess[]=minify,precompress(.gz,.brwith the brotli extension) andfingerprint(content hash in the file name, never a query). - Added:
expStaticPublishQueue(pages marked stale byeZContentCacheManager,Triggers[]=publish,[StaticPublishSettings] PublishDebounce) andexpStaticSchedule(Triggers[]=schedule,Schedule), both run by the cronjob partstaticpublish. - Added:
expStaticNotifier: the e-mail ofNotify[]([StaticPublishSettings] NotifyEmail) and the webhook (exp-static-webhook/1, signed with the secretWebhookSecretnames). - Added: The audit events
system.staticpublish.buildandsystem.staticpublish.form. - Added: The guide Static publishing: dynamic features, the specification The form endpoint and the help pages on search and forms, publishing automatically, notifications and webhooks, SEO and languages, with their German pages.
Updated
- Updated:
exp_static_pagekeeps a page stale when its content changed after the build that wrote it began. - Updated:
exp:static:targets --export=<ids|all>prints targets as an INI migration (also for a site package), with credentials as their secret names only (expStaticTarget::migrationIni()). - Updated: A package carries static publishing targets (
ezpm.php add <package> expstatictarget <ids|all>, install item typeexpstatictarget): the wizard's settings with credentials as their secret names only, no user or password in the git address and no webhook; installing adds a target that is not there yet and keeps one that is. - Updated:
exp:static:github status, the "On GitHub" box and "Set up on GitHub" warn whenDelivery=actionsdeploys from a branch that is not the repository's default one, which GitHub'sgithub-pagesenvironment refuses unless the branch is added to it. - Updated: The setup wizard describes the redirect pages, README, LICENSE, the workflow, minify, precompress, fingerprint and the triggers on publish and on a schedule, and has a field for the license of the content; the report of a build shows what does not work statically, the search index and the redirect pages.
2026-11-01: GraphQL in migrations, tagged content events, and the migrations through the APIs
A migration can run queries and mutations of the GraphQL API, its content changes reach webhooks and subscriptions tagged with it, and the state of the migrations can be read through GraphQL and REST. Guides: YAML migration steps, the GraphQL API, REST API v1 and v2.
Added
- Added: The step type
graphql: a persisted query (by name or SHA-256), a query or mutation text, or a.graphqlfile next to the migration, run in process (no HTTP, no token) as the run's user, with variables from references; its answer is kept asdata,resultanderrors. An error fails the step unlessallow_errors: trueand data. - Added: The header key
events: each|summary|none: each content event of a migration carries the fieldmigration(each, the default), or the migration notes onemutationeventmigrationwith theobjectIdsandnodeIdsit changed (summary), or none (none). A migration that fails and is rolled back leaves no events. - Added: The fields
migrationandobjectIdsof the event journal, in webhook payloads and inContentEventof the subscriptions. - Added: The GraphQL queries
migrationStatus,migrations,migrationRunsandmigrationRun(expMigrationGraphQLExtension, ingraphql.ini SchemaExtensionList[]), read only, for users withmigration/read. - Added: The REST routes
GET /api/migration/v1/status,/runsand/runs/<id>(rest.ini ProviderClass[migration]) and the API key scopemigration:read. Nothing runs or is undone through an API. - Added:
expMigrationService, the read API both answer from. - Added: The generated reference page of the step type, English and German; tests without a database
(
expMigrationGraphqlIntegrationTest), of the schema extension with a snapshot of its types (expMigrationGraphQLExtensionTest), and through the runner on SQLite with the real GraphQL server: a persisted query and a mutation from a migration, their events tagged (expMigrationGraphqlSqliteTest).
Updated
- Updated: The GraphQL feature page, the GraphQL recipes, the REST API guide, the YAML steps guide and the file format, with their German pages.
2026-11-02: migrations on every engine, and what a run costs
Every family of the migration engine is checked the same way on SQLite, MySQL/MariaDB, PostgreSQL and MongoDB, in CI as well, and three performance checks measure what a run costs on each. Guide: Migrations: engines and performance (German: Migrationen: Engines und Leistung).
Added
- Added: The step type
graphqlis checked on MySQL/MariaDB, PostgreSQL and MongoDB too (expMigrationGraphqlDatabaseTest,expMigrationGraphqlMongoDBTest), with the checks shared with SQLite inexpMigrationGraphqlEngineChecks: on MongoDB a failed migration keeps the content it made (no transactions), and so its events, tagged with the migration. - Added: Performance checks on every engine (
expMigrationPerformanceEngineChecks, with a SQLite, a database and a MongoDB class): 1,000 YAML migrations (discovery, ordering, checksums, the run, a second run, the status), a data step of 5,000 rows inserted, updated and deleted, and a PHP migration of 5,000 rows in batches with checkpoints, stopped and resumed.EXP_MIGRATION_PERF_REPORTnames a file the measurements are written to;EXP_MIGRATION_PERF_COUNTandEXP_MIGRATION_PERF_ROWSchange their size. - Added: The database job of the PHPUnit workflow runs the migration classes of the testsuite
mongodb-liveon a MongoDB 7 service, next to MySQL 8.0 and PostgreSQL 15. - Added: The guide "Migrations: engines and performance" in English and German: the matrix, how to run it, how CI does, the measured times and memory per engine and how to measure your own.
Updated
- Updated: On MongoDB, the conditions of
eZPersistentObject::fetchObjectList()that are lists (IN), ranges (BETWEEN) or operators (>,<=,like...) match as they do on the SQL engines; they matched nothing before, so a migration's full reindex of a class queued no object there.
2026-11-02: the Exp Debug bar shows the user's cache profile
Added
- The debug bar has a User tab and a chip
VC <profile>: the user's cache profile, a fingerprint of the parts of the content view cache key that depend on the user (role ids, limitation values, discounts, an interface switch, the preferences ofCachedViewPreferencesof every view mode, the user id withpr_user). Users with the same profile share the view cache files under the same ViewCacheTweaks; a second fingerprint covers the keys listeners ofcontent/view/cachekeysadded for the page's view. The tab lists every part as the key takes it, the preferences per view mode, the role assignments with their limitation and whether they come from the user or a group, the profile of anonymous visitors for comparison, and copies the profile as text. Forsetup/setup, and for an anonymous visitor who gets the debug output. See the Debug bar. eZNodeviewfunctions::viewCacheTweak( $nodeID ),userCacheKeys( $user, $viewCacheTweak )anduserPreferencesKey( $preferenceList, $values ): the parts of the view cache key that depend on the user, used bygenerateViewCacheFile()itself, so the debug bar shows exactly what the key takes. The cache file names do not change.- Translations ger-DE and eng-US; the toolbar theme has icons for the chip and the tab.
- Added: The User tab's words in every administration catalogue, not only ger-DE and eng-US.
- Updated: The German guide, feature page and help page of the debug bar describe the User tab.
Tests
expDebugBarUserTest(UC-01 to UC-09).
2026-11-02: static publishing, the documentation completed
The static publishing documentation is complete in English and German. The help manual gains the 15-minute tutorial
from an existing site to a live github.io address, plain-language pages on what static publishing is and whether a
site suits it, a GitHub account walkthrough, links and base path, images and downloads, private content, other git
hosts, troubleshooting by symptom and a FAQ. Every audience of the help center has a page of its own, the guides cover
CI and GitHub Actions line by line, custom domains, limits, multisite, Windows and clusters and the way over from the
static cache, and four tutorials show a blog, a documentation site, developing in public and a static HTML site
brought into Exponential and back. A "New in 6.0.15" page announces it.
Upgrading: nothing to do.
Added
- Added: The help pages Make a free website in 15 minutes, What static publishing is, Is static right for my site?, Create a GitHub account and repository, Links and base path, Images, files and downloads, Keep private content private, Other git hosts, When a publish fails and the FAQ.
- Added: New in 6.0.15: static publishing, a static publishing page for each help center audience, and a chapter of the Building a Site book.
- Added: The guides for administrators, custom domains and DNS, CI and GitHub Actions, troubleshooting, limits and GitHub's terms, multisite installations, Windows and clusters, and from the static cache to static publishing; the tutorials on developing in public, a static blog, a static documentation site and a static HTML site.
- Added: The specification of the manifest, the compatibility page Static publishing and its item in the upgrade checklist.
Updated
- Updated: The help hub, the GitHub Pages limits page and step 8 of the setup wizard page (publishing on content publish and on a schedule works now), with their German pages.
2026-11-02: the Exp Debug bar's User tab lists the policies of the user's roles
Added
- The User tab ends with a folded section Policies (for
setup/setup): the policies of the user's roles with their limitations in readable form (classes, sections, siteaccesses and nodes by name) and the role, searchable by module/function, role, limitation and names, filterable by role, 200 rows at a time. A search for a module and a function (content/read) also finds the policies that grant it with a wildcard (content/*,*/*). Loaded only when opened, by the new server functionexpdebugbar::policies::<role>::<offset>::<search>, everything in the path (the search as base64url) and anything else refused; the policies, limitations and values of all roles are read through their persistent objects, three queries per 500 ids, so hundreds of roles work on every database, MongoDB included. See the Debug bar. - The role ids in the Roles table and in the policies link to
role/view/<id>. expDebugBarPolicies(kernel/classes/debugbar/expdebugbarpolicies.php); the section's words in every administration catalogue.
Updated
- Updated: An earlier version of the section, filtered by module and function and also shown to anonymous visitors
with debug output, gave way to this one; the policies are for
setup/setuponly. - Updated: The guide, feature page and help page of the debug bar describe the section, in English and German.
Tests
expDebugBarPoliciesTest(UP-01 to UP-04, UP-06 to UP-08),expDebugBarUserPoliciesTest(UP-05, UP-09, UP-10);expDebugBarUserTestchecks the role link.
2026-11-02: suggestions under the header search while typing
The header search field of the administration suggests while the user types, as Ibexa's admin does: content, the
pages of PDF documents, user accounts, content classes and the administration's own pages, in groups, each only as far
as the user may see it, and "View all results" with the number of matches at the end. In admin and admin4 alike,
in both copies of admin4's header search (header and narrow screen menu); not in the browse mode.
Upgrading: nothing to do; the suggestions are on. Roles that use the administration need content/read (the view
content/suggest asks for it, as content/search does). Clear the caches so the templates, the packed scripts and
stylesheets and the INI settings are rebuilt. Switch it off with admininterface.ini [SearchSuggestSettings] Enabled=false.
Added
- Added: The view
content/suggest/(text)/<text>(policy functioncontent/read, read only, GET, private JSON:{ query, total, groups: [ { id, title, count, items } ] }, 403 for the anonymous user, 404 when switched off), built byexpSearchSuggest(kernel/classes/expsearchsuggest.php). The groups and their sources:- content:
eZSearch::search()with the search engine of the site, which applies the read permissions; with eZ Find (the classeZSolrand the extensionezfindactive) eZ Find, else the kernel'seZSearchEngine, also whensite.ini [SearchSettings] SearchEnginenames an engine that cannot be loaded. Only the API the kernel and these7enxweb/ezfindpackage share is used (SearchLimit,SearchOffset,SearchResult,SearchCount). Each hit is checked again withcanRead(), an object is listed once. - pages: the PDF pages that hold the words, only with eZ Find, the extension
ezfindpdfdeepsearchactive and its fetch functionpdfdeepsearch/page_hits(which checks the document's read permission); without it a document is suggested as a whole. - users: accounts by login, e-mail address or name, only those
canRead()allows. - classes: by name or identifier, only for a user who may open
class/grouplist. - admin: the top tabs and the links of each tab's left menu, drawn by the design for the user
(
parts/<part>/menu.tpl, with all of its access checks), as "Setup › Cache management"; kept per user, rights and address forAdminPagesCacheTTLseconds in the cache itemsearchsuggest(cleared with the template and user caches).
- content:
- Added:
admininterface.ini [SearchSuggestSettings]:Enabled,MinLength=3,Groups[](order, on/off),Limits[]per group,PagesPerDocument,PageHitClasses[],Wildcard(auto: a*after the last word only withsite.ini [SearchSettings] EnableWildcard=true),AdminPagesCacheTTL,AdminMenuSkipParts[]. - Added:
javascript/page/search_suggest.jsandtemplates/parts/search_suggest.tpl, the same files inadminandadmin4: the field is an ARIA combobox with a listbox (aria-activedescendant), asked after 250 ms fromMinLengthcharacters, a running request cancelled when the text changes (AbortController),↓/↑,Enter,Esc, a click outside; the server's texts set as text, the typed words marked with<mark>through the DOM; no jQuery, no inline script or style.Ctrl+/andCmd+/focus the field from anywhere (/alone stays admin4's, outside fields). - Added: The "Suggestions" block of
stylesheets/modules/search.css(the same in both designs): the.a4-suggest*classes on the--a4-*tokens (dark mode), in the flow under the field in the narrow screen menu, touch-sized entries, an outline in forced colours. - Added: German texts (context
design/admin/search/suggest), and the sources ineng-USanduntranslated. - Added:
tests/tests/kernel/classes/expSearchSuggestTest.php(DB-less: settings, fallbacks, wildcard, matching, permission filter, menu links, answer) andtests/tests/design/AdminSearchSuggestTest.phpwith the node harnesstests/tests/design/js/search_suggest.js. Guide: Searching, "Suggestions while typing".
2026-11-02: the search page with a filter bar, the advanced search in it
The administration's search page (content/search) works as Ibexa's: the search field on top, a filter bar beside the
results (above them, folded under "Show filters", on a narrow page), the active filters as chips, a sort order and a
result table. The former advanced search is part of it. In admin and admin4 alike.
Upgrading: nothing to do. Clear the caches so the templates, the translations, the packed scripts and stylesheets are
rebuilt. An old address of content/advancedsearch leads to the search page; its query string is not read (see the
view parameters below), a posted advanced search form is taken in with the same search. Links and bookmarks of the
form content/search?SearchText=... now open an empty search: Search links with view
parameters shows how to rewrite them. A siteaccess that runs the classic admin design with eZ Find active keeps eZ Find's own
design/admin/templates/content/search.tpl, which comes before the kernel's design in the design chain.
Added
- Added: The fetch function
fetch( 'content', 'filtered_search', hash( 'offset', ... ) )and the classexpContentSearchFilters(kernel/classes/expcontentsearchfilters.php). It reads the filters from the request (SearchText,SearchModeall/any/phrase,ClassID[],ClassAttributeID,SectionID[],Modified,ModifiedFrom,ModifiedTo,Published,PublishedFrom,PublishedTowith the periods day, week, month, year and custom,Owneras user ID, login or e-mail address,Language,SubTreeArray[],SortBy,SearchPageLimitand the header'sBrowsePageLimit), checks every value against the installation (an unknown class, section, language or user is dropped, an invalid date reported) and searches:- with eZ Find (
site.ini [SearchSettings] SearchEngine=ezsolrand the ezfind fetch functions present):fetch( 'ezfind', 'search' )withquery,offset,limit,class_id,class_attribute_id,section_id,subtree_array,filter(modified,published,owner_id,language_code),facet(class_identifier,section_id,translation, for the counts),sort_byandspell_check, all parameters of the packagese7enxweb/ezfind3.0. The extended attribute filtersezfindallwords(all words) andezfindcollapse(the parts of a document folded into it, where an installation has it) are asked for only whenezfind.ini [ExtendedAttributeFilters] FiltersList[]registers them and their class exists. "Any word" joins the words with OR; - with the built-in engine:
fetch( 'content', 'search' )withtext,class_id,class_attribute_id,section_id,subtree_array,publish_timestampandsort_by. It cannot filter by modification, owner or language, find any of the words, count or sort by relevance: those filters are not shown. Without words it searches only withAllowEmptySearch=enabled. The parameters of the advanced search are mapped:SearchContentClassIDtoClassID[],SearchContentClassAttributeIDtoClassAttributeID,SearchSectionIDtoSectionID[],SearchDate1, 2, 3, 5 toPublished=day|week|month|yearand 4 to a custom range of three months,SearchTimestamptoPublishedFrom,PhraseSearchTextto a quoted phrase in front of the words (alone:SearchMode=phrase).
- with eZ Find (
- Added:
javascript/page/content_search_filters.js(both designs): Apply is disabled until a filter changed, the from/to dates show for a custom range only (a typed date chooses it), the sort order searches on change, empty fields are not sent, the bar folds on a narrow page. No jQuery, no inline script; the page works without it. - Added: The "Results" block of
stylesheets/modules/search.css: the two columns from 820 px of page width (a container query), the compact filter bar with counts, chips, the result table; tokens only, dark mode, forced colours. - Added: German texts (context
design/admin/content/search), the sources ineng-USanduntranslated. - Added:
tests/tests/kernel/classes/expContentSearchFiltersTest.php(DB-less: checking, mapping, the parameters per back end, dates, query strings, chips, facets, the search run) and the node harnesstests/tests/design/js/content_search_filters.js, run byAdminSearchTest.
Changed
- Changed:
content/search.tpl,content/searchresult.tplandcontent/advancedsearch.tplofadminandadmin4are the same files. The result table has the columns type, name (with the path, the words found and a PDF's pages with ezfindpdfdeepsearch), modified, content type, translations (each a link to the item in that language) and Edit; "Edit selected" stays. The header reads "Results for “words” (count)", an empty page and a search without matches say what to do, "Did you mean" keeps the filters, the pages keep every filter and the sort order. - Changed: the page is
content/search_page.tpl;content/search.tplandcontent/advancedsearch.tplinclude it, the advanced search's own form is gone. eZ Find brings its owncontent/search.tplfor the design admin, read beforedesign/admin's: throughsearch_page.tplan old advanced search address still shows this page there. The kernel viewcontent/advancedsearchis unchanged, so other designs keep their template. - Changed: the search statistics (
LogSearchStats) count the filtered search the page showed, also withSearchViewHandling=default: the viewscontent/searchandcontent/advancedsearchtake thesearch_datathe template leaves. With that page the views leave their own search out (Search::templateSearchesItself(): the design'scontent/search.tplincludescontent/search_page.tpl), so a search runs once, with or without eZ Find. - Changed: a subtree or an owner in the filters shows its name only when the user may read it; without a subtree the
siteaccess's
PathPrefixtree is searched, ascontent/searchdoes; filters alone search only whereAllowEmptySearchallows it; "All words" passes the text on as typed, "Any word" keepsNOTand-word. - Changed:
design/admin/templates/content/searchresult.tplhas no inline script any more (javascript/page/content_searchresult.js, as in admin4). - Changed: the search page takes its search as view parameters only, never as a query string:
content/search/(text)/easter/(class)/2,16/(section)/1/(sort)/name_asc(expContentSearchFilters::VIEW_PARAMETERS:text,mode,class,attribute,section,modified,modified_from,modified_to,published,published_from,published_to,owner,language,subtree,sort,limit; a list once, its values joined by commas). The filter bar and the header search field ofadmin,admin3andadmin4are posted (method="post") and the view answers with a 303 to that address (expViewParameters); the pager, the chips, "Reset", "Did you mean" and "Go to the first page" link to it (expContentSearchFilters::viewSuffix()). The viewcontent/advancedsearchanswers in the same way withcontent/search. A search of the browse window (Mode=browse) keeps its own handling. Tests:expContentSearchFiltersTest(SF-14),AdminSearchTest. - Changed: every administration design has the suggestions and the search page, from one copy of each file in
design/standard, so admin4 still needs no other admin design:content/search_page.tpl,parts/search_suggest.tpl,parts/header_search.tpl(the header search with suggestions, included by thepage_search.tplof admin3 and admin4), the scriptssearch_suggest.js,page_search.js,content_search_filters.js,content_searchresult.jsandmodules/search.css. Onlycontent/search.tpl,advancedsearch.tplandsearchresult.tplstay in both admin and admin4, because design/standard has front-end templates of those names. admin3 now suggests too and its header has no inline script. The suggestions list is no longer tied to.searchblock, and Ctrl+/ finds any field with suggestions. The AdminUI design (exp_adminui 1.0.0.58) drops its own search pages and posts its header search with suggestions. Tests:AdminSearchTestAS-11,AdminSearchSuggestTestSG-01.
2026-11-02: overview and detail pages as dense rows
The overview pages of the administration (roles, sections, states, classes and class groups, PDF exports, RSS, URLs,
packages, drafts, workflow groups) drew one card per item, so a screen showed three or four items. They read as table
rows again, in admin and admin4 alike: one row per item with fixed columns, the facts on one line and the row
actions as icons. The detail pages (class/view, section/view and others) show the facts of a card on one line and
the attributes of a class as rows. One density, no switch: the decision of 15 October stands.
Upgrading: nothing to do. Clear the design cache so the packed stylesheets are rebuilt.
Changed
- Updated: Inside
.a4-page,ul.a4-cardsreads as a table: rows with a rule, zebra and hover, the status marks as an edge at the row's start, the facts of a row on one line, and from 701 px the rows are subgrids of the list (name, buttons, facts in columns). More than three facts take a line of their own below the name: four in equal columns, five and more as running text that wraps between the facts. Inurl/listthe address takes the whole first line of its row, the facts and buttons the second. - Updated: The row actions View, Edit, Open, Assign and Copy of card rows and tables are frameless icon buttons
(Bootstrap Icons), their label kept for screen readers; other actions, external links and buttons with an icon of
their own keep their text. Each such button has a
title(its label, or itsaria-labelwhere it has one) as tooltip, indesign/adminanddesign/admin4: class, class group, role, section, state, workflow group, URL, bookmark, pending, session, collected information, broken relation and settings lists. - Updated: From 601 px,
.a4-tablecells inside.a4-pagetake 4 px above and below; an action cell does not wrap, and a date and its modifier keep a space between them. On touch screens (pointer: coarse) the row buttons keep their full size. - Updated:
class/grouplistis a table: select, name, ID, classes, objects, modified and actions in columns. The class chips and the "Removing it" fact are hidden visually only (screen readers still read them); a group whose removal removes objects takes the red edge, andclass/removegrouplists the details before it asks. The facts carry classes of their own (.a4-classgroup-last,-modified,-removing,-classes), so the columns do not depend on their order. - Updated: The facts of every card that is not a list row (
.a4-card > dl.a4-facts, also inside its.a4-card-body: detail pages such asclass/viewandsection/view, settings and status pages) are one line of "Label: value"; a long value wraps. The one-line empty state of a section (.a4-section > p.a4-empty) is slim, and sections are 16 px apart. The attributes ofclass/vieware rows with their datatype settings on one line. - Added:
tests/tests/design/AdminListRowsTest.php. Guide: Styling the administration, Components.
2026-11-02: an image field takes a copy of an existing image
The edit form of an image field (ezimage) offers, besides the upload, Choose an existing image: the editor
browses the content tree (Browse for an image, starting in the media library) or searches by name (Search images
by name, thumbnails of the objects found) for an object that has an image, and its image file is copied into the
field being edited. The copy is a new file under the edited object's own storage path, stored through the same code
as an upload, so the field behaves as if the file had been uploaded: the aliases are made from it, it can be removed
or replaced, nothing refers to the other object, and removing the other object later leaves the copy. The other
object is only read. In admin, admin4 and every design that uses the standard edit template; works without
JavaScript (both the browse and the search are posts of the form).
Upgrading: nothing to do. Regenerate the kernel autoloads (a new kernel class) and clear the caches so the templates, the packed stylesheets and scripts and the INI settings are rebuilt.
Added
- Added:
expImageCopy(kernel/classes/datatypes/ezimage/expimagecopy.php):copyFromObject()checks that the selected object exists and that the current user may read it (content/read; refused with the same message as an object that does not exist), takes its image field (the fieldimage, else the first image field with a file), reads the file through the cluster file handler (DFS safe) into a private working copy and checks it as an upload is checked (the size limit of the field being edited, the file name blacklist, an image by name and by content, SVG only when accepted and then sanitized), then stores it witheZImageAliasHandler::initializeFromFile(). An empty alternative text takes the source image's.search()lists at most 12 objects of the classes with an image field whose name contains the text, through the tree fetch, which applies the read policies. - Added: The custom actions of the image datatype
<attribute id>_browse_for_image(content browser, browse.ini[ExpImageFromObjectToDataType], start nodemedia, selectable: the classes with an image field),_set_image_from_browse(the browser's answer),_search_existing_image(the search field<base>_data_imagesearch_<attribute id>) and_use_existing_image(the button's value is the object id). A refused copy is the field's validation error. - Added:
design/standard/javascript/expimagepick.js: Enter in the search field searches instead of sending the form with its default button. - Added: The UI and error strings in every administration catalogue.
- Added:
tests/tests/kernel/datatypes/ezimage/expImageCopyFromObjectTest.php(uses the installation's database, skipped without one): the copy is a file of its own with the source's bytes, the source is unchanged and its removal leaves the copy, the alternative text, refusal without read access, of an object without an image and of input that is no object id, and the search.
Updated
- Updated:
design/standardanddesign/admin4content/datatype/edit/ezimage.tplhave the "Choose an existing image" section; the upload is unchanged. Its styles are.a4-content-imagepick-*instylesheets/modules/content.cssofadmin4andadmin(tokens only). - Updated:
eZImageAliasHandlerhas the attributeexisting_image_pick(the search and copy state of the current request, kept on the handler, never in a static);eZImageType::isAcceptedImageFileName()is public. - Guide: Images, media and uploads.
2026-11-02: migration remotes and deploys in the administration, staged deploys and rollback
Upgrading: run the upgrade (./console exp:upgrade:status, then --apply) or the 6.0.0 to 6.0.15 database update
of your engine: the step 6.0.15-migration-deploy-tables creates the tables exp_migration_deploy and
exp_migration_deploy_host. Give the policies migration/remotes and migration/deploy to the roles that may manage
remotes and deploy.
Added
- Added: Setup > Migrations > Remotes lists the installations this one deploys its migrations to, tests a connection
(the remote's migration status read once, or for FTP one file read back) and opens a wizard that adds or changes a
remote: environment, groups, transport, address, PHP command line, a credential by secret name or environment
variable, and the host key. Review shows the lines written to
settings/override/migration.ini.append.phpthrough the INI editor; a secret is never written. - Added: Setup > Migrations > Deploys starts a preflight or a deploy of a selection of remotes or groups as a background job, follows every remote live over the migration stream, keeps the history from the stored reports, and on a deploy's page approves the gate of a staged deploy or rolls the deploy back. Saving or removing a remote, deploying, approving and rolling back ask "Verify it's you".
- Added:
exp:migration:deploy --staged(stages by environment in the order of[RemoteSettings] StageOrder[], all or nothing within a stage, a gate before each next stage),--continue=<deploy id>(approve the gate) and--rollback=<deploy id>(take back every remote that applied it), and--login,--run-id,--progressfor the administration's job. - Added: The tables
exp_migration_deployandexp_migration_deploy_host(kernel schema, the update files of MySQL, PostgreSQL and SQLite, upgrade step6.0.15-migration-deploy-tables). A deploy that fails or waits at a gate sends a plain-text notice in the kernel's admin mail model ([RemoteSettings] Notify[],NotifyEmail); the audit namesystem.migration.remoterecords changes to remotes. - Guide: Remotes and deploys, Remote deploy, in English and German.
Tests
expMigrationRemoteDeployStagedTest(no database: stages, gates, rollback, the records and the notices);expMigrationJobTest,expMigrationCommandContractTest,expAuditRecordTestandAdminContentSecurityPolicyTestcover the new options, the audit name and the pages.
2026-11-02: jQuery 4 without jQuery Migrate
Upgrading: nothing to do. A design or extension of your own that still needs a removed jQuery API switches Migrate
back on with ezjscore.ini [eZJSCore] LocalScripts[jqueryMigrate]=jquery-migrate-4.0.2.min.js (and
ExternalScripts[jqueryMigrate] for the CDN), in an override.
Updated
- Updated:
ezjsc::jqueryloads jQuery 4 without jQuery Migrate:LocalScripts[jqueryMigrate]andExternalScripts[jqueryMigrate]ofezjscore.iniare empty. Every admin siteaccess (admin, adminui, admin2, admin4, admin4l, editor) was checked page by page, including the edit forms, the online editor's dialogs, the multi-object edit, the priority drag and drop and the layouts editor: no Migrate message, and a static scan of the admin designs and the extensions they load found no removed API. Migrate 4.0.2 is still shipped, and the comment inezjscore.ininames the settings that switch it on again. - Guide: jQuery in designs, in English and German.
Tests
ezjscoreVendoredLibrariesTestaccepts an emptyjqueryMigrateas long as both settings are empty and the comment names the shipped release.
2026-11-02: the DS Editor's Change database opens the database chooser
Upgrading: nothing to do.
Updated
- Updated: Once the warning was confirmed in the session, the DS Editor's (sevenx_dse) Change database link leads to the database chooser instead of the warning again: the site's database with one click, the logins remembered in the session with the current connection marked, and the login form with every driver for any other database. Feature, in English and German.
Tests
SevenxDseDatabaseChooserTest(no database): the redirect, the chooser template, the login list without passwords and the translations of every catalogue.
2026-11-03: exp:checkdbfiles and the database check scripts compare with a git tag, not svn
Upgrading: nothing to do. On an installation that is not a git checkout, run exp:checkdbfiles --no-verify-branches.
Updated
- Updated:
exp:checkdbfilescompared the update files of the released branches with an export fromsvn.ez.no, which is gone, so every run without--no-verify-branchesended with aCline. It now compares them with a release of the kernel's own git repository (git archive):--compare-with=<tag or commit>, by default the newest tagv<major>.<minor>.<patch>. Without a git checkout or a tag it says so and stops. Compared withv6.0.14it reports, asC, the MySQL update files of older branches changed since then on purpose (the storage engine is set withdefault_storage_engine). - Updated:
bin/shell/checkdbupdate.shtakes the schema of the release it starts from from the git tagv<version>instead of an svn export;bin/shell/checkdbschema.shdrops its unused svn addresses.
Tests
CheckDbFilesVersionListTest: no svn left in the command and the two scripts, andexportGitRelease()extractsupdate/databaseof a commit and refuses an unknown one.
2026-11-02: eZSection::fetchByIdentifier() returns the section of the identifier
Upgrading: nothing to do.
Updated
- Updated:
eZSection::fetchByIdentifier()returns the section of the identifier, not an older one kept under its id. The sections read in a request are cached by id and by identifier; a section read by its identifier was only kept when nothing was cached under its id yet, so a section that had changed its identifier, or an id given again to a new section after another was removed (SQLite hands out the next free id), returned the other section. The migration section step then refused with "another section has the identifier ...". The row read now replaces a cached one whose identifier differs.
Tests
- The migration tests forget the cached sections when they switch the database (
expMigrationRunnerEngineChecks), as they already did for the languages; the structure step tests pass on SQLite again.
2026-11-02: user management from the console
The exp:user commands manage accounts from a shell, a deploy script or a CI job the way the administration does:
exp:user:list, show, create, edit, disable, enable, delete, reset-password, pending and approve. A
new account is published with the content/publish operation, so its workflows and the approval of new accounts run;
disabling is the setsettings operation and ends the account's sessions; a registration is approved or rejected as
Users > Unactivated and the approval item do. Every change reaches the audit and the user audit as in the
administration. Every changing command is a dry run unless --apply; a password is never taken on the command line
(--password-stdin, or generated and shown once); the anonymous user, the administrator accounts, the user who acts
and the last administrator are never disabled or removed. --format=json prints exp-user/1.
Upgrading: nothing to do.
Added
- Added: The commands
exp:user:list,exp:user:show,exp:user:create,exp:user:edit,exp:user:delete,exp:user:disable,exp:user:enable,exp:user:reset-password,exp:user:approveandexp:user:pending, with their entry points inbin/phpand the generated reference pages in English and German. - Added:
expUserManager(kernel/classes/user/expusermanager.php), the work behind the commands. - Added: The guide User management from the console, in English and German.
Updated
- Updated: The services
expuserandexpaccountcheck a new login, e-mail address and password and send the password reset link throughexpUserManager, so the console and the services apply the same rules. - Updated: The user hygiene list takes the console's filters
enabledandunactivatedand a list of account ids; the page itself is unchanged.
Tests
expUserCommandContractTest(UC-01 to UC-10, no installation needed),expUserManagerSqliteTestandexpUserManagerDatabaseTest(UE-1 to UE-7 on SQLite, MySQL/MariaDB and PostgreSQL).
2026-11-03: user management from the console, the second part and the help manual
Fourteen more exp:user commands complete the family: groups (the group tree; create, rename, move, remove an empty
group), roles (list, assign and unassign roles of users and groups, with a subtree or section limitation),
sessions (list and end tracked sessions), unlock, 2fa (show, reset the second step for a lost phone, remove
passkeys), require-password-change, resend-activation, export (CSV or JSON, never a password or hash), import
(each account created as exp:user:create does; a password column is refused), bulk (disable, enable, delete,
approve, reject or unlock over a filter; --apply needs --confirm with the count the dry run showed), audit,
hygiene, whoami and check-login. Each does what the administration does, through the same kernel paths and the
same audit events. The help center has a manual for them in English and German.
Upgrading: nothing to do.
Added
- Added: The commands
exp:user:groups,exp:user:roles,exp:user:sessions,exp:user:unlock,exp:user:2fa,exp:user:require-password-change,exp:user:resend-activation,exp:user:export,exp:user:import,exp:user:bulk,exp:user:audit,exp:user:hygiene,exp:user:whoamiandexp:user:check-login, with their generated reference pages in English and German. - Added:
expUserGroupManagerandexpUserBulk(kernel/classes/user/). - Added: The help manual User management from the console (command groups, FAQ, troubleshooting, tasks such as onboarding and offboarding, audience pages), the feature page User management from the console, the specification exp:user JSON output and exit codes and glossary terms, each in English and German.
Updated
- Updated: The guide User management from the console and the help pages on members, editor and administrator accounts, the user audit log, accounts and sign-in and user hygiene point to the console commands.
Tests
expUserCommandContractTestUC-11; UE-8 to UE-14 inexpUserManagerSqliteTestandexpUserManagerDatabaseTest.
2026-11-03: the website sidebar's Export buttons work while "Verify it's you" waits (expwt)
Upgrading: nothing to do; update the expwt extension.
Updated
- Updated: In the website sidebar's Export panel, the "Fields as a file" buttons (Markdown, Word), Create link and Withdraw show a busy state instead of turning grey, always come back and say a failure. While "Verify it's you" waits in its window, a row under the buttons brings the window back or cancels it, and a second action waits for the same window instead of being left hanging (expwt bb5ba8d).
Tests
tests/js/export.test.jsof expwt: the busy state, the waiting row, Cancel and one window for two actions.
2026-11-03: the header search takes the section from the search's view parameter
Upgrading: nothing to do.
Updated
- Updated: The scope popup of the header search (every administration design) preselects the section of the
current search,
/content/search/(text)/<text>/(section)/<id>, from the view (search_section_id), and the page layouts key their cache on it. No template readsSectionIDfrom the query string any more.
Tests
AdminSearchTest::testTheHeaderSearchTakesTheSectionFromTheViewParameter;Admin4lReviewTestAL-R1 andAdminStoredValueEscapingTestcheck the new cache key.
2026-11-03: fixes from the release audit
Upgrading: a monitoring check that matches the text of /ezinfo/is_alive must accept Exponential is alive
(ezinfo/is_alive answers "Exponential is alive"). A bookmark of the user
audit log or the account check with filters in a query string (?user=..., ?filter=...) opens the unfiltered page.
Updated
- Updated:
/ezinfo/is_aliveanswersExponential is alive(waseZ Publish is alive); without a database it still answersNo connection. - Updated: User audit log (
user/audit), Check user accounts (user/hygiene), the security activity (user/security) and "Verify it's you" (setup/reauth) take their filters as view parameters,/user/audit/(user)/<login>/(from)/<date>, never as a query string. The filter forms are posted and answered with a 303 to that address; the scope of "Verify it's you" is chosen by a link per siteaccess. - Updated: "Verify it's you" asks on
user/hygienefor the bulk actions and the CSV only (Rules[]=user/hygiene DisableButton EnableButton UnlockButton ConfirmButton ExportButton), so filtering the list does not ask. - Updated: The JSON endpoint the module code generator writes (Setup > Database tables) reads
/(offset)/,/(limit)/and/(<field>)/instead of a query string. - Updated: Four calls that threw
ArgumentCountErroron PHP 8 when reached:eZMySQLiDB::escapeString()before a connection, the MySQLi cluster backend's error report without a connection, adding a product with option sets to the basket, and theeZCodePageCodecconstructor. - Updated: The community activity block of the dashboard tags its links with the campaign "Exponential Community Project".
- Updated: CI fails when the autoload generation fails; its GitHub actions run on Node 24 (
actions/checkout@v7,actions/cache@v6,actions/setup-node@v7,actions/upload-artifact@v7). - Updated: PHPStan reports baseline entries that match nothing any more (
reportUnmatchedIgnoredErrors: true).
Removed
- Removed: 109 entries of
phpstan-baseline.neonthe code no longer needs.
Tests
ViewsTakeViewParametersTest,eZMySQLiDBEscapeStringWithoutConnectionTest;expReauthRulesTestchecks the hygiene rule.eZExtensionWithOrderingTest,expRequestLogRequestEndTestandeZExecutionPerRequestRegistrationTestleave PHP's exception handlers as they found them (no risky tests);eZImageCopyOwnFilesTestkeeps the object's name for its second version; five tests raise no deprecations on PHP 8.5.
2026-11-03: the archive library is Exponential's own fork
Upgrading: nothing to do; composer update installs se7enxweb/expcomponents-archive, which replaces
zetacomponents/archive.
Updated
- Updated: Package import, export and upload use
se7enxweb/expcomponents-archive1.5.2, Exponential's fork ofzetacomponents/archive1.5.1 (the sameezcArchive*classes), in place of the upstream package, which has had no release since 2023. It prints no deprecations on PHP 8.4 and 8.5 (implicitly nullable parameters, missing tentative return types,substr()given null,octdec()given padding), extracts GNU tar entries with long names or long links again, and its own test suite runs on PHP 7.4 to 8.5.
2026-11-03: runtime audit fixes: setup pages, preview links, SQLite keys, aborted requests, sign-in, HSTS
Upgrading: Velocity no longer sends Strict-Transport-Security unless [HTTPSSettings] HSTSMaxAge is set in
settings/override/velocity.ini.append.php; an installation that relied on the 300 seconds shipped before sets it
there.
Updated
- Updated: Setup, RAD survey (
/setup/rad) no longer fails with an error page when a directory belowextension/cannot be read by the web server user: the survey leaves it out. The settings and template walks of the content check and the help examples generator skip such a directory the same way. - Updated:
expContentPreviewLink::tokenFor()gives the token of a share link only to the user who made the link or who may share its version, as its description always said; anybody else gets null. - Updated: On SQLite, a table whose extra unique key is the one the shipped schema gives it (the session's
token_hash,exp_secret.name,ezcontentobject.remote_id,ezuser.login, the primary key ofezcontentobject_attribute) is still written without REPLACE, but no longer logs a warning that asks to rebuild the table. A key that the shipped schema does not have is still reported. - Updated: A request whose browser went away (a closed server-sent event stream) no longer writes "Fatal error" without a cause to the error log and renders no error page; a notice says the client closed the connection.
- Updated: The administration sign-in page (
design/admin) no longer redefines the view'spasskey_optionsvariable, which logged "Variable 'passkey_options' is already defined" on every sign-in page. - Updated:
[HTTPSSettings] HSTSMaxAgeinvelocity.iniships as 0 (no header) with a generic comment, since a browser keeps HSTS after it is lowered; the documentation says to start at 300 insettings/override.
Tests
expVelocityEnginesTest::testTheDefaultResponseHeadersAndHstsReachTheServerexpects no HSTS by default.- Checked by hand: the RAD survey's source walk as the web server user with an unreadable extension directory; the share link token refused to a user who did not make the link.
2026-11-03: view counts for multi-site installations
Upgrading: nothing to do for a single site; logfile.ini [AccessLogFileSettings] is read as before, and the first
run takes over the old updateview.log. To count several siteaccesses in one run, set
[ViewCountSettings] SiteAccessList[] or AllSiteAccesses=enabled
(View count statistics). Counts are lower for the same traffic on
purpose: robots, errors, redirects, files and module pages are no longer counted
(View counter 6.0.15).
Why: the cronjob kept one resume file for whatever log it read, found its place by the text of the last line, counted the front page for the tree root and every line of a shared log for every siteaccess, so installations with several sites, several logs or one log for several sites counted twice, never or for the wrong node, and rotation lost lines.
Added
- Added:
./console exp:viewcount:updatecounts the page views of the access logs intoezview_counter, with--dry-run,--top(the most viewed nodes and the most requested paths that are no node),--all-siteaccesses,--siteaccess-list,--log-file,--state-dir,--from-start(dry run only),--skip-to-end,--reset-stateand--format=json(exp-viewcount/1); exit codes 0, 1, 2 and 3 (a log was locked by another run). - Added: Multi-site counting: one run counts every siteaccess of
[ViewCountSettings] SiteAccessList[](or every non-administration siteaccess withAllSiteAccesses=enabled), each from the log its ownlogfile.ininames; siteaccesses sharing a log are read once and each line goes to the siteaccess the kernel's siteaccess matching finds for its host and path (MatchSiteAccess,[AccessLogFileSettings] Host,Port,Hosts[]). - Added: A resume state per log and per siteaccess (
var/<site>/viewcount/*.json,StateDir), by inode and byte offset with a hash of the first line, written atomically after the database commit, with a lock per log (LockTimeout); rotated copies (RotatedSuffixes[], also compressed) are read to their end before the new log. - Added:
logfile.ini [ViewCountSettings]decides what is a page view:Methods[],Statuses[],BotUserAgents[],SkipEmptyUserAgent,SkipExtensions[],SkipPathPrefixes[],ViewModes[],MaxLineLength,LineRegexp. - Added:
eZViewCounter::addCounts()adds the views of many nodes in one transaction;eZSiteAccess::matchAddress()finds the siteaccess of an address without the side effects of a web request. - Added: The guide View count statistics
(German), the behavior note
View counter 6.0.15
(German) and the generated pages of the command and of
[ViewCountSettings]in both languages.
Updated
- Updated: The cronjob
updateviewcount.phpis the same work as the command: "/" counts for the siteaccess'sIndexPagenode instead of the tree root; a path is looked up with itsPathPrefixfirst and then without one (PathPrefixExclude[]respected, the siteaccess's ownPathPrefixwhenlogfile.ininames none);SitePrefix[]removes a whole first path element only;/content/view/<any view mode>/<node>, URL encoded UTF-8 paths and view parameters reach their node; aliases are looked up in each siteaccess's languages; lines are read by a regular expression for the common, combined andvhost_combinedformats (IPv6, escaped quotes, fields after the user agent, no client address) instead of being split at quotes, and are no longer cut at 1023 bytes; all views of a run are written in one transaction and nodes are checked 500 at a time. - Updated: The cronjob's description no longer speaks of an "in-memory view count buffer", which never existed; it says that it counts the page views of the access logs.
- Updated: The state of the earlier cronjob (
updateview.log, or the per-logupdateview-<log>.logof the multi-site prototype this work builds on) is taken over on the first run: after its last line, or by its time when the line is no longer in the log.
Tests
tests/tests/kernel/classes/viewcount/: the parser (formats, IPv6, quotes, malformed lines), the filter (methods, statuses, robots, files, module paths, the shipped settings), the path resolver (IndexPage, view modes, PathPrefix and its fallback, PathPrefixExclude, UTF-8, view parameters, SitePrefix, languages), the reader and state (resume, a line being written, rotation by rename, copytruncate and gzip, truncation, long lines, two logs, two siteaccesses in one log, atomic state, the lock), the updater without a database (single site, dry run, shared log, hosts, a failed write, the old state taken over, the maintenance options, the report), the siteaccess matching, the configuration, andexpViewCountSqliteTestcounting intoezview_counteron SQLite.- Checked by hand on the demonstration server: dry runs of every siteaccess over an Apache and a Velocity log, and one real run into a copy of the database (616 views added, none on a second run, the live table unchanged).
2026-11-03: runtime audit fixes: help of access extensions, link check run time, audit dashboard speed
Upgrading: nothing to do. [linkCheckSettings] MaxRunTime (cronjob.ini) is new, 900 seconds by default.
Updated
- Updated: The help center lists the documentation of the extensions that are active only in a siteaccess
(
ActiveAccessExtensions[], such as expai and sqliimport) again: their siteaccess settings were read from a path the settings reader does not accept, so the list was always empty andexphelp/extensions/<name>answered 404. - Updated: The link check (
linkcheck.php, part infrequent) stops checking after[linkCheckSettings] MaxRunTimeseconds (900 by default; 0 checks until all are done). The links left are the ones checked most recently, and the next run starts with them. A run with many links to one host took most of an hour inHostDelayalone. - Updated: The audit dashboard keeps the figures of the six days before today for an hour (only today's are counted afresh), keeps the index's record count for ten minutes, and counts today's records by the time index on SQLite. On alpha (800 000 records) a page without cache went from 9 to 12 seconds to about half a second, and once an hour the past days are counted again.
Tests
expLinkCheckTest::testFileSettingsAndRecheckcoversMaxRunTimeandLinkcheck::overTime().
2026-11-03: the Exp Debug bar's User and Rendering tabs with sub-tabs, the session; the request and the user in the bar
Upgrading: whoever gets the debug output now reads the bar's settings, lists and caches without the policies; a site
that gives the debug output to visitors it does not trust with its configuration sets debugbar.ini [DebugBarSettings] ReadAccess=privileged.
Added
- The User tab shows the user's own PHP session in its sub-tab Session, loaded only when it is first seen (bar
open, User tab and sub-tab shown; the answer
Cache-Control: private, no-store), by the new server functionexpdebugbar::session: the keys of$_SESSIONas a table per group (the kernel and others first, then each active extension whose name starts its keys), with type, size as stored and value; nested arrays and objects open into indented rows, short ones read on one line, a search finds keys and values. Values of keys that look like secrets are masked, with everything below them (debugbar.ini [DebugBarSettings] SessionMask[]: token, csrf, nonce, secret, passw, hash, key$, credential, auth, jwt, bearer, otp, saml, assertion, ticket, sid$). Below, the frame: started, cookie name, the session id shortened to four characters, handler, lifetimes from PHP andsite.ini [Session], the cookie's parameters; the save path and the handler class forsetup/setuponly. At most 8 levels, 200 children and 3000 entries, keys not listed counted; keys and texts that are not UTF-8 as hex. The masks hide secrets, not personal data. Only ever the session of the request that asks, for whoever sees the User tab. See the Debug bar. expDebugBarSession(kernel/classes/debugbar/expdebugbarsession.php).- Added: Session values are also masked by what they look like, whatever their key is called: a JSON Web Token, the
credentials of an Authorization header, a PEM private key, well-known API key prefixes, 32 or more hex digits, a
long random text; a key that looks like a token is shortened and masked with everything below it. The badge says
why. More patterns:
debugbar.ini [DebugBarSettings] SessionValueMask[]. - Added: "Copy as text" copies the session as
path = valuelines, built on the server from the masked entries. - Added:
debugbar.ini [DebugBarSettings] SessionWarnBytes(65536): a larger session is marked large in the overview and the sub-tab with the reason; a hint names PHP's locking file handler whensession.save_handler=files. - Updated:
expdebugbar::sessionis asked for with POST (with the form token when signed in), refuses a request the browser marks as sent by another site (Sec-Fetch-Site), and every answer, a refusal included, carriesCache-Control: private, no-store, max-age=0,Pragma: no-cacheandExpires: 0, so no browser, proxy or Velocity response cache keeps it. The sub-tab panels take the focus (tabindex="0"). - All 156 catalogues have the new words of the debug bar's session and sub-tabs.
Updated
- Updated: The User tab opens with a compact overview (user, logged in, siteaccess, the session's number of keys and
size, the cache profile against anonymous visitors, the number of role assignments) and has four sub-tabs: Cache
profile, Preferences, Roles and policies (the policies as before, for
setup/setup), Session. The sub-tab shown last is kept, the arrow keys, Home and End move between them, the filter box counts the matches of each; the explanations are folded behind "?". - Updated: The Rendering tab opens with a one-line overview (the module view, the content view cache in a word, the
cache blocks read from the cache, the templates rendered and compiled) and shows its parts as sub-tabs like the User
tab: Controller, Template tree, Content view cache, Cache blocks, Templates; the explanations folded
behind "?".
expdebugbar.jshandles the sub-tabs of every tab (data-scope;localStorageexp-debug-<tab>-subtabandexp-debug-<tab>-help). - Updated: "Keep open on reload" reads "Keep open" in the bar of the default theme, the whole words as its tooltip, so the bar wraps later.
- Updated: The bar starts with the request in every theme, not only in the toolbar theme: the HTTP status and module/view, green, amber for a redirect (3xx but 304 Not Modified), red from status 400 (before: amber from 400, red from 500); a click opens the Rendering tab (before: the Server tab), the Server tab for users who do not see it.
- Updated: Whoever gets the debug output (
DebugByIP,DebugByUser), logged in or not, reads everything the bar shows: the settings with their values and file paths, the IP and user lists, the change log and the IP test, the cache list, the policies of their roles, the session's save path and handler class, the User and the Rendering tab with their chips, the versions of PHP and Exponential, OPcache and APCu, the process and the host. Changing a setting still needssetup/setup, clearing a cachesetup/managecache, with the form token; a reader sees the controls disabled and the cache list without its buttons. The newdebugbar.ini [DebugBarSettings] ReadAccessdecides it:debug, the default, orprivilegedfor the rule before (expDebugBarRegistry::readAccess(),readsAll()): the settings, the log and the policies forsetup/setup, the cache list forsetup/managecache, the versions, OPcache and APCu for either, the User and the Rendering tab forsetup/setupand an anonymous visitor with debug output. See the Debug bar. - Fixed: A reader by the debug output (
ReadAccess=debug, nosetup/setup) gets nobody else's data: the change log comes without the user id, login and address of whoever made each change, the IP and the user list are masked as secrets, the IP test answers for the reader's own address only without the lines of the list, and the summary's matched IP line stays withsetup/setup(expDebugBarServerFunctions::readsWithoutPolicy(),forReader(),listsMasked(),ownIpAnswer()). The guide warns that debug output on a public address shows every visitor their own session and recommendsReadAccess=privilegedin production. - Fixed: Class icons an extension maps to a file the icon theme does not have (CJW Newsletter's crystal paths under
bootstrap-icons in admin4) fall back to the theme's own mapping of that class instead of losing the icon
(
eZWordToImageOperator::themeOwnIcon(),themeOwnMap()); bootstrap-icons maps the six CJW Newsletter classes; a figure row that wraps keeps its height instead of overlapping the text below it. - Updated: The user chip names the login after a person icon (
admin · VC 3f9ac21e). - Updated: The guide, feature page and server functions of the debug bar describe it, in English and German; ger-DE and eng-US have the new words.
Tests
expDebugBarSessionTest(US-01 to US-06),expDebugBarUserTest::testTheChip,expDebugBarUserPoliciesTest::testWhoMaySeeTheTab(UP-05, both values of ReadAccess) andtestReadAccess(UP-11: the setting,readsAll(), the cache list without its form for a reader),testAReaderByTheDebugOutputGetsNobodyElsesData(UP-12),eZWordToImageOperatorDesignThemeTest,expDebugBarReportToolbarThemeTest(the request in every theme, the new colours, 304 and 1xx),expRenderStatsTest::testTabEscapesCutsAndMarks(the overview and the sub-tabs of the Rendering tab),expDebugBarReportPanelHeightTest(the shorter label).
2026-11-03: the tables of active extensions on upgrade, site packages without extension packages
Upgrading: run ./console exp:upgrade. The new database step 6.0.15-active-extension-tables creates the missing
tables of every active extension that ships a share/db_schema.dba, and does nothing where they are there.
Added
- Added: Upgrade step
6.0.15-active-extension-tables(expUpgradeStepActiveExtensionTables, database stage, every engine). For each extension that is active globally (ActiveExtensions[]) or in a siteaccess (ActiveAccessExtensions[]) and ships ashare/db_schema.dba, the step creates the tables of that file that the database lacks, through the engine's schema handler, as a new installation does after its site package. Nothing is dropped or changed and noshare/db_data.dbais loaded. The dry run shows the SQL. On alpha the step reports every table of the 14 active extensions with a.dbaas present. - Added: Extensions of a new installation (German
too), the one list of the extensions of 6.0.15. For each extension it gives where it comes from (kernel tree, root
composer.json, php-floor), who activates it on a new installation (the kernel, the administration, each site package), whether it has tables, and where those come from. The extension pages, the distribution page and the multisite page link to it.
Updated
- Updated: The site packages no longer require the extension packages
ezwt_extension,ezstarrating_extensionandezgmaplocation_extension(sevenx_site, ezwebin_site, ezflow_site, ezdemo_site and their_cleancopies). Composer installs ezstarrating and ezgmaplocation, and the installers still activate them. Their tables come from each extension's ownshare/db_schema.dba. The installers of every site package activate expwt, the website sidebar, in place of ezwt. The rebuilt packages are published on the package server with the first expwt release. See Site packages no longer install extension packages.
Tests
expUpgradeActiveExtensionTablesTest(SQLite, fixture extensions undervar/tmp) covers detection, the planned SQL, creation with the columns and keys of the.dba, a half-applied state, a second run that does nothing, no applicable extension, and the installer'sensureExtensionTables()making the same tables. WithEXP_TEST_EXTENSION_ROOTit also checks the Composer copies of ezstarrating and ezgmaplocation.- Fresh installs in a throwaway sandbox (SQLite,
exp:install, the rebuilt packages, no extension package in the repository) of sevenx_site, sevenx_multisite, cjw_multisite, ezwebin_site and ezflow_site completed with 0 setup errors. Each install has expwt, ezstarrating and ezgmaplocation active and ezwt inactive, and has the tablesezstarrating,ezstarrating_dataandezgmaplocation. The Composer copies of the three extensions were left unchanged.
2026-11-03: compiled {foreach} and {for} go on after a template includes itself
Upgrading: clear the template cache (php bin/php/ezcache.php --clear-tag=template) so that compiled templates are
generated again; templates compiled before keep the fault until then.
Fixed
- Fixed: A
{foreach}or{for}whose body includes the same template again (a tree or a menu that renders its children with{include}) stopped after the first item that recursed when templates were compiled (TemplateCompile=enabled); every other item of the outer loop was dropped. Interpreted templates were right. The compiled code keeps a loop's state in variables named after the loop, and a template that includes itself runs the same code in the same scope; the state was set to empty values before it was saved, so the outer loop came back empty. The state is now saved first, and only while a loop of the same code runs, socompact()never names an undefined variable (eZTemplateForeachFunction,eZTemplateForFunction). The variables of asequenceare saved with it: with a sequence the recursive loop ended in a fatal error (next(): Argument #1 ($array) must be of type array, null given). The workaround until then was{section}, which was not affected. - Fixed: A compiled
{for}removes its loop variable after the loop, as an interpreted one does; the code for it was never reached before. Its loop variable keeps the last value of the loop, as interpreted, not the one past it (seen after an{include}of the same template, whose loop shares the template variables). The template variable of asequencestays until the outermost loop of a template that includes itself ends, as interpreted.
Tests
eZTemplateCompiledLoopRecursionTestLR-01 to LR-04:{foreach}and{for}with a recursive{include}, deeper trees,reverse,offset, a loop after the recursive one,sequence,{delimiter},{break}and the loop variables read after the include, interpreted and compiled, without template errors and PHP notices.- Fixed: A compiled
{while}or{do}with asequencewhose body includes the same template again ended in a fatal error (next(): Argument #1 ($array) must be of type array, null given): the include set and then removed the sequence's variables of the loop outside. They are saved and restored around the loop now, as for{foreach}(eZTemplateCompiledLoop::saveSequenceState(),restoreSequenceState()). - Fixed: A compiled
{include}(and{tool_bar}) in a template that includes itself restored the parameters of the wrong level after the third level: the list of variables to restore was kept in one variable that the included copy overwrote, so the outermost level got the values of the one below it. It is a stack now. - Fixed: A compiled
{cache-block}whose body includes the same template again (a tree that caches each node with keys of its own) stopped withCall to a member function storeCache() on nulland was never stored: the inner block set and removed the variable that held the outer block's cache handler. The handler is kept on a stack until the block's text is stored. eZTemplateCompiledLoopRecursionTestLR-05 to LR-07:{while}and{do}with a sequence,{section}with a sequence and a delimiter,{skip}and{continue}, a{for}inside a{foreach}, and include parameters at four levels;eZTemplateCacheBlockRecursionTestCB-01: a cache-block tree rendered and read back from the cache. The tests set a siteaccess name when none is set, so they also pass run on their own.
2026-11-03: the datatypes of the installation, the companion of the class groups
Added
- Added: Setup > Datatypes,
/class/datatype/list, read only, beside the class groups (a Datatypes button on/class/grouplist, an entry in the group "Content model" of the Setup menu). Every datatype thatcontent.ini [DataTypeSettings] AvailableDataTypes[]registers, grouped by provider (the kernel, then each extension), with its name, PHP class and file, the number of classes, class attributes, published objects and their object attributes that use it, and its flags (translatable, searchable, collects information, sortable with its sort key type, relations, from text, from file, packages). The datatypes the database uses but nothing registers any more are listed apart in red with a notice (their content cannot be shown, edited or published); so are listed datatypes whose file is missing. Filters as view parameters (/(filter)/used|unused|orphans,/(group)/<provider>), a search and sortable columns (page/class_datatypes.js, CSP safe), the figures above the list. On alpha: 50 datatypes, 36 in use, 12 extensions providing datatypes, 920 class attributes, 196,224 object attributes. - Added:
/class/datatype/view/<identifier>: one datatype with its provider, class, file and flags, the columns of its value and of its class attribute settings and the tables its code names (read from its code), its view, edit, collect and class templates resolved for the administration and the default siteaccess, the classes and class attributes that use it with their set settings and their objects, the ten last modified nodes of those classes and its help page. - Added:
expDataTypeInventory(kernel/classes/expdatatypeinventory.php). The counts are two grouped queries overezcontentclass_attributeandezcontentobject_attribute(current version of the published objects) on SQLite, MySQL, PostgreSQL and Oracle, aggregation pipelines on MongoDB, kept five minutes in the cache directory (the second query takes 2.5 s on alpha's 3 GB SQLite database); Count again counts at once. - Added: The guide Datatypes (German too), the context help of
class/datatype, the strings in every catalogue.
Tests
expDataTypeInventoryTest(no database): the list built from a registry and counts of its own (groups, states, orphans, files missing, figures, filters), the view parameters, the location and provider of a datatype file, the flags of a datatype object and the columns and tables read from its code.expDataTypeInventorySqliteTest(SQLite in memory): the grouped counts of defined classes only and of the current version of published objects only, per class attribute, an unregistered datatype as an orphan, upper case column names as Oracle returns them.
2026-11-03: compiled {do} with {continue}, transactions, buffers and handlers on error paths
Fixed
- Fixed:
{continue}or{skip}in a compiled{do}loop jumped back to the top of the body without testing the{/do while}condition, so a loop whose last iteration ran{continue}went on (or never ended). The compiled loop tests the condition at the top of every iteration but the first now, as the interpreter does (eZTemplateDoFunction::templateNodeTransformation()). - Fixed: A transaction that an exception left open between
begin()andcommit()stayed open: the counter stayed raised, every latercommit()of the request only lowered it, andeZDB::checkTransactionCounter()committed the half done work at the end; in a persistent worker of Exponential Velocity the next request on the same connection ran inside it. The web kernel now rolls it back when a module throws (before the cleanup handlers write the session), a persistent worker rolls back what the request before left open at the start of each request (expKernelServiceScope::resetRequest()), andeZDB::transaction( callable )runs work in a transaction that is rolled back when the work throws. New:eZDB::rollbackOpenTransaction(). - Fixed: 445
begin()...commit()pairs of the kernel and lib (content publishing, object, node and version store and removal, relations, translations, sections, user login attempts and logout, workflows, roles, states, URL aliases, packages, migrations and the rest) roll the transaction back and throw the exception on when the code between them throws, instead of leaving it open for an outercommit(). Only the exception path changed; pairs whose commit depends on a condition (48, such as the SQLite serial id transaction ofeZPersistentObject::storeObject()) are covered by the request level rollback above. - Fixed: In the 51 namespaced files among them (the console commands and the module views) the rollback caught
Throwableof the file's own namespace, a class that does not exist, so it never ran: it catches\Throwablenow.
Tests
eZTemplateCompiledDoContinueTest:{continue}and{skip}inside an{if}and as a direct child,{break}, a false condition, a sequence and nested{do}loops, interpreted and compiled.eZDBTransactionExceptionTestTX-01 to TX-05:eZDB::transaction()commits and rolls back, a nested one rolls the outer transaction back,eZDB::rollbackOpenTransaction(), two requests of a persistent worker throughexpKernelServiceScope::resetRequest(), and the web kernel's rollback before its cleanup handlers.
2026-11-03: search suggestions and the filtered search page on the sites
Added
- Added: The search field of a site suggests content while the visitor types, as the administration's header search
does:
content/sitesuggest/(text)/<text>(expSearchSuggest::siteSuggest()). Only content objects the user may read (the anonymous user: what is public), of each only its name, address, class name and a small image; never user accounts, content classes, administration pages or settings; on a siteaccess with a PathPrefix only its own tree. Rate-limited per address (429 with Retry-After). An anonymous answer is keptPublicCacheTTLseconds, keyed by text, siteaccess, languages and user, and sentCache-Control: public; a signed-in user's answer isprivate, no-store. Policy:content/read. Settings:site.ini [SiteSearchSuggestSettings], on by default for every siteaccess. - Added:
design:parts/front_search_suggest.tpl, the include a site design puts inside its header search form: the field becomes an ARIA combobox (Up, Down, Enter, Escape, Ctrl+/), drawn bystylesheets/front_search.csswith--exps-*tokens and their dark values.javascript/page/search_suggest.jsserves the administration and the sites (the class prefix and the wiring come from the list's box). - Added:
design:content/front_search_page.tpl, the filtered search page for a site design: a design'scontent/search.tplthat includes it gets the administration's filtered search (view parameters, the form posted and answered with a 303, content type and sort filters, chips, "Did you mean", pages), with the results as the design's line views or its own result template. - Added: The help page "Searching" (English and German): suggestions and the search page on the site, and their settings.
Tests
expSiteSearchSuggestTestSSS-01 to SSS-06: the settings and their bounds, an item carries nothing of the account and no user or excluded class, a short text asks nothing, the view's declaration and cache key, a site's search template is the filtered page, the includes have no inline CSS or script and reuse the administration's strings.
2026-11-03: clean urls on every new installation
Fixed
- Fixed: An installation from the setup wizard, the kickstarter or
exp:installwrites[SiteAccessSettings] ForceVirtualHost=truetosettings/override/site.ini.append.phpwhen the web server rewrites addresses (always on the command line, otherwise when the wizard was reached withoutindex.phpor the login page answers without it). ezwebin_site, ezflow_site, ezdemo_site and their _clean variants had no such setting, so every link of a new site came out as/index.php/<siteaccess>/...; sevenx and cjw_multisite already wrote it themselves. - Fixed: The siteaccess urls a site package installer builds (
eZSiteInstaller::createSiteaccessUrls()without anindex_file) leaveindex.phpout when the installation runs withForceVirtualHost=true.
2026-11-03: all the content of a datatype, a page at a time
Updated
- Updated:
/class/datatype/view/<identifier>lists all the content using the datatype instead of ten nodes: every main node of the classes using it that the user may read (the tree fetch with its read check, an object once), with name, class, section, languages, owner, modified and published. Sortable column heads (aria-sort) for modified (the default, newest first), published, name and class, both ways; 10, 25, 50 or 100 a page with the shared pager and the total; a class filter posted and answered with a 303. Everything is a view parameter (/(sort)/name/(order)/asc/(limit)/50/(class)/5/(offset)/50), never the query string; the count is kept five minutes per classes and read policy (expDataTypeInventory::nodePage()). Guide Datatypes (English, German).
Tests
expDataTypeInventoryTest: the view parameters of the list, their fallbacks, the addresses and sort links and the fetch parameters, without a database;expDataTypeInventoryNodesSqliteTest: paging, every sort both ways, the class filter, main nodes only and an offset past the end on SQLite with the kernel schema and clean data.
2026-11-03: site packages on PostgreSQL, captions without an active git_manager
Updated
- Updated: The PostgreSQL schema reader (
eZPgsqlSchema) leaves out columns of PostgreSQL's own types (tsvector,tsquery) and the indexes over them, instead of stopping with "Unhandled column type: tsvector". The audit log's full-text column (expaudit_event.search_tsv, added by engine-specific SQL) had made every site package install on PostgreSQL fail. - Updated: Captions (
expUpgradeCaption, used by exp:upgrade and migration runs) use git_manager only when it is an active extension. An installed but inactive git_manager has no settings loaded, so it took the installation root as its backup folder and wrote its.htaccess("Require all denied") and an emptyindex.htmlthere, which left a new site answering with an empty page. git_manager itself no longer takes the root as its backup folder either.
2026-11-03: site packages on MongoDB
Updated
- Updated: The MongoDB driver answers
SELECT column AS namewith the column under that name (and sorts by an alias as by its column). The projection used to ask for a field called "column AS name", so the row came back without it: the installer's check reported every class and attribute name as "stored without a language". - Updated: A MongoDB schema reader made from a connection (
expMongoSchemawithout a.dbaschema) lists the database's collections as tables. It answered none, so the installer reported every declared table missing and created every extension's tables again on each pass. - Updated:
subTreeByNodeID()andsubTreeCountByNodeID()apply theAttributeFilteron MongoDB (node and object fields, state, visibility and class attributes by their sort key; every comparison; "and" and "or"). The MongoDB path left it out, so a fetch for one group by name, or the events of a month, returned the whole subtree. A filter that cannot be applied there (class_name, an unknown operator) gives an empty result, never the whole subtree.
2026-11-03: locales for 22 Indigenous languages of the Americas
Added
- Added: Locales for 22 Indigenous languages of the Americas, so each can be a content language: Central Yup'ik
(
esu-US), Inupiaq (ipk-US), Aleut (ale-US), Alutiiq (ems-US), Tlingit (tli-US), Haida (hai-US), Gwich'in (gwi-US) and Tsimshian (tsi-US) of Alaska; Navajo (nav-US), Cherokee (chr-US), Lakota (lkt-US), Ojibwe (oji-US,oji-CA), Cree (cre-CA), Inuktitut (iku-CA) and Mohawk (moh-CA); Nahuatl (nah-MX), Yucatec Maya (yua-MX), K'iche' (quc-GT), Guarani (grn-PY), Aymara (aym-BO) and Mapudungun (arn-CL). Cherokee and Lakota are generated from Unicode CLDR 44; the others are written by hand, with the formats of their country and, where no established names were found, English or Spanish month and day names, as each file's header says. - Added: The help page "Indigenous languages of the Americas" (
help/government/languages/), in English and German: adding the language, translating content, addresses, the interface language, and fonts for Canadian syllabics and the Cherokee syllabary. "Languages in Exponential" lists the new locales and has a section on them.
2026-11-03: expwt in place of ezwt in the requirements
Updated
- Updated: composer.json no longer requires se7enxweb/ezwt, the website toolbar. expwt, the website sidebar that
replaces it, comes with exponential-php-floor 1.0.6, 1.1.8, 1.2.8 and 1.3.8 (each line, PHP 7.4 included); the
floor requirement asks for those versions. The site designs follow: ezwebin 6.0.32, ezflow 6.1.17 and ezdemo
6.0.20, rebuilt as modern, responsive and accessible designs that use expwt's edit actions and page toolbar.
Existing installations move with
./console exp:wt:migrate-from-ezwt --apply(or the upgrade step6.0.15-expwt-enable).
2026-11-04: a shorter Dashboard menu
Added
- Added: menu.ini
CollapseAfter=<n>for a left menu drawn as one list: its first <n> links show, in the order drawn, and the rest follow in a "More (count)" disclosure, closed at first and open when it holds the current page. A native<details>, so it needs no script and works with the keyboard; every copy ofparts/ini_menu.tpl(admin4, admin) reads it. Test:AdminLeftMenuGroupsTest.
Updated
- Updated: the Dashboard tab's left menu shows its first 10 links (
CollapseAfter=10), and Audit trail moves to the end, third to last, after the links extensions append and right before Change password and Edit profile (TrailLinks[]=audit).
2026-11-04: the guide Configuring Exponential 6.0
Added
- Added: The guide "Configuring Exponential 6.0" (
doc/eng-us/CONFIGURING.md, in German too), right after "Installing Exponential 6.0" in the help center: the settings layers and which file wins, the settings page andexp:ini, siteaccesses and their matching, languages, the database, mail, storage and clusters, sessions, caches, HTTPS and the security headers, the Content Security Policy, trusted proxies, two-step sign-in, "Verify it's you", secrets, roles, extensions and their tables, cronjobs, when a change takes effect under PHP-FPM and Velocity, debugging and logs, the help center and the website toolbar, with a troubleshooting table. "Installing" and "Updating" link to it.
2026-11-04: the guide Extending Exponential 6.0
Added
- Added: The guide "Extending Exponential 6.0" (
doc/eng-us/EXTENDING.md, in German too), right after "Updating Exponential 6.0" in the help center: the ways to extend and the parts of an extension, the RAD wizards, activation, autoloads and extension tables, modules, views with view parameters, policies and the left menus, templates and the rules of the administration designs, datatypes, workflow events, cronjob parts, console commands, GraphQL schema extensions, audit branches, secrets, translations, help pages, website toolbar panels, Exponential Layouts, tests and releases, with a troubleshooting table. "Installing" and "Updating" link to it.
2026-11-04: the guide Documenting an extension
Added
- Added: The guide "Documenting an extension" (
doc/eng-us/guides/documenting-an-extension.md, in German too): the convention for an extension's documentation,README.mdandINSTALL.mdon top ofextension/<name>/and every other page as Markdown (.md) inextension/<name>/doc/, in English; how documentation evolved from plain text files to Markdown, why Markdown, that plain-text pages still show, and an old plain-text document converted step by step; how the help center shows the pages, front matter, context help, what is never shown and how to check it.
Updated
- Updated: Pages that mention extension documentation name its place, README and INSTALL on top and the rest in doc/,
and link the guide. The extension development course, its reference and tutorial, and the extension anatomy page no
longer say the help center needs
ExtensionDocDirs[]to show an extension's pages: it shows every active extension on its own, andExtensionDocDirs[]is only for an inactive one.
2026-11-04: the help index reads the help settings as they are on disk
Updated
- Updated:
exp:help indexfirst removes a cached copy ofexphelp.inithat is older than a file it was built from. With the INI file time check off (asconfig.phpsets it), a landing that changedexphelp.iniand built the index before the INI caches were cleared stored the old settings in the index: the new help books "Configuring" and "Extending" went to the end of the help tree instead of theirPageOrder[]place. The deploy, the landing and the background build all run this command, so none of them needs a manual rebuild after a settings change.
2026-11-04: the guide Integrating With Exponential 6.0
Added
- Added: The guide "Integrating With Exponential 6.0" (
doc/eng-us/INTEGRATING.md, in German too), after "Extending Exponential 6.0" in the help center: choosing a way in, the REST content API and OAuth 2.0, personal API keys and tokens, GraphQL, remote services, SOAP, WebDAV and syndication, packages, migrations, imports, exports, feeds and sitemaps, static publishing, GraphQL webhooks and the audit trail's signed webhook, mail and the job queue, LDAP and single sign-on, headless front ends with CORS and the HTTP cache, search, AI services, the console in CI, and security for integrations, with a troubleshooting table. "Installing" links to it; "Uninstalling" moves to order 16.
2026-11-04: the guide Distributions of Exponential
Added
- Added: The guide "Distributions of Exponential" (
doc/eng-us/DISTRIBUTIONS.md, in German too) in the help center, after "Integrating With Exponential 6.0": which distribution to choose, a comparison table, and for Exponential 6, Exponential Platform Legacy, Nexus (with its CJW and starter flavors), DXP, Platform 3.2.9 and Exponential Basic what each is and who it is for, its PHP versions and databases, kernel and administration, what it ships, how to install it, its newest release and the way forward; then the site packages of Exponential 6, the PHP floor packages, moving between distributions, demos and a troubleshooting table. Every fact was read from the distributions' own repositories. "Installing" links to the guide.
2026-11-04: the guide History of Exponential (1999 - 2026)
Added
- Added: The guide "History of Exponential (1999 - 2026)" (
doc/eng-us/HISTORY.md, in German too) in the help center, before "Branch history": a timeline, the family tree of the code, the takeover of the legacy kernel in 2023, every 6.0 release with its date, the rename to Exponential, the 6.0.15 line, the Exponential Foundation and 7x, the distributions and the community; then the history of the code before Exponential, release by release, from eZ Systems and eZ publish 1.x in 1999 through 3.x, 4.x, the Community Project, eZ Publish 5, eZ Platform and Ibexa, how the legacy kernel lived on until 2023, and the licenses, version numbers and names over time, with its sources. The history chronicle and the branch history link to it.
2026-11-04: a tab moved into Setup can keep its own left menu
Added
- Added:
menu.ini [Leftmenu_setup] GroupTabsLeftMenu(pull request #355). Withtab, the pages of a tab that is moved into Setup (GroupTabs[]with the tab left out of[TopAdminMenu] Tabs[], inHiddenTabs[]or inSetupTabs[]) still mark the Setup tab but draw the tab's own left menu, the view'sleft_menuorparts/<part>/menu.tpl, in admin4 and in admin4l's left menu block. The defaultsetupkeeps the Setup menu with the tab's group open. design/admin and exp_adminui never move a tab into Setup and always draw the tab's own menu. The design tests render both templates for each case, and the admin4 design page explains the setting in English and German.
Updated
- Updated: The generated help examples match their sources again (
exp:help:examples --check), among them the page of[Leftmenu_setup].
2026-11-04: the help has its own left menu
Added
- Added:
menu.ini [Leftmenu_help]anddesign:exphelp/menu.tpl(pull request #356). The help views draw the help's own left menu (Help Start, Contents, Search the help, Extensions, Report an error, each for users withexphelp/read) instead of the Setup menu, while the Setup tab stays marked. A page the menu does not name marks the link it belongs to: a page of the viewer, a topic, a manual and the page tree mark Contents, an extension's page marks Extensions, an audience page marks Help Start. The help viewer's own page tree ("All help pages", with the books in their order) stays beside the page as before. - Added: the header's help control is marked on the help's own pages in admin4 (the "?"), design/admin (the Help link) and exp_adminui 1.0.0.62.
2026-11-04: the guide Getting Support
Added
- Added: The guide "Getting Support" (
doc/eng-us/SUPPORT.md, in German too) in the help center, after "Distributions of Exponential": the fastest path to an answer, helping yourself first (the help, the logs, the system card, the upgrade check, the facts a helper needs), turning a problem into one of PHP, Composer, the database, the web server, the operating system or the browser and asking in that layer's own communities, the volunteers of the Exponential community, the wider PHP ecosystem, the Exponential Foundation, and 7x for the fastest result with total product responsibility for the whole stack; security reports stay private. "Installing" and "Configuring" link to it from their troubleshooting; "Uninstalling" moves to order 18.
2026-11-04: the guide Exponential Community
Added
- Added: The guide "Exponential Community" (
doc/eng-us/COMMUNITY.md, in German too) in the help center, after "Security Policy": where to start, who the community is, every public website of the project with what it is and what runs it, the forums and chats, how to report a bug and a vulnerability, contributing code, translations, documentation and extensions, a start page for each role, releases, the roadmap and decisions, recognition and copyright, supporting the project, and Exponential elsewhere on the internet (the social media accounts and the sites by 7x that feature Exponential). CONTRIBUTING.md, README.md and the history book link to it.
2026-11-04: the guide Exponential Competition
Added
- Added: The guide "Exponential Competition" (
doc/eng-us/competition/, in Germandoc/ger-de/wettbewerb/) in the help center: Exponential compared with proprietary DXPs and SaaS, the PHP content management systems and frameworks, and the systems in every other language, for a site without a budget that must keep working for many years. It states the criteria (cost at zero funding, license freedom, exit, metering, hosting, upgrade stability, extensibility, multi-site and languages, security, data ownership), the values of free software, a summary matrix, and for each product its license and business model, what it does well, where it fails that reader and what Exponential does instead, each with its sources. Distributions of Exponential and Exponential Community link to it.
2026-11-04: a front siteaccess switches designs
Added
- Added: A front siteaccess can let every visitor switch between designs with one click
(
site.ini [DesignSettings] SwitchableDesigns[], withSwitchableDesignLabel[],SwitchableDesignChain[]andDesignSwitchCookie). The view parameter/(design)/<name>on any address chooses a design and keeps it in a cookie; the siteaccess's ownSiteDesignstays the default. Off by default and never on an administration siteaccess. A chosen design gets its own cache variant (design bases, compiled templates,{cache-block}, the content view cache); responses sendVary: Cookie, and a page in a chosen design is private.fetch( 'uiswitch', 'front_designs' )gives templates the choices. Documented indoc/eng-us/features/6.0/front-design-switch.md, in German too.
2026-11-04: a site names the classes its search offers
Added
- Added:
site.ini [SiteSearchSuggestSettings] IncludeClasses[]: when it lists classes, only those are suggested by the site's search suggestions and offered in the content type filter of the search page (content/front_search_page.tpl). Empty, as shipped, keeps every class thatExcludeClasses[]does not exclude. For a site whose content tree also holds other sites' classes, such as a multisite package. Documented in "Searching on the site", in German too.
2026-11-04: the REST and GraphQL API of an installation served below a path
Updated
- Updated: An installation served below a path (an Apache Alias such as
/doc) answers its REST and GraphQL API at/doc/api/...: the REST request parser removes the installation's path in front ofApiPrefix, as it already removedindex_rest.phpin non-vhost mode. Before, every API address there answered "Not Found".
2026-11-04: the links in REST answers
Updated
- Updated: The links a REST answer writes (the fields of a node, its children) use https when the request came over
HTTPS, also behind a proxy, and carry the path the installation is served below (an Alias such as
/doc, orindex_rest.phpin non-vhost mode). Before, every link washttp://<host>/api/....
2026-11-05: SQLite work waits for a busy database instead of failing
Updated
- Updated: On SQLite a write outside a transaction (an
INSERTof a link, anUPDATEof a cronjob) queues on the driver's writer lock like a transaction and waits up to[DatabaseSettings] SQLiteBusyTimeoutms in a web request (default 5000) orSQLiteCLIBusyTimeoutms on the command line (default 30000). Before, it lost every race against a loop of short transactions (an import's link pass) and failed with "database is locked", so an external link saved in the online editor could lose its URL id. - Updated: Cronjobs and scripts wait
SQLiteCLITransactionWaitseconds (default 300) for a transaction to start, not the 25 s of a web request;runcronjobs.phpno longer stops with "A database transaction in Exponential failed" behind a long import. A lock SQLite reports at once (WAL recovery, aCOMMITwaiting for readers) is tried again within the wait, and an error says the database was busy, how long the work waited and which setting sets the limit. Upgrade notes.
2026-11-05: generated passwords, keys and file names from the secure random source
Updated
- Updated:
eZUser::createPassword()draws every character from the password character table withrandom_int(). Before, it built the password frommd5()of the time andmt_rand(). Length and table are unchanged; the$seedparameter is accepted and no longer used, so a password can never be reproduced from it. This is the password of the user/forgotpassword mail, of a user account created with a generated password, ofbin/php/resetuserpassword.phpand of the expservicesgeneratePasswordcall. - Updated: The key of a forgot-password link is
bin2hex( random_bytes( 16 ) )(32 characters, as before). Theopenssl_random_pseudo_bytes()call and itsmt_rand()fallback are gone. - Updated:
ezpRestTokenManager::generateToken()returns a token ofezpRestToken::generateToken()(40 hex characters). Before, it returned the same value on every call. The kernel does not call it; an extension may. - Updated: Stored files of the file and media datatypes, files stored by
eZHTTPFile::store()and the temporary file of the Word text extractor are named fromrandom_bytes()instead ofmd5()of the time andmt_rand(). The names keep their form (32 hex characters and the suffix). - Updated: A user account the site installer creates for an
ezuserattribute gets a random login, address and password. Before, its password waspublish. - Added: Dependabot (
.github/dependabot.yml: Composer, npm and GitHub Actions, weekly) and a CodeQL workflow (.github/workflows/codeql.yml) for the JavaScript of the repository and its workflows. CodeQL has no PHP analysis; PHPStan and the tests check the PHP code. - Updated: The MongoDB adapter's class and its limits page say that an empty result of untranslated SQL must be read
as "no" by code that decides access, and why
escapeString()returns the value unchanged. - Added: Tests
tests/tests/kernel/classes/security/expRandomSourcesTest.php(no database).
2026-11-05: a subtree copy re-points every link of its XML text at the copies
Updated
- Updated: When a subtree is copied, the links and embeds in the XML text of the copies that point inside the copied
subtree are changed to point at the copies. The scan that did this skipped every link and embed that came before a
literal (code) block in the same text, and an embed or object that came before a later link. A copy of 2,714
nodes kept 730 node ids in 189 XML texts pointing at the originals. The XML text is now parsed as XML:
node_idandobject_idoflink,embed,embed-inlineanda,idofobject, andhrefvalueseznode://<node id>andezobject://<object id>are re-pointed wherever they are; nothing inside a literal block is changed, ids outside the copied subtree stay, and a text with nothing to change is stored byte for byte as it was. This applies to the content job "copy" and to the direct copies (content/copysubtree,eZContentObjectTreeNodeOperations::copySubtree(),bin/php/ezsubtreecopy.php), which shareexpContentJobCopySubtree::remapXMLText(). Copies made before keep their links to the originals; copy again to get them right. - Added: Tests in
tests/tests/kernel/classes/contentjobs/expContentJobCopyLinksTest.php(no database) with an XML fixture: links before, between and after literal blocks, several literal blocks, embeds,eznode://andezobject://hrefs, ids outside the copy.
2026-11-05: cond gives its empty default without template compilation
Updated
- Updated: The admin4 left menu and the Setup overview keep their groups when templates run without compilation
(
TemplateCompile=disabled): a group withoutGroupLinks_<group>[]starts from an empty list. Before, every link not yet placed went into the first such group ("Audit"). Contributed by Ekkehard Dörre (pull request #357). - Updated:
condtakes its last parameter, the value when no condition holds, as given without template compilation too. The processing interpreter took it only when it was true, socond( ..., array() ),hash(),0,''andfalse()gave no value there, while a compiled template gave the empty value. That missing value contains everything ($list|contains( 'x' )was true), which is how the menu lost its groups; the same form is used in about 45 places of the admin, admin4 and standard designs (the help's search, report, viewer and dashboard, the top menu tabs, maintenance mode, the audit dashboard), all of which now get the empty value in both modes. The template language page describescond's last parameter. - Added: Cases for
condintests/tests/lib/eztemplate/eZTemplateCompiledInterpretedParityTest.php(no database): empty array, empty hash, 0, false, empty string and later clauses, each processed and compiled, with the values written in the template and passed in as variables.
2026-11-05: URL map files for the URL translator and wildcards
The first part of importing and exporting the URL translator (global URL aliases) and the URL wildcards: the files. Importing them into an installation, the admin screens and the console commands follow.
Added
- Added:
expUrlMapRow(kernel/classes/urlmap/), one mapping of an old address to a new one with the columnsold_url,new_url,kind,siteaccess,languageandnote. The kinds are redirect, alias, wildcard-forward and wildcard-direct, or empty for automatic (a*makes a wildcard). Old and new addresses are normalized when they are read: scheme and host split off and in lower case, default ports dropped, slashes and dot segments resolved, a trailing slash dropped, percent-encoded UTF-8 decoded (but%2F,%3F,%23,%25and spaces kept encoded),eznode://123writtennode:123. A row knows what keeps it from being imported, and the line it came from. - Added: Readers and writers for CSV (delimiter found from the content, byte order mark, UTF-16 and Windows-1252
input, quoted fields over several lines, English and German column names), JSON (
exp-url-map/1), Excel .xlsx (read and write), OpenDocument .ods (write), Apache (Redirect,RedirectMatch,RewriteRulewithRandL, host and query conditions) and nginx (rewrite,returninlocation =and regular expression locations,map). Simple regular expressions become wildcards with{1}placeholders; a rule a mapping cannot express is read as a row that is not importable, with the reason. The rule writers write aliases, direct wildcards and node targets, which only Exponential can serve, as#exp:comments that they read back. - Added:
expSpreadsheetReaderandexpSpreadsheetWriter(kernel/classes/spreadsheet/): the table of the first sheet of an .xlsx file, and a minimal valid .xlsx or .ods of one sheet, with the PHP extension zip and no other library. Without the extension, reading and writing spreadsheets stops with a message that names it. - Added:
expUrlMapFormats, which chooses the reader and writer by format name, file extension or content. - Added: The specification The URL map file formats, also in German.
- Added: Tests in
tests/tests/kernel/classes/urlmap/(no database) with fixtures for every format: normalization and validation, CSV and JSON edge cases (empty files, byte order marks, CRLF, Unicode paths, malformed rows with their line), workbooks from LibreOffice and hand-made ones with every kind of cell, the structure of the written .xlsx and .ods, a round trip from every readable format through every writer and back, 10,000 rows through every format, and, where LibreOffice is installed, the written .xlsx and .ods converted to CSV by LibreOffice.
2026-11-05: URL map files: .ods import, temporary redirects, Apache Redirect as a prefix
Follow-ups of the URL map files, as decided after their first part.
Updated
- Updated: A redirect keeps its status. The new kinds
redirect-temporaryandwildcard-forward-temporaryanswer with 302, and the new, optional seventh columnstatusgives another status of the same kind: 308 (permanent), 303 or 307 (temporary). A file without the column is read as before. The Apache and nginx readers no longer turn 302, 303 and 307 into 301 with a note: they read the temporary kinds with their status, and the writers write each row's status (R=307,return 307,rewrite ... redirectfor 302). - Updated: Apache's
Redirect /old /new(andRedirectPermanent,RedirectTemp) is read as Apache answers it, as a prefix: the path itself as a redirect and a wildcard/old/*->/new/{1}for everything below it. - Updated: OpenDocument spreadsheets (.ods, LibreOffice Calc) are read too:
expSpreadsheetReader::readOdsFile()and the readerexpUrlMapOdsReader, with repeated rows and columns, spaces, tabs and line breaks in cells; a ZIP file is recognized as .ods by its mimetype. - Updated: The temporary files of the spreadsheet classes go to the installation's cache directory
(
var/<site>/cache/spreadsheet, elsevar/tmp), not the system's temporary directory, so they work underopen_basedir. - Updated: The URL map file formats, also in German.
- Updated: The tests of
tests/tests/kernel/classes/urlmap/: an .ods saved by LibreOffice, .ods cells of every kind, files that are not .ods, every redirect status through every format and back.
2026-11-05: URL map import and export, redirects checked on the full request
The second part of importing and exporting the URL translator and the URL wildcards: planning, importing, undoing and exporting, and a table of redirects for what a URL alias cannot hold. The administration's screens follow.
Added
- Added:
exp_url_redirectandexpUrlRedirect: redirects checked once per request, on the address asked for, before the URL aliases and wildcards (also under Velocity): an old address with a query (/index.php?id=12, the order of the parameters ignored), a temporary redirect (302, 303, 307) or 308, a target with a query or on another site; for one siteaccess or every one; the targetnode:<id>follows the node. The request's query is not carried over. Read from a cache file beside the wildcard cache. - Added:
expUrlMapPlannerandexpUrlMapPlan: each row of a URL map planned as new, unchanged, changed, duplicate, conflict (a page's own address, a module address, the front page), broken-target, unknown-host or not-importable, with where it goes (global URL alias, wildcard or redirect), the siteaccess from a full address's host (the siteaccess matching, or an old host mapped to one), the language, and a report as JSON or CSV. - Added:
expUrlMapImporter: imports a plan as one batch in one transaction, with the conflict policy skip or overwrite and per-row decisions; records each row it writes before and after inexp_urlmap_batchandexp_urlmap_batch_item, soundo()puts everything back exactly (a row changed since is kept unless forced); clears the wildcard and redirect caches; audit eventscontent.urlalias.importandcontent.urlalias.import_undo. - Added:
expUrlMapExporter: the global URL aliases (optionally the custom aliases of nodes), the wildcards and the redirects as URL map rows for every writer; importing the export finds every row unchanged. - Added: The console commands
exp:url:import,exp:url:export,exp:url:batchesandexp:url:undo, and the upgrade step6.0.15-url-mapwith the three tables in the kernel schemas and the 6.0.15 update files. - Added: A temporary forward wildcard,
eZURLWildcard::TYPE_FORWARD_TEMPORARY(302; the wildcard pages show it with the import screens);error.inidefines 302, 303, 307 and 308, and a moved page answers with its redirect's status. - Added: URL map import and export, the BC note, also in German; the generated pages of the commands and settings.
- Added: Tests: the planner's classification without a database, the commands' contract, and import, resolve, export, overwrite and undo on SQLite, MySQL and PostgreSQL.
- Updated: A redirect of
exp_url_redirectruns the error module as a moved URL alias does, so an old address that is no module of the site redirects instead of answering 404; a URL alias imported for a siteaccess with aPathPrefixis stored below the prefix (unlessPathPrefixExclude[]names its first element), where the siteaccess looks it up.
2026-11-06: URL map screens in the administration
The third part of importing and exporting the URL translator and the URL wildcards: the screens of the administration, in the admin4 and admin designs (admin4l, admin3 and admin2 inherit them).
Added
- Added: Import a URL map (
content/urlimport): a file uploaded or pasted (CSV, JSON, .xlsx, .ods, Apache or nginx rules; the format found or chosen), the siteaccess and language of bare paths, the conflict policy and the old hosts; a preview of every row with its state as a filter (new, changed, unchanged, duplicate, conflict, broken target, unknown host, not importable), a tick per row that can be imported, hosts mapped and the file planned again, a dry run, the import as one batch, and the plan as CSV or JSON. The file and the plan stay on the server for the user who uploaded it, at most a day (expUrlMapImportSession); the forms post only the decisions of the rows shown. - Added: The import history (
content/urlimporthistory): every import with who, when, the file, the counts and the state; one import's entries a page at a time and Undo, which lists the entries changed since and keeps them unless ticked (expUrlMapImporter::changedSince()). - Added: The export (
content/urlexport) as a download in every format, and the list of redirects with a query or a status (content/urlredirects), with removing one or the ticked ones. - Added: The policy function
content/urltranslator_importfor importing and undoing (the export and the redirects stay undercontent/urltranslator); importing and undoing ask "Verify it's you" ([ReauthGroup_system]). - Added: The buttons Import, Export, Import history and Redirects on the URL translator and the wildcards pages, shown by policy; the context help and the guide URL maps: import, export and undo, also in German; the generated pages of the views; the strings in every administration catalogue.
- Updated: The wildcard pages show a temporary forward wildcard as "Temporary redirect (302)" instead of "Undefined",
and the filter Redirecting lists both forward types (
eZURLWildcard::fetchFilteredList()takes a list of types). - Updated: URL map import and export and Verify it's you, also in German.
- Added: Tests: the screens' helpers and the store of an import without a database, the reauthentication rules, and the history's checks on SQLite, MySQL and PostgreSQL.
2026-11-06: URL map documentation
Added
- Added: The feature page Importing and exporting URL maps: the formats, the kinds and statuses, query-string redirects, the preview and its states, undo, export, the administration screens, the policies and the console commands.
- Added: The help-center tutorial Migrating URLs: collect the old addresses from the old sitemap or the web server's 404s, build a mapping file, import it with a preview and a dry run, test the old addresses, undo an import, and export for the web server or a backup.
- Added: URL map examples: small mapping files in CSV, JSON, Apache and
nginx format, one with query-string and temporary redirects and one with wildcards, each checked with
exp:url:import --dry-run. - Updated: The glossary has the terms URL map, query-string redirect and temporary redirect; the URL translator and wildcards pages of the help, the import and export tools table, the specification and the BC note link to the new pages. All of it in German too.
- Updated: The tutorial Migrating URLs, the feature page and the examples use the helpers:
exp:url:from-sitemapandexp:url:from-404(tabs From the old sitemap, From the 404 log) start the map,exp:url:suggest(Suggest targets) fills in the targets,exp:url:test(Test the old addresses after an import) checks them; the 404 log gives paths only, as it hides query values. In German too.
2026-11-06: every Zeta Component is Exponential's own fork, expcomponents
Exponential officially forks the Zeta Components under their Apache License 2.0 as expcomponents and modernizes
them internally. All 21 forks, se7enxweb/expcomponents-<name> (repositories github.com/se7enxweb/expcomponents-<name>),
were released on 2026-11-06 and are on Packagist; they are maintained by 7x and the Exponential Foundation, each one
replaces its zetacomponents/<name> package, ships a NOTICE file with the upstream notices, and keeps the ezc*
class names and APIs.
Upgrading: item 30 of the list above. composer install from the release's lock file (or, in a Composer project,
composer update se7enxweb/exponential --with-all-dependencies) removes the zetacomponents/* packages and installs
the forks; nothing that requires an upstream name breaks
(Upgrade notes: the switch to expcomponents).
Added
- Added: The feature page Exponential Components (expcomponents): what
the libraries are, their history from eZ Components (eZ Systems, New BSD) through Apache Zeta Components and Zeta
Components (Apache 2.0) to expcomponents, why they are forked, what stays the same, every fork with its repository,
release and kernel constraint, Composer's
replace, the license and how it combines with the kernel's GPL version 2 or later, where the repositories and packages are, releases, issues and the intent of the modernization. - Added: The upgrade notes The switch to expcomponents: who is affected, the
composer.jsonbefore and after, the upgrade by console and by hand, projects and extensions that requiredzetacomponents/*themselves, the replace semantics, the mail fork's refusal of unsafe addresses, autoloading, verifying withcomposer show | grep expcomponents, troubleshooting and rollback. - Added: The specification The expcomponents packages: for each library its purpose, the kernel classes, files and features that use it, the replaced package, the repository and the release; the forks the kernel does not require and the upstream packages that are not forked.
- Added: The guide Contributing to expcomponents: clone, test on every supported PHP version, the rules a change keeps, license headers and notices, commit messages, the ChangeLog, pull requests, releases and versions, syncing with upstream and security reports.
- Added: The glossary terms expcomponents, eZ Components and Zeta Components, and FAQ entries for PHP developers, the ecosystem, licensing and bundlers.
- Added: All of it in German too: Exponential Components, Upgrade-Hinweise, Die expcomponents-Pakete, Zu expcomponents beitragen.
Updated
- Updated:
composer.jsonandcomposer.json.distrequire the forks in place of the upstream packages:se7enxweb/expcomponents-authentication~1.4.2, base^1.9.6, cache~1.6.4, configuration~1.4.1, console-tools^1.7.6, database~1.5.5, debug~1.3.1, event-log~1.5.1, feed~1.4.7, image-conversion~1.5.2(with image-analysis 1.2.1), mail~1.10.2, mvc-tools~1.2.5, persistent-object~1.8.3, signal-slot~1.2.1and system-information~1.1.3, next to archive (~1.5.3) and webdav (~1.2.3), which were switched before. The optional forks authentication-database-tiein 1.2.1, graph 1.7.3 and workflow 1.5.2 were released too; the kernel does not require them. Each fork is tested on PHP 7.4 to 8.5. - Updated:
se7enxweb/expcomponents-mail1.10.2 brings upstream's protection against header injection: it refuses an address with whitespace or< > , ; "(it would end the address or add recipients to the header) before writing any header.ezpMailMessageIdTestMI-04 checks the localhost Message-Id with a sender domain that is no host name but a valid address, and MI-05 that such a sender is refused. - Updated:
se7enxweb/expcomponents-image-conversion1.5.2 scales and crops with ImageMagick again after upstream's switch toproc_open()argument lists; the fix is offered upstream as zetacomponents/ImageConversion pull request #9. - Updated:
expComposerKernelRequirementsTestCR-03 and CR-04 check the fork names of the configuration and image conversion components. - Updated: The pages that named the Zeta Components as the kernel's current libraries name the expcomponents: the
installation chapters (introduction, requirements, getting the code, setup wizard, troubleshooting, migrating from
4.x),
INSTALL.md,UPDATE.md, the console upgrade guide, the REST guide, the ezie page, the standards pages (APIs and protocols; PHP and Composer, which now says that Composer's class map, notezcBase::autoload, loads the libraries), the help center's ecosystem, hosting, integrator, PHP developer, trainer and project pages, the pages on licenses, copyright and third-party components, the goals and the support page. Historical statements stay in the past tense. All of it in German too.
2026-11-06: URL map helpers for moving a site
The fourth part of importing and exporting the URL translator and the URL wildcards: console helpers that build a URL map from the old site and check it against the new one. None of them changes the installation.
Added
- Added:
exp:url:from-sitemap(expUrlMapSitemapSource): the addresses of an old site's sitemap, sitemap index (with the sitemaps it names) or crawl list, gzip-compressed or not, as rows with an emptynew_url; the sitemap's host only unless--only-hostor--any-host, every address once,--limit, paths or--full-urls. - Added:
exp:url:suggest(expUrlMapSuggester): for every row without a target, the visible pages of the siteaccess scored from 0 to 100 by their URL alias and name against the old address (decoded, transliterated, without endings such as.html);--fill-abovefillsnew_urlwith the best one (never a tie), the others go into the note or--report. 4,811 old addresses against 4,496 pages take seconds. - Added:
exp:url:from-404(expUrlMap404Source): the addresses of the not-found log (var/log/404.log, its rotated and gzip files) or of web server access logs (--log), the most asked first, with--min-hitsand--since, without assets, bots and methods other than GET and HEAD. - Added:
exp:url:test(expUrlMapTester): every old address requested on--base, several at a time, the redirects followed hop by hop, and each row reported as landed, wrong-status, wrong-target, not-redirected or error, as a table, JSON or a CSV or JSON report; exit 1 when a row did not land. - Added:
expUrlMapHttp, the HTTP client of the helpers: GET over curl_multi with a timeout, a user agent, a size limit and the proxy ofsite.ini, http and https only. - Added: The generated pages of the four commands and the section Helpers for moving a site of the BC note, also in German.
- Added: Tests: the four helpers without a network or a database, and the command contract.
Updated
- Updated: A URL map row's target path resolves the way the siteaccess serves it, below its
PathPrefixfirst (PathPrefixExclude[]honored), so/recipeson a siteaccess withPathPrefix=fit-healthyis no longer a broken target; the export writes an alias stored below the prefix without it and with its siteaccess, so a round trip keeps it in place. - Updated: The help of
--format,--limit,--reportand--output-formatof theexp:urlcommands names the new commands.
2026-11-06: URL map helpers in the administration
Added
- Added: The import screen (
content/urlimport) has the helpers as tabs, view parameter/(source)/sitemap|404|suggest: From the old sitemap (an address of a public host, never one of the server's own network, or an uploaded sitemap or list), From the 404 log and Suggest targets, each with the targets suggested and filled in from a score; the map is downloaded as CSV or opened as an import to preview. In the admin4 and admin designs. - Added: Test after an import: the old addresses of the file (the first 500) are requested on the site of its siteaccess and the rows that did not land are listed; the result stays with the import.
- Added:
expUrlMapHttpoptionpublic_only: an address is fetched only when every address of its host is a public one, and the connection is pinned to the checked address. - Added: The strings in every administration catalogue; tests of the tabs' helpers without a database.
2026-11-06: the documented INI load order
Updated
- Updated: The documented INI load order is corrected, in English and German: after the defaults come the extensions
a siteaccess activates for itself (
ActiveAccessExtensions[]), the extension siteaccess directories, the siteaccess, the active extensions (ActiveExtensions[]) and the global override, aseZINI::overrideDirsByScope()reads them. An active extension therefore wins oversettings/siteaccess/<siteaccess>/; the configuration guide says so and where to put a value of one siteaccess that must beat an extension. Within one layer the extension listed first wins, not the last one as the guide said (Configuring, section 2).
2026-11-10: URL map formats and examples in the administration
Whoever writes a URL map for the first time can now see what each format looks like before uploading one.
Added
- Added: The tab Formats and examples of the URL map import (
/content/urlimport/(source)/examples), also a button on the URL translator and URL wildcards pages: an example file of every format (CSV, CSV with wildcards, CSV with queries and temporary redirects, JSON, Excel, LibreOffice, Apache and nginx rules), each with what it shows, the extensions its format is read from and the rows the import makes of it; the text ones shown to copy (Copy in admin4), Download, and Open in the import, which puts the file into the import's text field with its format. Below: the columns and the kinds. The upload page links it next to the file field. - Added:
expUrlMapExamples: the examples are the files of the documentation (URL map examples), the German ones for a German interface, each read with its format's reader. - Added:
old-site.xlsxandold-site.ods(Germanalte-site.*) to the URL map examples, written from the CSV example by the import's own writers. - Added: The strings in every administration catalogue; the test
expUrlMapExamplesTest.
Updated
- Updated: Migrating URLs, Importing and exporting URL maps and the examples' README name the tab, also in German.
- Updated: admin4's copy chips (
data-a4-copy) name a long value bydata-a4-copy-namein their notice.
2026-11-11: INI cache files of a process working elsewhere
A process that worked in another directory than the installation root (the Velocity warm-up test in a temporary
root) stored a site.ini without settings/override/ in the INI cache under the name every process of the
installation reads. The site, the administration and every script then ran without the global override, the
siteaccess settings and the extensions' settings until the INI cache was cleared.
Updated
- Updated: eZINI reads the base files and keeps its cache below the installation root, but reads the override
directories relative to the working directory. Override directories that resolve to another place than the
installation's own get a cache file name of their own (
eZINI::workingSettingsDirectory()); a directory that resolves to the installation's own, through a symbolic link for example, counts as the same, and the usual cache file names do not change. Test:eZINIWorkingDirectoryTest. - Updated: The Velocity warm-up test switches the INI cache off while the warm-up runs, checks that the warm-up writes nothing into its temporary root, and removes all of that root, also after a failure.
- Updated: Configuring names the case in its troubleshooting table, also in German.
2026-11-11: class ids follow the database
A process that switched its global database (a script, or the migration runner tests with their SQLite database) kept the class and class attribute ids by identifier it had read from the first one. A later class filter of the process then used the ids of that database, or missed a class it never had: in a full test run the hide cronjob hid nothing, and the forum, product and comment services found nothing, in tests that pass on their own.
Updated
- Updated:
eZContentClass::classIDByIdentifier(),classIdentifierByID()andeZContentClassAttribute::classAttributeIDByIdentifier(),classAttributeIdentifierByID()remember the database their ids were read from, and read them again when the global database is another one (eZDB::setInstance()). The file caches were already kept per database. Tests: RS-6 inexpMigrationRunnerSqliteTest. - Updated: The migration runner tests expire the class caches when they switch back to the installation's database
(
runnerEnd()); RS-5 checks that the process keeps no ids of the test's database, without reading the installation's database, so it also runs where there is none. - Updated:
eZContentAccessForAnotherUserTestgives the checks an empty class identifier hash and content language list of its own database handler, not those an earlier test left in memory.