fix: #3621557 Put a real CSRF token in the HTMX reset URL instead of the placeholder
Related to [#3621557].
Problem
Clicking Reset Canvas layout never resets anything. The POST is refused with
'csrf_token' URL query argument is invalid and the watchdog records an access
denied, on Drupal 11.4 with Canvas 1.11.
The hx-post URL carries Crypt::hashBase64($path) — the CSRF placeholder,
a hash of the path — instead of a token. Being a hash of the path it is
byte-identical for every user on every site, so the failure reads as a stale
token rather than a missing one.
Cause
hook_menu_local_tasks_alter() generated the URL with Url::toString() on the
assumption that omitting $collect_bubbleable_metadata makes
RouteProcessorCsrf emit a real token. It does not: the url_generator service
is MetadataBubblingUrlGenerator, which always generates with bubbleable
metadata and bubbles it into the active render context, whatever the caller
asks for. The route processor therefore always takes its placeholder branch.
The hook runs with no render context, so that bubbling goes nowhere and the lazy builder that would replace the placeholder is never registered. The placeholder reaches the browser verbatim.
Proposed resolution
Swap the placeholder the generator produced for the token core would have
rendered into it, using the injected csrf_token service and the route's own
internal path. The session cache context a real token requires was already
being added.
The no-JavaScript fallback (the standalone confirm form) was unaffected and still works.
Verification
Plain Drupal 11.4.6 + Canvas 1.11.0, Canvas Override enabled on Article:
- before:
hx-posttoken equalsCrypt::hashBase64('node/1/canvas/reset/confirm'), POST → 403 - after: real per-session token, POST → 204 with
HX-Redirect, layout cleared, status message shown, new revision recorded - no-JS confirm form: still resets and records a revision
- 18/18 unit tests pass, no new phpcs findings
Checkpoints
- Reproduced the problem
- Fixed the problem
- Verified manually in a browser
- Automated test coverage
- Followed the Drupal coding standards
- Updated documentation where needed
This change was produced with AI assistance (Claude) and reviewed and tested by a human before submission, in line with the Drupal AI policy.