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']) . '"' : ''); ?>
Der Verein
-Unser Sportheim auf der = e($club['address']['venue']) ?> lässt sich für Feiern und Veranstaltungen mieten.
-Die Online-Buchung mit Belegungskalender und Konditionen befindet sich gerade in Vorbereitung. Bis dahin nimm bitte direkt Kontakt mit uns auf – wir klären Termin, Ausstattung und Preise unkompliziert mit dir.
-
- = e($club['name']) ?>
- E-Mail: = e($club['email']) ?>
-
= e($item['text']) ?>
+
+
+ Lage & Adresse
+
+ = e($addr['venue']) ?>
+ = e($addr['street']) ?>
+ = e($addr['zip']) ?> = e($addr['city']) ?>
+
Jetzt anfragen
+ Anfrage senden +