Fix frontend CSRF, clean reader URLs, and review findings

CSRF / actions:
- List action forms now send both REQUEST_TOKEN (Contao gate) and _token
  (Symfony, validated via isCsrfTokenValid) — mirrors the editor; fixes
  the 400/500 "Ungültiger CSRF-Token" on publish/toggle/duplicate/delete

Public reader / URLs:
- Reader & results read auto_item via Input::get (marks the route param used)
  so clean path URLs /<page>/<alias> no longer 404
- List controller generates path URLs via ['parameters' => '/'.$alias]
- Allow re-participation in the same session after a 60s cooldown

Review fixes:
- Results: merge answer snapshots by questionId+type (no more split cards on
  label edits); seed "Vielleicht" when allowMaybe so screen matches Excel
- Reader: replace generic answer hint with per-option "weiter zu Frage X"
  only when a jump is configured; strip HTML tags from survey description
- PDF: render free-text frequency summary; drop duplicate "Minimum" label
- Drag-sort: bundle SortableJS locally, drop the duplicated inline sort CSS/JS
  from _survey_assets, throttle the backend MutationObserver
- Backend: move hardcoded tl_survey operation labels to TL_LANG (de+en)
- Remove dead code: SurveyEditorVoter, SurveyPermissionService,
  categoryFilter/categorySummary + helpers, orphaned publishSurvey lang key

Reader layout:
- max-width 1000px (other views keep 1400px); back/next buttons fill the
  width as equal, card-sized halves; sort handle icon offset tweak

Docs: update AI_HANDOVER (dual-token CSRF, clean URLs) and README

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jürgen Mummert
2026-06-14 13:00:05 +02:00
parent 1b16675ae9
commit db7094fd1e
29 changed files with 230 additions and 456 deletions
@@ -51,6 +51,7 @@
<div class="survey-list-card__head-actions survey-list-card__head-actions--template">
<form method="post" class="survey-question-action-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('duplicate-survey-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="duplicate-survey">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
@@ -79,12 +80,14 @@
{% endif %}
<form method="post" class="survey-question-action-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('duplicate-survey-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="duplicate-survey">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
</form>
<form method="post" class="survey-question-action-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('delete-survey-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="delete-survey">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button class="survey-button danger" onclick="return confirm('{{ 'survey.list.delete_confirm'|trans|e('js') }}') && confirm('{{ 'survey.list.delete_confirm_second'|trans|e('js') }}');">{{ 'survey.list.delete'|trans }}</button>
@@ -94,6 +97,7 @@
<div class="survey-list-card__head-actions">
<form method="post" class="survey-list-publish-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('publish-survey-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="publish-survey">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button
@@ -111,6 +115,7 @@
<form method="post" class="survey-list-eye-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('toggle-active-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="toggle-active">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button type="submit" class="survey-visibility-dot{{ survey.isActive ? ' is-on' : '' }}" title="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}" aria-label="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}"></button>