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

.
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.
.
refactor(mail,library): move their upload directories onto the disk
MailFileService kept its own UPLOAD_PATH constant and chmod'ed every attachment to 0666 — writable by anything running on the server; the mode now comes from the configuration of the disk. LibraryCoverStorage replaces four places that spelled out the three cover directories, and the size is an enum rather than a directory name repeated in each of them.

UserClean, which deletes what a removed user leaves behind, drops its hand-written recursive directory removal for deleteDirectory() — which the port gains here, along with what the library export needed.
.
refactor(forum): move message attachments onto the storage disk
Ten places built 'forum/attach/' by hand, two of them in the admin module, and the model of an attachment opened it with FileInfo to answer whether it is a picture and how big it is. ForumAttachmentStorage owns the directory and answers both questions; the preview endpoint asks it for a local copy instead of assuming the file is on this server.

The name of an attachment is a column of rows that predate the validation this CMS has now, so it goes through basename() before it becomes a path — the old code interpolated it as it came.
.
refactor(album): move album pictures onto the storage disk
Six places built 'users/album/{id}/' by hand — the upload, both deletes, the presenter, the download and the edit form — and the upload created the directory itself with mkdir(0777), which umask then narrowed. AlbumPhotoStorage owns the directory now; the disk creates what it needs with the permissions of the configuration.

"Use this picture as my profile photo" no longer copies between two hand-built paths: it asks UserImages to take the photo from the two files of the album, and copies nothing when either of them is missing — the old code copied whichever half it found and left the profile with mismatched pictures.

The port gains copy(), which that needed.

The installer seeds its demo photos through the same storage, so a fresh site gets them with the same permissions as an upload.
.
refactor(profile): move user pictures onto the storage disk
The avatar and the profile photo were addressed by hand in six places — two use cases writing them, two deleting them, the Twig runtime building the URL of one and a mutator of the user model building the URLs of the other — each with its own UPLOAD_PATH concatenation. Johncms\Users\UserImages owns the names now, and everything goes through the disk.

The port gains the two operations this needed: storeGenerated(), which hands the image processor a path to write to and puts the result on the disk (the counterpart of withLocalCopy), and lastModified(), which the cache-busting parameter of the avatar is built from.

Also drops the avatar_file variable the profile form passed to a template that never read it.
.
docs(storage): document the storage port and the file registry
Explains what FileStore is for, what happens when only half of a store or a delete succeeds, and when to reach for a disk directly. Also states the rule the port exists for: no module names League\Flysystem.