Upgrade to 1.0.0-rc845

Concerns: pushword/admin, pushword/admin-block-editor, pushword/core, pushword/newsletter, pushword/page-scanner, pushword/snippet, @pushword/js-helper

Newsletter: a forged bounce report could unsubscribe any address

Affects sites that set newsletter.bounce_maildir or newsletter.bounce_imap_dsn and ran pw:newsletter:bounces, on rc832 through rc844. Any message delivered to the bounce mailbox naming a Final-Recipient marked that address Bounced, terminally. A report is now honoured only when it returns a copy of a newsletter this site signed (Bounces). Audit what was dropped, and put back any address you believe alive through a fresh opt-in (POST /api/newsletter/contact), not an UPDATE:

php bin/console dbal:run-sql \
    "SELECT id, email, bounced_at FROM newsletter_contact WHERE bounced_at IS NOT NULL ORDER BY bounced_at"

Bounces for mail sent before the upgrade count unverified and are left alone. If unverified keeps climbing on new sends, your relay returns no copy of the failed message and no bounce is acted on at all — pw:newsletter:bounces --dry-run tells that apart from a quiet mailbox.

Docker: a restored database gained a default-credential super admin

Affects instances booted on a restored database. The entrypoint's "this database already has an account" check only recognised an existing admin@example.tld, so any other admin address let it create a second ROLE_SUPER_ADMIN on the published default credentials. It now asks the database. Audit the accounts of any instance you booted on a restored volume:

docker compose -f compose.prod.yaml exec pushword \
    php bin/console dbal:run-sql 'SELECT id, email, roles FROM user'

Delete an account you do not recognise with pw:user:delete <email> — its API token dies with it — and rotate the tokens of the accounts you keep if you cannot rule out use. A hand-edited docker/docker-entrypoint.sh needs its seeding condition replaced:

if php bin/console dbal:run-sql "SELECT 'PW_HAS_USER' AS marker FROM user LIMIT 1" 2>/dev/null | grep -q PW_HAS_USER; then
	echo '~~ No account created: this database already has one.'
elif php bin/console pw:user:create …

Docker: the entrypoint clears a var/cache built elsewhere

A var/ volume arriving from another machine carried a compiled container with foreign absolute paths, and the boot died on SQLSTATE[HY000] [14] unable to open database file. The entrypoint now empties var/cache first. Nothing to do unless you maintain docker/docker-entrypoint.sh by hand — add this above the doctrine:schema:update line:

if [ -d var/cache ]; then
	find var/cache -mindepth 1 -delete
fi

Newsletter: a campaign translations PATCH merges per field

PATCH /api/newsletter/campaign/{id} replaced the locale entry it named, dropping the fields the request left out; it now merges per field, like customProperties merges per key. To clear a field send it as ""; blanking every field of a locale drops the locale, as does {"de": null} — and locale keys are now normalised, so {"DE": null} removes de instead of silently removing nothing.

Core: a missing template no longer 500s the page

Page or snippet content naming a template that cannot be loaded — most easily pages_list(view: 'foo') — took the whole public page down. It now degrades like any other render error: an invisible marker, a logged warning, a badge for a logged-in editor, and a pw:page-scan finding. If you were relying on 500s to catch a renamed or deleted variant, watch php bin/console pw:page-scan instead.

Page scanner: a directive shown in a code sample no longer silences the page

page-scanner-ignore and page-scanner-check comments are now read outside fenced blocks and backticks only, so a page documenting the syntax stops applying it. Run php bin/console pw:page-scan once and look at those pages first — they may report findings they were silencing by accident. To name a URL containing a comma, escape it: \,.

Failed external URL verdicts were cached for external_url_cache_ttl (24h) like successes, so a fixed link kept being reported all day. Failures now expire on their own TTL, and php bin/console pw:page-scan --recheck drops the whole pool for a run. Nothing to set — set it to external_url_cache_ttl to keep the previous behaviour:

pushword_page_scanner:
  external_url_failure_cache_ttl: 3600   # default; capped by external_url_cache_ttl

Core: a blockquote opening on > [!label] renders as a notice

Markdown gained the DocFX / GitHub alert syntax, rendered through /component/notice.html.twig with a notice notice-<label> wrapper. Content already holding that syntax — typically pasted from a README — changes appearance; escape the bracket (> \[!NOTE]) to keep the literal text. Sites building their stylesheet from a custom entry point rather than the app.css of @pushword/js-helper need the vendor @source line documented in manage assets, or the notice renders unstyled.

Since rc835 every link in a .clickable box stretched over it, and the last one in DOM order won — a card whose description held a link led there from everywhere, its own title included. Only .clickable-link stretches now. Rebuild your assets (yarn build / npm run build); core's component/card.html.twig already marks its title link. Add the class yourself in a card.html.twig you override (link(title, link, {class: 'clickable-link'}, obfuscateLink)) and in any .clickable box of your own holding more than one link.

Core: a horizontal list whose cards all fit stops fading its edges

pages_list(view: 'horizontalScroll') faded both edges and drew position dots even with nothing to scroll; they now go quiet with the arrows. Rebuild your assets (yarn build / npm run build). A site that copied .horizontal-scroll into its own stylesheet wants three lines from utility.css: the two --horizontal-scroll-fade-*: 0px declarations beside the fade animations, and scroll-marker-group: var(--horizontal-scroll-markers) in place of a constant after.

Core: a media uploaded after the pages naming it gets its usage rows

media_usage was written on page writes only, so a media uploaded after the pages naming it — or deleted and re-uploaded under the same name — read as used by nobody, which is the list pw:media:clean-unused --force deletes. Media are tracked on their own now, but existing rows are not repaired by the upgrade:

php bin/console pw:media:usage:rebuild

Until you do, keep reading pw:media:clean-unused as the dry run it defaults to. If you ran it with --force since rc835, compare its output against your backup.

Block editor: a fenced code block holding a blank line is one block again

The editor cut markdown into blocks without seeing code fences, so a fence containing a blank line was split and headings inside it appeared in the outline rail. A fence is now atomic (CommonMark rules). Nothing to run, but a page saved from the old split kept it — check any page whose code samples contain blank lines. Rail edits also stop normalising blank lines they did not touch, which keeps a flat-file site's diffs to the block that changed.

Block editor: an outline-rail click no longer lands on the block that moved up

Rail rows held block numbers the document had already moved past for 300 ms, so a double-click on "delete the section" deleted the next one too. The rail now redraws immediately after an edit and a stale row refuses to act. Nothing to run — expect a click right after an edit to do nothing but refresh the rail.

Admin: the unsaved-changes copy is per editor, and restores only their fields

The localStorage recovery of unsaved page edits was keyed by page alone, restored the whole form, and banked a copy on every block-editor open. It is now keyed by editor, cleared at sign-out, and writes back only the fields that copy changed — warning first (adminPageUnsavedChangesConflict, a new translation key) when the page moved meanwhile.

Copies held before the upgrade are no longer offered; they sit under pw:unsaved:page:<id> until the next sign-out clears them. Three cases need a look:

  • a dashboard overriding configureAssets() without calling the parent publishes no window.pwLogoutPath, and its copies survive the sign-out;
  • a site overriding @pwAdmin/page/edit.html.twig carries the key itself — it becomes 'pw:unsaved:' ~ app.user.id ~ ':page:' ~ entity.instance.id — and pwUnsavedChangesTranslations gains a conflict entry beside message;
  • a rich editor of your own writing into a form field needs the block editor's handshake (data-pw-baseline-pending on the field, removed with a bubbling pw:baseline-ready once it has written), or its normalisation is stored as an edit.