'[a-z0-9-]{1,80}'], methods: ['POST'])] public function submit(Request $request, string $slug): Response { $form = $this->forms->findBySlug($slug); $path = trim($request->request->getString('page'), '/') ?: $this->settings->homePath(); $page = $this->pages->findPublished($path); if (null === $form || null === $page) { throw $this->createNotFoundException('Formular oder Seite nicht gefunden.'); } $senderHash = hash_hmac('sha256', (string) $request->getClientIp(), $this->secret); // Three quiet checks before the real validation. A caught bot is answered like a success // 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) // 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($request, $page, $form, $checked); } if (!$silentlyDropped) { $submission = $this->forms->store($form['id'], $checked['values'], $checked['consent'], $senderHash); if ($this->deliver($form, $checked['values'])) { $this->forms->markDelivered($submission); } } // Post/Redirect/Get so a reload does not send the message twice. return $this->redirect($this->settings->publicPath($path).'?gesendet='.rawurlencode($slug).'#'.rawurlencode($request->request->getString('block')), 303); } /** The signed timestamp makes the age of the form checkable without storing anything. */ private function timingIsPlausible(Request $request): bool { $issued = $request->request->getInt('issued'); if (!hash_equals(hash_hmac('sha256', (string) $issued, $this->secret), $request->request->getString('issuedSignature'))) { return false; } $age = time() - $issued; return $age >= FormRepository::MIN_SECONDS && $age <= FormRepository::MAX_SECONDS; } private function deliver(array $form, array $values): bool { $lines = []; foreach ($form['fields'] as $field) { $lines[] = $field['label'].': '.($values[$field['name']] ?? ''); } // Der Absender ist die eigene Adresse des Formulars, sonst der Empfaenger. Viele Hoster // liefern nur aus, was von einer Adresse ihrer eigenen Domain kommt. $email = (new Email()) ->from(($form['settings']['sender'] ?? '') ?: $form['settings']['recipient']) ->to($form['settings']['recipient']) ->subject($form['settings']['subject']) ->text(implode("\n", $lines)."\n\n-- \nGesendet über ".rtrim($this->siteOrigin, '/')); $replyTo = $form['settings']['replyToField']; if ('' !== $replyTo && '' !== ($values[$replyTo] ?? '')) { $email->replyTo($values[$replyTo]); } try { $this->mailer->send($email); return true; } catch (TransportExceptionInterface) { // The submission is already stored, so a broken mail setup loses no enquiry. // It stays marked as undelivered and is visible in the administration. return false; } } /** * 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 { $zustaende = []; $walk = function (array $blocks) use (&$walk, &$zustaende, $form, $checked): void { foreach ($blocks as $block) { // 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'] ?? []); // 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 $antwort; } }