Cloudflare Turnstile optional, Formular-Fehlerseite repariert
Turnstile laesst sich in den Einstellungen einschalten: aus, bis beide Schluessel eingetragen sind; die Sicherheitsrichtlinie oeffnet sich nur auf Seiten mit Formular, und nur fuer challenges.cloudflare.com. Antwortet Cloudflare nicht, gilt die Absendung als bestanden - ein Ausfall dort darf keine Kundenanfrage kosten. Die drei eingebauten Pruefungen ohne Dritte bleiben davon unberuehrt. Dabei gefunden: wer ein Pflichtfeld vergass, bekam die Seite ohne Gestaltung zurueck. Der Formularcode baute sie selbst ein zweites Mal auf und liess Theme, Blockstile und Schriften weg. Jetzt geht auch dieser Weg durch den Seitencontroller, die Meldungen reisen als Anfrageattribut mit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a2dfd0f796
commit
e0d06ac44c
11 files changed
+311
-44
No files matched your search
@@ -6,6 +6,7 @@ use App\Content\FormRepository;
|
||||
use App\Content\MenuRepository;
|
||||
use App\Content\PageRepository;
|
||||
use App\Content\SiteSettings;
|
||||
use App\Content\Turnstile;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -28,6 +29,7 @@ final class FormController extends AbstractController
|
||||
private MenuRepository $menus,
|
||||
private SiteSettings $settings,
|
||||
private MailerInterface $mailer,
|
||||
private Turnstile $turnstile,
|
||||
private string $secret,
|
||||
private string $siteOrigin,
|
||||
) {
|
||||
@@ -48,12 +50,16 @@ final class FormController extends AbstractController
|
||||
// so it learns nothing, but nothing is stored and nothing is sent.
|
||||
$silentlyDropped = '' !== trim($request->request->getString('website'))
|
||||
|| !$this->timingIsPlausible($request)
|
||||
|| $this->forms->rateLimited($senderHash);
|
||||
|| $this->forms->rateLimited($senderHash)
|
||||
// Die vierte, freiwillige Pruefung. Sie antwortet wie die drei anderen still mit
|
||||
// Erfolg: ein Absender, der erfaehrt, woran er gescheitert ist, versucht es gezielt
|
||||
// noch einmal.
|
||||
|| !$this->turnstile->passed($request->request->getString('cf-turnstile-response'), $request->getClientIp());
|
||||
|
||||
$checked = $this->forms->check($form, $request->request->all());
|
||||
if (!$silentlyDropped && [] !== $checked['errors']) {
|
||||
// Re-render the page the form sits on, with the entered values and the messages.
|
||||
return $this->renderPage($page, $form, $checked, new Response(status: 422));
|
||||
return $this->renderPage($request, $page, $form, $checked);
|
||||
}
|
||||
if (!$silentlyDropped) {
|
||||
$submission = $this->forms->store($form['id'], $checked['values'], $checked['consent'], $senderHash);
|
||||
@@ -104,50 +110,38 @@ final class FormController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
private function renderPage(array $page, array $form, array $checked, Response $response): Response
|
||||
/**
|
||||
* Die Seite mit den Fehlermeldungen - gerendert vom Seitencontroller, nicht hier.
|
||||
*
|
||||
* Vorher baute diese Methode die Seite selbst zusammen und liess dabei alles weg, was sie
|
||||
* nicht kannte: Theme, Blockstile, Schriften, das Stylesheet-Buendel. Wer ein Pflichtfeld
|
||||
* vergass, sah eine Seite ohne Gestaltung. Jetzt geht der Weg durch dieselbe Stelle wie jeder
|
||||
* andere Aufruf, und die Formularzustaende werden als Anfrageattribut mitgereicht.
|
||||
*/
|
||||
private function renderPage(Request $request, array $page, array $form, array $checked): Response
|
||||
{
|
||||
$navigation = [];
|
||||
foreach ($this->menus->all() as $slot => $menu) {
|
||||
$navigation[$slot] = $this->menus->resolve($menu['items'], (string) $page['path']);
|
||||
}
|
||||
$issued = time();
|
||||
$states = [];
|
||||
$walk = function (array $blocks) use (&$walk, &$states, $page, $form, $checked, $issued): void {
|
||||
$zustaende = [];
|
||||
$walk = function (array $blocks) use (&$walk, &$zustaende, $form, $checked): void {
|
||||
foreach ($blocks as $block) {
|
||||
if ('form' === ($block['type'] ?? null)) {
|
||||
$slug = $block['props']['form'] ?? '';
|
||||
$mine = $slug === $form['slug'];
|
||||
$states[$block['id']] = [
|
||||
'form' => $mine ? $form : $this->forms->findBySlug($slug),
|
||||
'sent' => false,
|
||||
// Only the form that was submitted shows messages and keeps its input.
|
||||
'errors' => $mine ? $checked['errors'] : [],
|
||||
'values' => $mine ? $checked['values'] : [],
|
||||
'blockId' => $block['id'], 'page' => $page,
|
||||
'issued' => $issued,
|
||||
'issuedSignature' => hash_hmac('sha256', (string) $issued, $this->secret),
|
||||
];
|
||||
// Nur das abgeschickte Formular zeigt Meldungen und behaelt seine Eingaben.
|
||||
if ('form' === ($block['type'] ?? null) && ($block['props']['form'] ?? '') === $form['slug']) {
|
||||
$zustaende[$block['id']] = ['errors' => $checked['errors'], 'values' => $checked['values'], 'sent' => false];
|
||||
}
|
||||
$walk($block['children'] ?? []);
|
||||
}
|
||||
};
|
||||
$walk($page['document']['blocks'] ?? []);
|
||||
|
||||
$response = $this->render('page.html.twig', [
|
||||
'page' => $page,
|
||||
'canonical' => rtrim($this->siteOrigin, '/').$this->settings->publicPath($page['path']),
|
||||
'navigation' => $navigation,
|
||||
'postLists' => [],
|
||||
'postMeta' => null,
|
||||
'needsTabs' => false,
|
||||
'pageStyles' => '',
|
||||
'googleFonts' => null,
|
||||
'forms' => $states,
|
||||
], $response);
|
||||
$response->headers->set('Cache-Control', 'no-store');
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
$response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'none'; style-src 'self'; img-src 'self' https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'");
|
||||
// Die Startseite antwortet unter "/" - mit ihrem eigenen Pfad wuerde sie dorthin umleiten,
|
||||
// und aus der Fehlermeldung wuerde eine Weiterleitung ohne Meldung.
|
||||
$adresse = $page['path'] === $this->settings->homePath() ? '' : (string) $page['path'];
|
||||
$antwort = $this->forward('App\Controller\PageController::page', [
|
||||
'path' => $adresse,
|
||||
'request' => $request->duplicate(null, null, ['kant_form_state' => $zustaende, '_route' => 'page']),
|
||||
]);
|
||||
// Die Eingabe war unvollstaendig - die Seite ist dieselbe, die Antwort ist es nicht.
|
||||
$antwort->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
|
||||
return $response;
|
||||
return $antwort;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user