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>
182 lines
8.3 KiB
PHP
182 lines
8.3 KiB
PHP
<?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;
|
||
}
|
||
}
|