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

simba77
docs: bump the documentation submodule
simba77
feat(mail): send every message as html and text, and address it properly
A message carrying only html reads as bulk mail to a spam filter and is unreadable in a client that shows text. Each message now goes out as both: the text form comes from a .txt.twig template beside the html one - written here for all six emails of the theme - and is derived from the markup by the converter of symfony/mime when a template does not provide one. The text templates reuse the strings of the html ones, so nothing new has to be translated.

Twig now picks its escaping strategy from the file name instead of always escaping as html. Every existing .twig template is html and is escaped exactly as before; a .txt.twig one is not markup, and escaping it turned the ampersand of a confirmation link into & in the text a reader follows.

The queue understands reply_to, reply_to_name, cc and bcc, the last two taking one address or several. The contact form notification sets reply-to to the visitor who wrote it, so answering the notification answers them rather than the mailbox of the site.

Messages can be signed with dkim, configured by the dkim section. A key that cannot be loaded is logged and ignored: the mail of a site must not stop because of it.
simba77
docs: bump the documentation submodule
simba77
fix(mail): retry a failed delivery instead of losing the message
A transport error stamped the message as sent, so it was never tried again and nobody could tell it had not arrived. Delivery is now tracked: a message that could not be sent keeps its place and is tried again after a configurable delay, and once the attempts run out it is recorded as failed with its error - never as sent. A message that cannot be built at all (no recipient, an address no server would accept, a template that does not render) is given up on at once, because every future run would fail the same way, and the rest of the batch still goes out.

A batch is now claimed before it is read, so two workers - the cron of a busy site firing again before the previous run finished - cannot pick the same message, and a claim nobody released within the timeout goes back into circulation. The claim needs no row locks, so it works on the older MySQL versions as well.

EmailSender is a service with its dependencies injected rather than a static method reaching into the container, and reports what the run did. The queue lives behind EmailQueueInterface. The table is defined once in MailSchema, shared by the installer, the new mail:upgrade-schema command and the tests; mail:cleanup removes delivered messages, never the failed ones.

Also fixes MailRenderer dying with a TypeError when no translator was registered yet, which is what a process rendering a message outside the usual bootstrap runs into.
simba77
feat(mail): configure the mailer through a dsn
The mail settings are now compiled into a Symfony Mailer DSN by MailDsnResolver, and a site can write that DSN directly instead: every transport the mailer supports becomes reachable, including the provider bridges and the failover/roundrobin schemes. The structured transport + options form keeps working and is compiled down to the same string.

Fixes of the previous builder: the encryption setting was passed as the Swiftmailer options encryption/auth_mode, which Symfony ignores, so port 465 could never connect - the scheme is now smtps where implicit TLS is meant; a username without a password was dropped entirely; verify_peer, auto_tls, require_tls and local_domain are supported; sendmail without a command falls back to sendmail_path of php.ini; the native and null transports were added.

The transport also receives the logger and the http client, the latter being what the API transports of the provider bridges send through.
simba77
docs: bump the documentation submodule
Points at docs(security): describe the html sanitizer.
simba77
feat(security): let a module declare its own html policy
A policy was an enum case, so the only way for a module to have one was to patch
the core and lose the patch on the next update. A module now registers a service
implementing HtmlPolicyProviderInterface — PSRContainerFactory tags it, the same
way it does for voters and permission providers — and asks for the policy by
name:

$this->sanitizer->sanitize($text, 'my-module.signature');

HtmlPolicyDefinition describes what the content may carry without naming a
library: elements with their attributes, allowed classes, linkify, link schemes,
frame targets. The shape maps onto symfony/html-sanitizer as directly as onto
HTMLPurifier, so it does not tie the abstraction to the current library.

It is an allow list and cannot be widened into something unsafe: elements that
carry behaviour, attributes starting with 'on' and executable schemes are
refused where they are declared, with an exception that names the mistake.
Asking for an undeclared policy throws instead of falling back, and the built-in
policies stay reachable only through the enum, so a module cannot claim the name
the whole site is cleaned by.
simba77
docs(agents): describe the html sanitizer and its policies
escaping.md gains a section on sanitizing rich content: which interface to take,
what each policy is for, that toPlainText sanitizes before stripping, and that a
URL is validated by scheme rather than run through a markup sanitizer.

The security checklist checks the policy fits the content and flags a caller
that builds a sanitizer of its own.
simba77
refactor(security): drop the raw purifier from the container
Nothing asks for \HTMLPurifier any more, so the service, its alias and the
Johncms\Security\HTMLPurifier factory are gone. The library is now reachable
only through HtmlPurifierFactory, behind HtmlSanitizerInterface.
simba77
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.