diff --git a/app/actions/sportheimbuchung-senden.php b/app/actions/sportheimbuchung-senden.php new file mode 100644 index 0000000..7a7bd82 --- /dev/null +++ b/app/actions/sportheimbuchung-senden.php @@ -0,0 +1,127 @@ + $ok, 'error' => $error ?: null]); + } else { + header('Location: ' . url('sportheimbuchung') . ($ok ? '?sent=1' : '?error=' . $error) . '#buchung', true, 303); + } + exit; +}; + +$field = static fn (string $key): string => trim((string) ($_POST[$key] ?? '')); + +$route = 'sportheimbuchung'; +$token = $field('ft'); + +// --- Spam-Checks: Bots bekommen ein stilles "OK" (kein Feedback-Kanal) --- +if ($field('company_url') !== '') { + log_spam($route, 'honeypot'); + $respond(true); +} +if (!form_token_valid($token)) { + log_spam($route, 'token'); + $respond(true); +} + +// Rate-Limit pro IP (sichtbarer Hinweis statt still — legitime NAT-Nutzer nicht im Dunkeln lassen). +if (!rate_limit_ok('submit|' . $route . '|' . client_ip(), 5, 600)) { + log_spam($route, 'ratelimit'); + $respond(false, 'ratelimit'); +} + +// --- Validierung --- +$name = $field('name'); +$email = $field('email'); +$phone = $field('phone'); +$eventType = $field('event_type'); +$eventDate = $field('event_date'); +$attendees = $field('attendees'); +$message = $field('message'); + +$validTypes = ['', 'Geburtstagsfeier', 'Party', 'Vereinsveranstaltung', 'Weihnachtsfeier', 'Firmenveranstaltung', 'Sonstiges']; + +$valid = $name !== '' && mb_strlen($name) <= 200 + && filter_var($email, FILTER_VALIDATE_EMAIL) !== false + && mb_strlen($phone) <= 50 + && in_array($eventType, $validTypes, true) + && ($eventDate === '' || preg_match('/^\d{4}-\d{2}-\d{2}$/', $eventDate)) + && ($attendees === '' || (ctype_digit($attendees) && (int) $attendees >= 1 && (int) $attendees <= 500)) + && mb_strlen($message) <= 3000 + && ($_POST['privacy'] ?? '') === '1' + && !preg_match('/[\r\n]/', $name); + +if (!$valid) { + $respond(false, 'validation'); +} + +// --- Inhalts-/Aufkommens-Heuristiken (still abweisen, Bot-typisch) --- +if (preg_match_all('~https?://~i', $message) > 2) { + log_spam($route, 'links'); + $respond(true); +} +// Globaler Tages-Cap als Brevo-Kosten-Backstop — die Flut soll ihn nicht bemerken. +if (!rate_limit_ok('daily', 100, 86400)) { + log_spam($route, 'daily-cap'); + $respond(true); +} +// Token-Mehrfachnutzung begrenzen (Replay-Schutz, großzügig für menschliches Mehrfach-Senden). +if (!rate_limit_ok('token|' . $token, 3, 86400)) { + log_spam($route, 'replay'); + $respond(true); +} + +// --- Versand --- +$smtp = config('smtp'); + +$dateFormatted = $eventDate !== '' ? date('d.m.Y', strtotime($eventDate)) : '–'; + +$body = "Sportheimbuchung-Anfrage über tsv08kulmbach.de\n" + . str_repeat('-', 40) . "\n" + . "Name: {$name}\n" + . "E-Mail: {$email}\n" + . ($phone !== '' ? "Telefon: {$phone}\n" : '') + . ($eventType !== '' ? "Veranstaltungstyp: {$eventType}\n" : '') + . "Gewünschtes Datum: {$dateFormatted}\n" + . ($attendees !== '' ? "Personenzahl: {$attendees}\n" : '') + . str_repeat('-', 40) . "\n\n" + . ($message !== '' ? $message . "\n\n" : '') + . str_repeat('-', 40) . "\n" + . 'Datenschutz zugestimmt: ja (' . date('d.m.Y H:i') . ")\n"; + +try { + $mail = new PHPMailer(true); + $mail->isSMTP(); + $mail->Host = $smtp['host']; + $mail->Port = (int) $smtp['port']; + $mail->SMTPAuth = true; + $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; + $mail->Username = $smtp['username']; + $mail->Password = $smtp['password']; + $mail->CharSet = PHPMailer::CHARSET_UTF8; + + $mail->setFrom($smtp['from'], $smtp['from_name']); + $mail->addAddress($smtp['to']); + $mail->addReplyTo($email, $name); + $mail->Subject = 'Sportheimbuchung: ' . ($eventType !== '' ? $eventType : 'Anfrage') . ', ' . $dateFormatted; + $mail->Body = $body; + + $mail->send(); +} catch (Throwable $e) { + error_log('[' . date('c') . '] Sportheimbuchung-Mailversand fehlgeschlagen: ' . $e->getMessage() . "\n", 3, STORAGE_PATH . '/logs/mail.log'); + $respond(false, 'mail'); +} + +$respond(true); diff --git a/app/components/form-field.php b/app/components/form-field.php index adaa5d1..2d328ed 100644 --- a/app/components/form-field.php +++ b/app/components/form-field.php @@ -35,7 +35,9 @@ $commonAttrs = 'name="' . e($name) . '" id="' . e($id) . '"' . ($required ? ' required' : '') . ' aria-describedby="' . e($describedBy) . '"' . (!empty($field['autocomplete']) ? ' autocomplete="' . e($field['autocomplete']) . '"' : '') - . (!empty($field['maxlength']) ? ' maxlength="' . e($field['maxlength']) . '"' : ''); + . (!empty($field['maxlength']) ? ' maxlength="' . e($field['maxlength']) . '"' : '') + . (isset($field['min']) ? ' min="' . e((string) $field['min']) . '"' : '') + . (isset($field['max']) ? ' max="' . e((string) $field['max']) . '"' : ''); ?>
diff --git a/app/components/sportheimbuchung-form.php b/app/components/sportheimbuchung-form.php new file mode 100644 index 0000000..015adf8 --- /dev/null +++ b/app/components/sportheimbuchung-form.php @@ -0,0 +1,86 @@ + 'Bitte prüfe deine Eingaben – Pflichtfelder fehlen oder die E-Mail-Adresse ist ungültig.', + 'ratelimit' => 'Zu viele Anfragen in kurzer Zeit. Bitte versuche es in ein paar Minuten erneut.', + 'mail' => 'Deine Anfrage konnte gerade nicht gesendet werden. Bitte versuche es später erneut oder schreib uns direkt an ' . $club['email'] . '.', +]; + +$today = date('Y-m-d'); + +$f = [ + 'name' => ['type' => 'text', 'name' => 'name', 'id' => 'buch-name', 'label' => 'Name', 'required' => true, 'autocomplete' => 'name', 'maxlength' => 200], + 'email' => ['type' => 'email', 'name' => 'email', 'id' => 'buch-email', 'label' => 'E-Mail', 'required' => true, 'autocomplete' => 'email', 'maxlength' => 200], + 'phone' => ['type' => 'tel', 'name' => 'phone', 'id' => 'buch-phone', 'label' => 'Telefonnummer', 'autocomplete' => 'tel', 'maxlength' => 50], + 'event_type' => ['type' => 'select', 'name' => 'event_type', 'id' => 'buch-type', 'label' => 'Art der Veranstaltung','options' => $form['event_types']], + 'event_date' => ['type' => 'date', 'name' => 'event_date', 'id' => 'buch-date', 'label' => 'Gewünschtes Datum', 'required' => true, 'min' => $today, 'hint' => 'Frühestmögliches Buchungsdatum'], + 'attendees' => ['type' => 'number', 'name' => 'attendees', 'id' => 'buch-attendees', 'label' => 'Ungefähre Personenzahl','required' => true, 'min' => '1', 'max' => '500'], + 'message' => ['type' => 'textarea', 'name' => 'message', 'id' => 'buch-message', 'label' => 'Weitere Wünsche oder Fragen', 'rows' => 5, 'maxlength' => 3000], + 'privacy' => [ + 'type' => 'checkbox', 'name' => 'privacy', 'id' => 'buch-privacy', 'required' => true, + 'label' => 'Ich stimme den Datenschutzbestimmungen zu.', + 'label_html' => 'Ich stimme den Datenschutzbestimmungen zu.', + ], +]; +?> +
+
+
+

+

+

Oder direkt per Mail an

+
+
+
+ +

Danke für deine Anfrage! Wir melden uns kurzfristig bei dir.

+ +

+ +
+ +

Pflichtfelder sind mit Stern markiert.

+ +
+ $f['name']]); ?> + $f['email']]); ?> +
+ +
+ $f['phone']]); ?> + $f['event_type']]); ?> +
+ +
+ $f['event_date']]); ?> + $f['attendees']]); ?> +
+ + $f['message']]); ?> + $f['privacy']]); ?> + + + + + +

+ +

+
+
+
diff --git a/app/pages/sportheimbuchung.php b/app/pages/sportheimbuchung.php index 3c62cb5..18dfa3c 100644 --- a/app/pages/sportheimbuchung.php +++ b/app/pages/sportheimbuchung.php @@ -3,44 +3,85 @@ declare(strict_types=1); /** - * Sportheimbuchung — Platzhalter/Stub. Inhalt (Verfügbarkeiten, Buchungsablauf, - * Konditionen) folgt; Kontaktweg über data/club.json (Single Source of Truth). + * Sportheimbuchung — Hero → 4 Argumente → Bento (Fotos + Lage + CTA) → Formular → FAQ. + * Inhalte: data/sportheimbuchung.json. Adresse: data/club.json (Single Source of Truth). */ +$page = json_load('sportheimbuchung'); $club = json_load('club'); $meta = [ - 'title' => 'Sportheim mieten & buchen', - 'description' => 'Das Sportheim des TSV 1908 Kulmbach für Feiern und Veranstaltungen mieten – Ansprechpartner und Buchungsablauf.', - 'schema' => page_schema([ - ['name' => 'Startseite', 'slug' => ''], - ['name' => 'Der Verein', 'slug' => 'verein'], - ['name' => 'Sportheimbuchung', 'slug' => 'sportheimbuchung'], - ]), + 'title' => 'Sportheim mieten – TSV 08 Kulmbach', + 'description' => 'Das Sportheim auf der Hans-Rausch-Sportanlage in Kulmbach für Feiern und Veranstaltungen mieten – Küche, Außenbereich und Beamer optional. Jetzt unverbindlich anfragen.', + 'og_image' => 'img/pages/sportheim-2.jpg', + 'schema' => page_schema( + [ + ['name' => 'Startseite', 'slug' => ''], + ['name' => 'Der Verein', 'slug' => 'verein'], + ['name' => 'Sportheim mieten', 'slug' => 'sportheimbuchung'], + ], + $page['faq'] ?? [] + ), ]; + +$addr = $club['address']; + +component('hero', ['hero' => $page['hero']]); ?> -