Files
survey-bundle/src/Controller/FrontendModule/MemberSurveyListController.php
T
admin.juergenandClaude Fable 5 965ce47d48 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>
2026-08-28 10:57:00 +02:00

182 lines
8.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Mummert\SurveyBundle\Controller\FrontendModule;
use Contao\Config;
use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
use Contao\CoreBundle\Twig\FragmentTemplate;
use Contao\FrontendUser;
use Contao\ModuleModel;
use Contao\PageModel;
use Mummert\SurveyBundle\Model\SurveyModel;
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
use Mummert\SurveyBundle\Repository\SurveyRepository;
use Mummert\SurveyBundle\Service\SurveyEditorService;
use Mummert\SurveyBundle\Service\SurveyLoginInfoService;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AsFrontendModule(type: 'member_survey_list', category: 'survey', template: 'member_survey_list')]
#[AsFrontendModule(type: 'member_survey_template_list', category: 'survey', template: 'member_survey_list')]
final class MemberSurveyListController extends AbstractFrontendModuleController
{
public function __construct(
private readonly SurveyRepository $surveyRepository,
private readonly SurveyEditorRepository $surveyEditorRepository,
private readonly SurveyEditorService $surveyEditorService,
private readonly SurveyLoginInfoService $surveyLoginInfoService,
) {
}
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
{
$templatesOnly = 'member_survey_template_list' === (string) ($model->type ?? '');
$user = $this->getUser();
if (!$user instanceof FrontendUser) {
$template->set('loginRequired', true);
$template->set('loginUrl', $this->resolveLoginUrl());
$template->set('sessionMinutes', $this->surveyLoginInfoService->getSessionLifetimeMinutes());
$template->set('surveys', []);
$template->set('createUrl', null);
$template->set('templatesOnly', $templatesOnly);
return $template->getResponse();
}
if ($request->isMethod('POST') && $request->request->has('_survey_action')) {
return $this->handleAction($request, (int) $user->id);
}
$editPage = $templatesOnly ? null : $this->resolvePage((int) ($model->surveyEditPage ?? 0));
$readerPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyReaderPage'));
$resultsPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyResultsPage'));
$items = [];
foreach ($this->surveyRepository->findEditableByMember((int) $user->id, $templatesOnly) as $survey) {
$surveyId = (int) $survey['id'];
$alias = (string) $survey['alias'];
$items[] = [
'id' => $surveyId,
'title' => (string) $survey['title'],
'description' => (string) ($survey['description'] ?? ''),
'isTemplate' => !empty($survey['isTemplate']),
'isActive' => !empty($survey['isActive']),
'isLocked' => !empty($survey['isLocked']),
'published' => !empty($survey['published']),
'questionCount' => (int) ($survey['questionCount'] ?? 0),
'participationCount' => (int) ($survey['participationCount'] ?? 0),
'editUrl' => $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['survey' => $surveyId]) : null,
'publicUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['parameters' => '/'.$alias]) : null,
// Absolute URL für den "Link kopieren"-Button, damit der geteilte
// Link auch außerhalb der Seite funktioniert.
'copyUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['parameters' => '/'.$alias], UrlGeneratorInterface::ABSOLUTE_URL) : null,
'draftUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['parameters' => '/'.$alias, 'preview' => '1']) : null,
'resultsUrl' => $resultsPage instanceof PageModel ? $this->generateContentUrl($resultsPage, ['parameters' => '/'.$alias]) : null,
];
}
if ($templatesOnly && [] === $items) {
return new Response('');
}
$template->set('loginRequired', false);
$template->set('surveys', $items);
$template->set('createUrl', !$templatesOnly && $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['create' => '1']) : null);
$template->set('templatesOnly', $templatesOnly);
return $template->getResponse();
}
private function handleAction(Request $request, int $memberId): Response
{
$surveyId = (int) $request->request->get('item_id', 0);
$action = (string) $request->request->get('_survey_action');
$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) {
$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
// eingeloggte Mitglieder. Der Contao-REQUEST_TOKEN ist cookie-gebunden
// und im Frontend-Fragment-Flow nicht stabil verfügbar.
if (!$this->isCsrfTokenValid($action.'-'.$surveyId, $token)) {
$this->addFlash('error', 'Ihre Sitzung war zwischenzeitlich abgelaufen. Bitte versuchen Sie es erneut.');
return $redirect;
}
$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 {
if ('duplicate-survey' === $action) {
$duplicate = $this->surveyEditorService->duplicateSurvey($survey, $memberId);
$this->addFlash('success', sprintf('Die Umfrage "%s" wurde als Kopie angelegt.', (string) $duplicate->title));
}
if ('publish-survey' === $action) {
$this->surveyEditorService->publishSurvey($survey, $memberId);
$this->addFlash('success', 'Die Umfrage ist jetzt veröffentlicht.');
}
if ('toggle-active' === $action) {
$isActive = $this->surveyEditorService->toggleSurveyActive($survey, $memberId);
$this->addFlash('success', $isActive ? 'Die Umfrage ist jetzt wieder aktiv.' : 'Die Umfrage wurde vorübergehend deaktiviert.');
}
if ('delete-survey' === $action) {
$this->surveyEditorService->deleteSurvey($survey);
$this->addFlash('success', 'Die Umfrage mit allen Fragen, Antworten und Ergebnissen wurde gelöscht.');
}
} catch (\Throwable $exception) {
$this->addFlash('error', $exception->getMessage());
}
return new RedirectResponse($request->getBaseUrl().$request->getPathInfo());
}
private function resolvePage(int $pageId): ?PageModel
{
if ($pageId <= 0) {
return null;
}
$page = $this->getContaoAdapter(PageModel::class)->findById($pageId);
return $page instanceof PageModel ? $page : null;
}
private function resolveLoginUrl(): ?string
{
$page = $this->surveyLoginInfoService->findLoginPage();
return $page instanceof PageModel ? $this->generateContentUrl($page) : null;
}
}