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

.
docs(captcha): bump the documentation submodule
.
feat(admin): add the captcha settings page
.
feat(captcha): add the recaptcha v3, smartcaptcha and hcaptcha providers
.
feat(captcha): introduce a provider contract and update mobicms/captcha to 5.x
.
docs(content): bump the documentation submodule
Adds the page describing the content pipeline: how to render a user's text, and how a module adds a step or a media provider.

Corrects a comment left in the news comments controller with it: Twig\Markup implements JsonSerializable, so json_encode() writes it out as a string rather than as an empty object. The cast to string stays — it keeps the type of the field of a JSON payload obvious — but not for the reason the comment gave.
.
fix(container): keep the storages looked up with di() public
A forum message with an attachment answered with a 500: ForumFile is an Eloquent model, so it cannot be constructor-injected and fetches ForumAttachmentStorage by name, while the definition was private. A private service is inlined into the ones that take it when the container is compiled and stops answering to a name of its own — injection keeps working, the runtime lookup throws.

The same had happened to AlbumPhotoStorage (the installer of the album) and LibraryCoverStorage (Utils, Tree). All three are now declared public next to a comment saying why.

RuntimeServiceLookupTest walks every di() call under system/src and modules, resolves the class names through the imports of the file, and fails when the compiled container does not hold that name as public. It checks visibility rather than has(): a ContainerBuilder still reports a private definition after compile, and only the dumper drops it.

The bug predates the content pipeline; it surfaced while testing pages for it.
.
refactor(content): render user content through one extensible pipeline
Every text a user wrote went through the same chain repeated at ten call sites — sanitize, embedMedia, smilies — and each step parsed and serialized the whole text again, on top of the parse of the sanitizer. The list of steps was hard-coded in MediaEmbed, so a module could not add one.

ContentRendererInterface replaces that chain. The text is parsed once and every step works on that tree: render(), renderOrNull() for a source that can have nothing to show, and toPlainText() for previews and titles. ContentContext carries the HTML policy and whether the smilies of the staff render.

Two extension points, tagged by PSRContainerFactory so a module only has to implement the interface: ContentTransformerInterface is a step of the pipeline (priority orders them), EmbedProviderInterface turns the URL of a media site into a player. A provider names a Twig template rather than building markup, so a theme overrides the look of a player by mirroring its path.

Drops imangazaliev/didom and simba77/ckeditor-media-embed: PHP 8.4 ships an HTML5 parser in ext-dom, and HtmlFragment parses a post as the contents of body, which is where a fragment belongs. The wrapper-cutting hacks around libxml go with them.

Fixed along the way: the smilies no longer render inside attributes or inside code and pre, since the step walks text nodes; the YouTube player is sized with the Bootstrap 5 ratio classes instead of the Bootstrap 4 ones the theme has not carried for years; the video id is matched against what YouTube accepts before it reaches the address of an iframe; the preview link wraps the image alone rather than the caption with it; a JSON payload of news comments carries the markup as a string.

New guide: .agents/content-pipeline.md.
.
test: stop two unit tests from depending on the environment
The quality test encoded a flat colour, which compresses to the same size at
any quality, so it passed only by a couple of bytes here and failed on CI. It
now encodes a picture where every pixel differs from its neighbours.

The reserved-character test relied on a key validation symfony/cache does
inside an assert(), which never runs with zend.assertions=-1. It is skipped
there instead.
.
docs(storage): bump the documentation submodule
Brings in the two storage pages, the reworked image examples and the upgrade notes. Also catches up with the cache and image pages that were already pushed to the documentation repository.
.
feat(storage): add the s3 driver
Built on league/flysystem-aws-s3-v3, which stays optional: it pulls the AWS SDK behind it, and a site keeping its files on its own server has no use for either. Configure the driver without the package and the disk refuses to build with the command to install it, the way CachePoolFactory handles a missing redis extension.

Nothing else changes for the modules: withLocalCopy() downloads to a temporary file when the disk is not on this server, which is what the image processor and getID3 need, and the public address comes from the 'url' of the disk — the bucket, or a CDN in front of it.