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

.
fix(users): build the profile website link from a validated address
The field holds an address, not markup, but it was run through the HTML
sanitizer and printed as whatever came back; a link appeared only because the
rich-content policy linkifies bare URLs.

It is now built explicitly: http(s) becomes a link with rel="nofollow noopener",
anything else is printed as escaped text. An allow list of schemes is what keeps
javascript: out of an href, which a markup sanitizer was never the tool for.
.
refactor(consent): sanitize the title and the banner by policy
Both formatters built an HTMLPurifier config of their own with an HTML.Allowed
string; the same allowances now live in HtmlPolicy::Inline and
HtmlPolicy::InlineWithParagraphs, and the formatters only choose between them.

toPlainText of a title now sanitizes before stripping the tags, so the body of
an element that was never allowed cannot reach a breadcrumb as text; it also
collapses runs of whitespace, which a one-line title does not notice.

Adds the first tests of the consent module: which policy each formatter asks
for, and what survives it.
.
refactor(security): move the remaining purifier callers to the sanitizer
The models and the legacy classes that reach for the container by hand now ask
for HtmlSanitizerInterface: ForumMessage, DownloadFile, NewsArticle,
UserMutators, Comments, the news comments controller and ArticleTextRenderer.

News\Application\Utils\Helpers::purifyHtml is gone; NewsArticle sanitizes on
its own and no longer reaches from Domain into the Application layer for it.
.
refactor(security): take the sanitizer interface in the injected consumers
Nine services, use cases and controllers asked the container for a raw
\HTMLPurifier; they now take HtmlSanitizerInterface and speak of policies
instead of the library. Behaviour is unchanged: every call site keeps the
default rich-content policy it had.

The tests that stubbed the vendor class mock the interface instead, and
ItemContentFormatterTest asserts what the formatter owes (null for no content,
sanitized before it becomes Markup) rather than re-testing the sanitizing that
HtmlSanitizerTest already covers.
.
feat(security): introduce a policy-based html sanitizer
Callers state what kind of content they have (HtmlPolicy) and ask
HtmlSanitizerInterface to clean it; which library does the work is now an
implementation detail of a single class.

- HtmlPolicy: RichContent, Inline, InlineWithParagraphs
- HtmlPurifierFactory holds every policy, including the two configs that the
consent module used to build for itself
- the definition of the rich policy is cached in data/cache/htmlpurifier, so it
is no longer recompiled on every request and no longer written into vendor/
- Johncms\Security\HTMLPurifier is deprecated and delegates to the factory
- 85 tests covering xss vectors, preserved editor markup, policy boundaries and
multibyte text
.
ci(node): keep the emnapi peer entries in the lock file
The npm of the runner requires @emnapi/runtime in the lock file while the older npm of the development machine drops it, so npm ci failed. The lock file is regenerated with the npm version of the runner and now satisfies npm 10 and 11 alike.
.
ci(node): run the build on node 24
npm 11 drops the @emnapi/* peer entries of an optional package from the lock file, and npm 10 of node 22 refuses to install without them, so npm ci failed. The workflow now uses the same node as the development environment, with the setup-node action updated and the npm cache turned on.
.
chore: update node packages
.
fix(logs): write php errors to the log of the application
The boot pointed the php error_log at a second file, so failures happening before the error handlers exist ended up in errors-<date>.log instead of johncms-<date>.log. LoggerFactory now tells which file it writes to, and log_errors is on outside DEBUG as well.
.
chore: update composer packages