История коммитов

.
chore(docker): give composer a writable cache directory
Composer runs as www-data, whose home directory is owned by root, so every run
in the container warned that it could not create its cache and proceeded
without one.

COMPOSER_HOME now points at an explicit path that both image stages create and
chown after the user id is adjusted, and compose mounts a named volume over it
so the cache survives container recreation.
.
chore: raise the minimum php version to 8.4
The docker image already moved to 8.4; this brings the rest of the project in
line so the declared requirement, the CI matrix and the runtime checks agree.

- composer.json requires ^8.4 (lock file refreshed), the test workflow builds on
8.4 and the readme states it
- SystemChecker::MIN_PHP_VERSION was still 7.3 and the second installer step
duplicated the bound as literals; the checker is now the single source of
truth and the step reads from it
- phpstan baseline regenerated: the platform version appears in three ignored
patterns, the error count is unchanged
.
chore(docker): rename the compose project and dev domain to johncms
The stack ran under the johncms9 project name, which pinned the container names
and the local domain to the 9.x line.

- .env.example now ships johncms / johncms.loc
- the agent guides target the johncms.php-fpm container
- verify.sh reads COMPOSE_PROJECT_NAME from .env instead of hard-coding the
container name, and matches it exactly so a similarly named project on the
same host cannot be picked up by mistake
.
i18n(admin): translate the mail settings screen into russian
The 28 strings of the new screen, appended to ru.po without touching what was there, and the dictionary regenerated from it. The other languages come from Crowdin.
.
chore(i18n): regenerate the message templates
The templates had drifted from the sources: apart from the strings of the new mail settings screen, which land in the admin domain, every change is a line number.
.
fix(security): keep a policy from widening the link schemes of another
The library keeps its scheme validators in a registry shared by the whole process and, with the override it enables by default, hands out one that is already there without checking it against the policy being applied. One policy allowing http therefore let every policy built after it through as well, however narrow its own list of schemes - so a policy declaring https only accepted http, mailto and ftp as soon as anything else had sanitized a link first.

Not an XSS hole: javascript: and data: are refused by other means. What leaked are the ordinary schemes a policy deliberately left out.

This is what the flaky CustomHtmlPolicyTest was reporting: the suite runs defects first, so a failure moved the test to the front of the queue, where nothing had built a purifier yet and it passed again. With --order-by=default it failed every time. The regression test now builds the permissive policy first, the way the defect needs.
.
docs: bump the documentation submodule
.
feat(admin): edit the mail settings from the panel
The mail of a site could only be configured by editing a file, and whether the settings worked could only be found out by waiting for the queue and reading a log. The panel gains a screen for the connection string, the transport, the smtp parameters and the staging redirection, behind the permission that guards the settings of the site.

The settings are checked against the dsn resolver before they are written: a configuration the mailer cannot build a transport from would take the mail of the site down until somebody edited the file by hand. Signing and the queue stay in the file - they are set once and are not what this screen is opened for.

The test message is sent immediately rather than queued, so the answer of the mail server, including the text of its error, is shown on the same screen where the settings were entered.
.
feat(mail): send every message to one mailbox on a staging copy
A copy of a live site runs with the database of the live one, so the first test of the registration writes to a real visitor. Setting redirect_to sends every message to the given mailbox instead, keeping the real recipients in the X-Original-To header.

Both the headers and the smtp envelope are redirected: changing only the headers would still deliver the message to the address it was addressed to. The listener is attached only when the setting is filled in, so a live site dispatches nothing.
.
docs: bump the documentation submodule