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
+4 -3
View File
@@ -3,8 +3,6 @@
$GLOBALS['TL_LANG']['tl_survey']['title'] = ['Titel', 'Titel der Umfrage.'];
$GLOBALS['TL_LANG']['tl_survey']['alias'] = ['Öffentlicher Link-Schlüssel', 'Wird automatisch als 20-stelliger Link-Schlüssel erzeugt.'];
$GLOBALS['TL_LANG']['tl_survey']['category'] = ['Kategorien', 'Ordnet die Umfrage einer oder mehreren Kategorien zu.'];
$GLOBALS['TL_LANG']['tl_survey']['categoryFilter'] = ['Kategorien', 'Filtert die Liste nach einer Umfrage-Kategorie.'];
$GLOBALS['TL_LANG']['tl_survey']['categorySummary'] = ['Kategorien', 'Zugeordnete Kategorien der Umfrage.'];
$GLOBALS['TL_LANG']['tl_survey']['isTemplate'] = ['Umfrage ist eine Vorlage', 'Wird den zugewiesenen Nutzern als Vorlage angezeigt.'];
$GLOBALS['TL_LANG']['tl_survey']['templateFilter'] = ['Vorlage', 'Filtert die Liste nach Vorlagenstatus.'];
$GLOBALS['TL_LANG']['tl_survey']['templateSummary'] = ['Vorlage', 'Zeigt an, ob die Umfrage als Vorlage markiert ist.'];
@@ -19,7 +17,6 @@ $GLOBALS['TL_LANG']['tl_survey']['meta_legend'] = 'Verwaltungsdaten';
$GLOBALS['TL_LANG']['tl_survey']['published'] = ['Veröffentlicht', 'Die Umfrage öffentlich freischalten.'];
$GLOBALS['TL_LANG']['tl_survey']['isActive'] = ['Vorübergehend aktiv', 'Schaltet eine bereits veröffentlichte Umfrage öffentlich an oder aus.'];
$GLOBALS['TL_LANG']['tl_survey']['isLocked'] = ['Gesperrt', 'Struktur ist wegen vorhandener Antworten gesperrt.'];
$GLOBALS['TL_LANG']['tl_survey']['publishSurvey'] = ['Umfrage veröffentlichen', 'Veröffentlicht die Umfrage endgültig.'];
$GLOBALS['TL_LANG']['tl_survey']['publishedWarning'] = ['Veröffentlichungshinweis', 'Zeigt einen Warnhinweis bei veröffentlichten Umfragen.'];
$GLOBALS['TL_LANG']['tl_survey']['toggleActive'] = ['Sichtbarkeit umschalten', 'Schaltet die veröffentlichte Umfrage vorübergehend an oder aus.'];
$GLOBALS['TL_LANG']['tl_survey']['assignedMembers'] = ['Zugewiesene Mitglieder', 'Wählen Sie die Mitglieder aus, die diese Umfrage bearbeiten dürfen.'];
@@ -30,6 +27,10 @@ $GLOBALS['TL_LANG']['tl_survey']['duplicateSurvey'] = ['Duplizieren', 'Diese Umf
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyConfirm'] = 'Sind Sie sicher, dass Sie die Umfrage "%s" mit allen Fragen duplizieren wollen?';
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyDone'] = 'Die Umfrage "%s" wurde dupliziert.';
$GLOBALS['TL_LANG']['tl_survey']['showResults'] = ['Ergebnisse anzeigen', 'Die Ergebnisse dieser Umfrage im Frontend anzeigen'];
$GLOBALS['TL_LANG']['tl_survey']['editheader'] = ['Umfrage bearbeiten', 'Umfrage bearbeiten'];
$GLOBALS['TL_LANG']['tl_survey']['edit'] = ['Fragen bearbeiten', 'Fragen der Umfrage bearbeiten'];
$GLOBALS['TL_LANG']['tl_survey']['delete'] = ['Löschen', 'Eintrag wirklich löschen?'];
$GLOBALS['TL_LANG']['tl_survey']['show'] = ['Details', 'Details des Elements ID %s anzeigen'];
$GLOBALS['TL_LANG']['tl_survey']['createdBy'] = ['Angelegt von', 'ID des erstellenden Mitglieds.'];
$GLOBALS['TL_LANG']['tl_survey']['updatedBy'] = ['Aktualisiert von', 'ID des zuletzt bearbeitenden Mitglieds.'];
$GLOBALS['TL_LANG']['tl_survey']['createdAt'] = ['Angelegt am', 'Zeitpunkt der Erstellung.'];
+4 -3
View File
@@ -3,8 +3,6 @@
$GLOBALS['TL_LANG']['tl_survey']['title'] = ['Title', 'Title of the survey.'];
$GLOBALS['TL_LANG']['tl_survey']['alias'] = ['Public link key', 'Automatically generated as a 20-character link key.'];
$GLOBALS['TL_LANG']['tl_survey']['category'] = ['Categories', 'Assigns the survey to one or more categories.'];
$GLOBALS['TL_LANG']['tl_survey']['categoryFilter'] = ['Categories', 'Filters the list by a survey category.'];
$GLOBALS['TL_LANG']['tl_survey']['categorySummary'] = ['Categories', 'Assigned categories of the survey.'];
$GLOBALS['TL_LANG']['tl_survey']['isTemplate'] = ['Survey is a template', 'Shown to assigned users as a template.'];
$GLOBALS['TL_LANG']['tl_survey']['templateFilter'] = ['Template', 'Filters the list by template status.'];
$GLOBALS['TL_LANG']['tl_survey']['templateSummary'] = ['Template', 'Shows whether the survey is marked as a template.'];
@@ -19,7 +17,6 @@ $GLOBALS['TL_LANG']['tl_survey']['meta_legend'] = 'Administrative data';
$GLOBALS['TL_LANG']['tl_survey']['published'] = ['Published', 'Make the survey publicly accessible.'];
$GLOBALS['TL_LANG']['tl_survey']['isActive'] = ['Temporarily active', 'Turns an already published survey on or off publicly.'];
$GLOBALS['TL_LANG']['tl_survey']['isLocked'] = ['Locked', 'Structure is locked because answers already exist.'];
$GLOBALS['TL_LANG']['tl_survey']['publishSurvey'] = ['Publish survey', 'Publishes the survey permanently.'];
$GLOBALS['TL_LANG']['tl_survey']['publishedWarning'] = ['Publication warning', 'Shows a warning hint for already published surveys.'];
$GLOBALS['TL_LANG']['tl_survey']['toggleActive'] = ['Toggle visibility', 'Temporarily switches the published survey on or off.'];
$GLOBALS['TL_LANG']['tl_survey']['assignedMembers'] = ['Assigned members', 'Choose the members who are allowed to edit this survey.'];
@@ -30,6 +27,10 @@ $GLOBALS['TL_LANG']['tl_survey']['duplicateSurvey'] = ['Duplicate', 'Duplicate t
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyConfirm'] = 'Are you sure you want to duplicate the survey "%s" with all questions?';
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyDone'] = 'The survey "%s" has been duplicated.';
$GLOBALS['TL_LANG']['tl_survey']['showResults'] = ['Show results', 'Display the results of this survey in the frontend'];
$GLOBALS['TL_LANG']['tl_survey']['editheader'] = ['Edit survey', 'Edit survey'];
$GLOBALS['TL_LANG']['tl_survey']['edit'] = ['Edit questions', 'Edit the survey questions'];
$GLOBALS['TL_LANG']['tl_survey']['delete'] = ['Delete', 'Really delete the entry?'];
$GLOBALS['TL_LANG']['tl_survey']['show'] = ['Details', 'Show the details of entry ID %s'];
$GLOBALS['TL_LANG']['tl_survey']['createdBy'] = ['Created by', 'ID of the creating member.'];
$GLOBALS['TL_LANG']['tl_survey']['updatedBy'] = ['Updated by', 'ID of the member who last edited it.'];
$GLOBALS['TL_LANG']['tl_survey']['createdAt'] = ['Created on', 'Creation time.'];