Sportheim mieten: Buchungsseite mit Anfrageformular
Neue Seite /sportheimbuchung mit Bento-Layout (Fotos, Lage, Ausstattung) und einem Buchungsanfrage-Formular nach Projektkonvention (form-field-Komponente, Status-Region, Honeypot + ft-Token, Whitelist-Validierung, Rate-Limiting, PRG). Versand via Brevo SMTP über app/actions/sportheimbuchung-senden.php, POST-Route in index.php. form-field um min/max-Attribute erweitert (Personenzahl-Feld). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGzc6GhWhmLJt1jC2q1SRZ
This commit is contained in:
127
app/actions/sportheimbuchung-senden.php
Normal file
127
app/actions/sportheimbuchung-senden.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* POST /sportheimbuchung-senden — Sportheim-Buchungsanfrage validieren und via Brevo SMTP versenden.
|
||||
* Antwort: PRG-Redirect zu /sportheimbuchung#buchung (?sent=1 | ?error=…).
|
||||
*/
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
$wantsJson = str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json');
|
||||
|
||||
$respond = static function (bool $ok, string $error = '') use ($wantsJson): never {
|
||||
if ($wantsJson) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['ok' => $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);
|
||||
Reference in New Issue
Block a user