Redirect with a flash message instead of a 500 page on stale survey actions
Actions from outdated tabs (survey deleted meanwhile, assignment revoked, session expired) hit guard clauses that threw RuntimeException and produced an HTTP 500 error page - seen 11x during the customer pretest on 2026-08-27 (duplicate on a deleted survey, edit link to a deleted survey). All frontend guards now add a clear flash message and redirect to a freshly loaded view: - list actions: distinct messages for deleted survey, revoked assignment, expired session, unknown action - editor: resolveSurvey redirects to the survey overview; editor POST with invalid token redirects back to the editor - reader/preview back button: reload the current question instead of failing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,7 +75,12 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
return $template->getResponse();
|
return $template->getResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
$survey = $this->resolveSurvey($request, $memberId);
|
$survey = $this->resolveSurvey($request, $memberId, $model);
|
||||||
|
|
||||||
|
if ($survey instanceof Response) {
|
||||||
|
return $survey;
|
||||||
|
}
|
||||||
|
|
||||||
$createQuestionMode = $this->isCreateQuestionRequested($request);
|
$createQuestionMode = $this->isCreateQuestionRequested($request);
|
||||||
|
|
||||||
if ($survey instanceof SurveyModel && $request->isMethod('POST') && $request->request->has('_survey_action')) {
|
if ($survey instanceof SurveyModel && $request->isMethod('POST') && $request->request->has('_survey_action')) {
|
||||||
@@ -225,7 +230,12 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
return $template->getResponse();
|
return $template->getResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveSurvey(Request $request, int $memberId): ?SurveyModel
|
/**
|
||||||
|
* Liefert die angeforderte Umfrage, null (kein survey-Parameter, z. B. Anlegen-Modus)
|
||||||
|
* oder einen Redirect zur Übersicht: Aufrufe aus veralteten Tabs oder Lesezeichen
|
||||||
|
* (Umfrage gelöscht, Zuweisung entzogen) dürfen keine 500er-Fehlerseite erzeugen.
|
||||||
|
*/
|
||||||
|
private function resolveSurvey(Request $request, int $memberId, ModuleModel $model): SurveyModel|Response|null
|
||||||
{
|
{
|
||||||
$surveyId = (int) $request->query->get('survey', 0);
|
$surveyId = (int) $request->query->get('survey', 0);
|
||||||
|
|
||||||
@@ -236,11 +246,15 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
$survey = $this->surveyRepository->findById($surveyId);
|
$survey = $this->surveyRepository->findById($surveyId);
|
||||||
|
|
||||||
if (!$survey instanceof SurveyModel) {
|
if (!$survey instanceof SurveyModel) {
|
||||||
throw new \RuntimeException('Die Umfrage wurde nicht gefunden.');
|
$this->addFlash('error', 'Die Umfrage wurde nicht gefunden – sie wurde möglicherweise inzwischen gelöscht.');
|
||||||
|
|
||||||
|
return new RedirectResponse($this->resolveListUrl($model));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->surveyEditorRepository->isEditor((int) $survey->id, $memberId)) {
|
if (!$this->surveyEditorRepository->isEditor((int) $survey->id, $memberId)) {
|
||||||
throw new \RuntimeException('Sie dürfen diese Umfrage nicht bearbeiten.');
|
$this->addFlash('error', 'Sie sind dieser Umfrage nicht (mehr) zugewiesen.');
|
||||||
|
|
||||||
|
return new RedirectResponse($this->resolveListUrl($model));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $survey;
|
return $survey;
|
||||||
@@ -301,7 +315,9 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
$token = (string) $request->request->get('_token');
|
$token = (string) $request->request->get('_token');
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid($action.'-'.$itemId, $token)) {
|
if (!$this->isCsrfTokenValid($action.'-'.$itemId, $token)) {
|
||||||
throw new \RuntimeException('Ungültiger CSRF-Token.');
|
$this->addFlash('error', 'Ihre Sitzung war zwischenzeitlich abgelaufen. Bitte versuchen Sie es erneut.');
|
||||||
|
|
||||||
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -100,18 +100,39 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
|||||||
$action = (string) $request->request->get('_survey_action');
|
$action = (string) $request->request->get('_survey_action');
|
||||||
$token = (string) $request->request->get('_token', '');
|
$token = (string) $request->request->get('_token', '');
|
||||||
|
|
||||||
|
// Aktionen aus veralteten Tabs (Umfrage inzwischen gelöscht, Sitzung
|
||||||
|
// abgelaufen) dürfen keine 500er-Fehlerseite erzeugen: stattdessen
|
||||||
|
// Hinweis als Flash-Meldung und zurück zur frisch geladenen Übersicht.
|
||||||
|
$redirect = new RedirectResponse($request->getBaseUrl().$request->getPathInfo());
|
||||||
|
|
||||||
if ($surveyId <= 0 || '' === $action) {
|
if ($surveyId <= 0 || '' === $action) {
|
||||||
throw new \RuntimeException('Ungültige Umfrage-Aktion.');
|
$this->addFlash('error', 'Die Aktion konnte nicht zugeordnet werden. Bitte versuchen Sie es erneut.');
|
||||||
|
|
||||||
|
return $redirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session-basierter Symfony-Token (wie im Editor): zuverlässig für
|
// Session-basierter Symfony-Token (wie im Editor): zuverlässig für
|
||||||
// eingeloggte Mitglieder. Der Contao-REQUEST_TOKEN ist cookie-gebunden
|
// eingeloggte Mitglieder. Der Contao-REQUEST_TOKEN ist cookie-gebunden
|
||||||
// und im Frontend-Fragment-Flow nicht stabil verfügbar.
|
// und im Frontend-Fragment-Flow nicht stabil verfügbar.
|
||||||
if (!$this->isCsrfTokenValid($action.'-'.$surveyId, $token)) {
|
if (!$this->isCsrfTokenValid($action.'-'.$surveyId, $token)) {
|
||||||
throw new \RuntimeException('Ungültiger CSRF-Token.');
|
$this->addFlash('error', 'Ihre Sitzung war zwischenzeitlich abgelaufen. Bitte versuchen Sie es erneut.');
|
||||||
|
|
||||||
|
return $redirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
$survey = $this->resolveEditableSurvey($surveyId, $memberId);
|
$survey = $this->surveyRepository->findById($surveyId);
|
||||||
|
|
||||||
|
if (!$survey instanceof SurveyModel) {
|
||||||
|
$this->addFlash('error', 'Diese Umfrage existiert nicht mehr – sie wurde möglicherweise inzwischen gelöscht. Die Übersicht wurde aktualisiert.');
|
||||||
|
|
||||||
|
return $redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->surveyEditorRepository->isEditor($surveyId, $memberId)) {
|
||||||
|
$this->addFlash('error', 'Sie sind dieser Umfrage nicht mehr zugewiesen. Die Übersicht wurde aktualisiert.');
|
||||||
|
|
||||||
|
return $redirect;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ('duplicate-survey' === $action) {
|
if ('duplicate-survey' === $action) {
|
||||||
@@ -140,17 +161,6 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
|||||||
return new RedirectResponse($request->getBaseUrl().$request->getPathInfo());
|
return new RedirectResponse($request->getBaseUrl().$request->getPathInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveEditableSurvey(int $surveyId, int $memberId): SurveyModel
|
|
||||||
{
|
|
||||||
$survey = $this->surveyRepository->findById($surveyId);
|
|
||||||
|
|
||||||
if (!$survey instanceof SurveyModel || !$this->surveyEditorRepository->isEditor($surveyId, $memberId)) {
|
|
||||||
throw new \RuntimeException('Sie dürfen diese Umfrage nicht bearbeiten.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $survey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolvePage(int $pageId): ?PageModel
|
private function resolvePage(int $pageId): ?PageModel
|
||||||
{
|
{
|
||||||
if ($pageId <= 0) {
|
if ($pageId <= 0) {
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ final class ShowSurveyController extends AbstractFrontendModuleController
|
|||||||
$submission = $this->surveySubmissionService->resolveActiveSubmission($survey, $request->getSession(), null);
|
$submission = $this->surveySubmissionService->resolveActiveSubmission($survey, $request->getSession(), null);
|
||||||
|
|
||||||
if ($request->isMethod('POST') && 'back' === (string) $request->request->get('_survey_navigation')) {
|
if ($request->isMethod('POST') && 'back' === (string) $request->request->get('_survey_navigation')) {
|
||||||
$this->assertNavigationTokenValid($request, 'survey-back-'.(int) $survey->id);
|
// Ungültiger Token (Sitzung abgelaufen, veralteter Tab): kein 500er,
|
||||||
|
// sondern die aktuelle Frage per Redirect neu laden.
|
||||||
|
if (!$this->isNavigationTokenValid($request, 'survey-back-'.(int) $survey->id)) {
|
||||||
|
return new RedirectResponse($request->getUri());
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->surveySubmissionService->goBack($survey, $submission, $request->getSession())) {
|
if ($this->surveySubmissionService->goBack($survey, $submission, $request->getSession())) {
|
||||||
return new RedirectResponse($request->getUri());
|
return new RedirectResponse($request->getUri());
|
||||||
@@ -170,7 +174,11 @@ final class ShowSurveyController extends AbstractFrontendModuleController
|
|||||||
$question = $this->resolvePreviewQuestion($survey, (int) $request->request->get('preview_question', 0));
|
$question = $this->resolvePreviewQuestion($survey, (int) $request->request->get('preview_question', 0));
|
||||||
|
|
||||||
if ($request->isMethod('POST') && 'back' === (string) $request->request->get('_survey_navigation')) {
|
if ($request->isMethod('POST') && 'back' === (string) $request->request->get('_survey_navigation')) {
|
||||||
$this->assertNavigationTokenValid($request, 'survey-preview-back-'.(int) $survey->id);
|
// Ungültiger Token: Entwurfsansicht ohne Fehlerseite von vorn beginnen
|
||||||
|
// (der Zustand wandert nur durch POST-Felder, ein GET startet neu).
|
||||||
|
if (!$this->isNavigationTokenValid($request, 'survey-preview-back-'.(int) $survey->id)) {
|
||||||
|
return new RedirectResponse($request->getUri());
|
||||||
|
}
|
||||||
|
|
||||||
if ([] !== $history) {
|
if ([] !== $history) {
|
||||||
$question = $this->resolvePreviewQuestion($survey, (int) array_pop($history));
|
$question = $this->resolvePreviewQuestion($survey, (int) array_pop($history));
|
||||||
@@ -285,13 +293,11 @@ final class ShowSurveyController extends AbstractFrontendModuleController
|
|||||||
return implode(',', $history);
|
return implode(',', $history);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertNavigationTokenValid(Request $request, string $tokenId): void
|
private function isNavigationTokenValid(Request $request, string $tokenId): bool
|
||||||
{
|
{
|
||||||
$token = (string) $request->request->get('_navigation_token', '');
|
$token = (string) $request->request->get('_navigation_token', '');
|
||||||
|
|
||||||
if (!$this->isCsrfTokenValid($tokenId, $token)) {
|
return $this->isCsrfTokenValid($tokenId, $token);
|
||||||
throw new \RuntimeException('Ungültiger CSRF-Token.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolvePublicAlias(Request $request): string
|
private function resolvePublicAlias(Request $request): string
|
||||||
|
|||||||
Reference in New Issue
Block a user