true, 'samesite' => 'Strict', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'use_strict_mode' => true, ]); session_start(); if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 1048576) { http_response_code(413); exit('Anfrage zu groß.'); } header('Cache-Control: no-store'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header('Referrer-Policy: no-referrer'); header('Permissions-Policy: camera=(), microphone=(), geolocation=()'); header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"); require_once __DIR__ . '/inc/db.inc.php'; require_once __DIR__ . '/inc/mail.inc.php'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); header('Allow: POST'); exit('Methode nicht erlaubt.'); } $submittedCsrf = $_POST['csrf_token'] ?? ''; $storedCsrf = $_SESSION['registration_csrf'] ?? ''; if (!is_string($submittedCsrf) || $storedCsrf === '' || !hash_equals($storedCsrf, $submittedCsrf)) { http_response_code(403); exit('Ungültige Anfrage.'); } unset($_SESSION['registration_csrf']); function postString(string $key): string { $value = $_POST[$key] ?? ''; return is_string($value) ? trim($value) : ''; } function postStringList(string $key): array { $value = $_POST[$key] ?? []; $values = is_array($value) ? $value : [$value]; return array_values(array_filter( array_map( static fn ($item): string => is_string($item) ? trim($item) : '', $values ), static fn (string $item): bool => $item !== '' )); } function postMealSelectionsByDay(): array { $value = $_POST['meals'] ?? []; if (!is_array($value)) { return []; } $result = []; foreach ($value as $day => $meals) { if (!is_array($meals)) { continue; } $result[(string) $day] = array_values(array_filter( array_map(static fn ($item): string => is_string($item) ? trim($item) : '', $meals), static fn (string $item): bool => $item !== '' )); } return $result; } function formatMealSummary(array $days, array $mealsByDay): string { $dayLabels = ['1' => 'Fr.', '2' => 'Sa.', '3' => 'So.']; $mealLabels = [ 'breakfast' => 'Frühstück', 'lunch' => 'Mittagessen', 'coffee' => 'Kaffee', 'dinner' => 'Abendessen', 'drinks' => 'Getränkepauschale', ]; $parts = []; foreach ($days as $day) { $labels = array_map(static fn (string $meal): string => $mealLabels[$meal] ?? $meal, $mealsByDay[$day] ?? []); $labels[] = $mealLabels['drinks']; $labels[] = $mealLabels['rehearsal_room']; $parts[] = ($dayLabels[$day] ?? $day) . ': ' . implode(', ', $labels); } return implode('; ', $parts) ?: '–'; } function escapeHtml(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function smtpResponse($socket): string { $response = ''; do { $line = fgets($socket); if ($line === false) { throw new RuntimeException('Keine gültige SMTP-Antwort erhalten.'); } $response .= $line; } while (isset($line[3]) && $line[3] === '-'); return $response; } function smtpCommand($socket, string $command, array $expectedCodes): void { fwrite($socket, $command . "\r\n"); $response = smtpResponse($socket); $code = (int) substr($response, 0, 3); if (!in_array($code, $expectedCodes, true)) { throw new RuntimeException('SMTP-Befehl wurde abgelehnt.'); } } function encodedHeader(string $value): string { return '=?UTF-8?B?' . base64_encode($value) . '?='; } function calculateBookingTotal(PDO $pdo, bool $under27, string $guestType, ?string $roomType, array $mealsByDay): float { $ageSuffix = $under27 ? 'under_27' : 'over_27'; $keys = []; if ($guestType === 'regular_guest' && $roomType !== null) { $keys[] = $roomType . '_' . $ageSuffix; } if ($guestType === 'day_guest') { foreach ($mealsByDay as $meals) { foreach (array_unique(array_merge($meals, ['drinks'])) as $meal) { if (in_array($meal, ['breakfast', 'lunch', 'coffee', 'dinner'], true)) { $keys[] = $meal . '_' . $ageSuffix; } elseif ($meal === 'drinks') { $keys[] = 'drinks'; } } $keys[] = 'rehearsal_room'; } } if ($keys === []) { return 0.0; } $uniqueKeys = array_values(array_unique($keys)); $placeholders = implode(',', array_fill(0, count($uniqueKeys), '?')); $statement = $pdo->prepare("SELECT setting_key, price FROM pricing_settings WHERE setting_key IN ($placeholders)"); $statement->execute($uniqueKeys); $prices = []; foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) { $prices[(string) $row['setting_key']] = (float) $row['price']; } return round(array_sum(array_map( static fn (string $key): float => $prices[$key] ?? 0.0, $keys )), 2); } function sendRegistrationEmail( array $mailConfig, string $recipient, string $name, string $participantPassword, string $guestType, bool $under27, ?string $roomType, array $days, array $mealsByDay, string $allergies, string $roommates, string $otherNotes ): void { $roomLabels = [ 'single' => 'Einzelzimmer', 'double' => 'Doppelzimmer', 'multi' => 'Mehrbettzimmer', ]; $dayLabels = ['1' => 'Fr.', '2' => 'Sa.', '3' => 'So.']; $mealLabels = [ 'breakfast' => 'Frühstück', 'lunch' => 'Mittagessen', 'coffee' => 'Kaffee', 'drinks' => 'Getränkepauschale', 'dinner' => 'Abendessen', 'rehearsal_room' => 'Proberaumpauschale', ]; $guestLabel = $guestType === 'day_guest' ? 'Tagesgast' : 'Dauergast'; $ageLabel = $under27 ? 'Unter 27' : 'Ab 27'; $roomLabel = $roomLabels[$roomType ?? ''] ?? '–'; $dayText = implode(', ', array_map(static fn (string $day): string => $dayLabels[$day] ?? $day, $days)) ?: '–'; $mealText = formatMealSummary($days, $mealsByDay); $roommates = $roommates !== '' ? $roommates : '–'; $allergies = $allergies !== '' ? $allergies : '–'; $otherNotes = $otherNotes !== '' ? $otherNotes : '–'; $textBody = "Hallo {$name},\n\n" . "deine Registrierung für das Chorwochenende wurde erfolgreich gespeichert.\n\n" . "Zugang zur Änderung oder Löschung:\n" . "E-Mail-Adresse: {$recipient}\n" . "Passwort: {$participantPassword}\n\n" . "Zusammenfassung:\n" . "Altersgruppe: {$ageLabel}\n" . "Gasttyp: {$guestLabel}\n" . "Zimmerart: {$roomLabel}\n" . "Anwesenheitstage: {$dayText}\n" . "Mahlzeiten: {$mealText}\n" . "Zimmerpartner-Wunsch: {$roommates}\n" . "Allergien / Essensvorlieben: {$allergies}\n" . "Sonstiges: {$otherNotes}\n\n" . "Viele Grüße\n{$mailConfig['from_name']}"; $html = static fn (string $value): string => escapeHtml($value); $htmlBody = '' . '

Hallo ' . $html($name) . ',

' . '

deine Registrierung für das Chorwochenende wurde erfolgreich gespeichert.

' . '

Deine Zugangsdaten

E-Mail-Adresse: ' . $html($recipient) . '
Passwort: ' . $html($participantPassword) . '

' . '

Zusammenfassung

' . '

Viele Grüße
' . $html($mailConfig['from_name']) . '

'; $boundary = '=_cat_' . bin2hex(random_bytes(12)); $message = 'From: ' . encodedHeader($mailConfig['from_name']) . ' <' . $mailConfig['from_address'] . ">\r\n" . 'To: ' . $recipient . "\r\n" . 'Subject: ' . encodedHeader('Deine Registrierung für das Chorwochenende') . "\r\n" . "MIME-Version: 1.0\r\n" . 'Content-Type: multipart/alternative; boundary="' . $boundary . "\"\r\n\r\n" . '--' . $boundary . "\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n" . $textBody . "\r\n\r\n" . '--' . $boundary . "\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n" . $htmlBody . "\r\n\r\n" . '--' . $boundary . "--\r\n"; $context = stream_context_create([ 'ssl' => [ 'peer_name' => $mailConfig['host'], 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $socket = stream_socket_client( 'tcp://' . $mailConfig['host'] . ':' . $mailConfig['port'], $errorNumber, $errorMessage, 20, STREAM_CLIENT_CONNECT, $context ); if ($socket === false) { throw new RuntimeException('SMTP-Server nicht erreichbar.'); } stream_set_timeout($socket, 20); try { $response = smtpResponse($socket); if ((int) substr($response, 0, 3) !== 220) { throw new RuntimeException('SMTP-Verbindung wurde abgelehnt.'); } smtpCommand($socket, 'EHLO localhost', [250]); smtpCommand($socket, 'STARTTLS', [220]); if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { throw new RuntimeException('STARTTLS konnte nicht aktiviert werden.'); } smtpCommand($socket, 'EHLO localhost', [250]); smtpCommand($socket, 'AUTH LOGIN', [334]); smtpCommand($socket, base64_encode($mailConfig['username']), [334]); smtpCommand($socket, base64_encode($mailConfig['password']), [235]); smtpCommand($socket, 'MAIL FROM:<' . $mailConfig['from_address'] . '>', [250]); smtpCommand($socket, 'RCPT TO:<' . $recipient . '>', [250, 251]); smtpCommand($socket, 'DATA', [354]); $message = preg_replace('/^\./m', '..', $message); fwrite($socket, $message . "\r\n.\r\n"); $response = smtpResponse($socket); if ((int) substr($response, 0, 3) !== 250) { throw new RuntimeException('E-Mail konnte nicht angenommen werden.'); } fwrite($socket, "QUIT\r\n"); } finally { fclose($socket); } } function sendAdminNotificationEmail( array $mailConfig, string $recipient, string $name, string $participantEmail, string $guestLabel, string $ageLabel, string $roomLabel, string $dayText, string $mealText, string $roommates, string $allergies, string $otherNotes ): void { $textBody = "Neue Registrierung für das Chorwochenende\n\n" . "Name: {$name}\n" . "E-Mail: {$participantEmail}\n" . "Altersgruppe: {$ageLabel}\n" . "Gasttyp: {$guestLabel}\n" . "Zimmerart: {$roomLabel}\n" . "Anwesenheitstage: {$dayText}\n" . "Mahlzeiten: {$mealText}\n" . "Zimmerpartner-Wunsch: {$roommates}\n" . "Allergien / Essensvorlieben: {$allergies}\n" . "Sonstiges: {$otherNotes}\n"; $subject = encodedHeader('Neue Registrierung – Chorwochenende'); $message = 'From: ' . encodedHeader($mailConfig['from_name']) . ' <' . $mailConfig['from_address'] . ">\r\n" . 'To: ' . $recipient . "\r\n" . 'Subject: ' . $subject . "\r\n" . "MIME-Version: 1.0\r\n" . "Content-Type: text/plain; charset=UTF-8\r\n" . "Content-Transfer-Encoding: 8bit\r\n\r\n" . $textBody; $context = stream_context_create([ 'ssl' => [ 'peer_name' => $mailConfig['host'], 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $socket = stream_socket_client( 'tcp://' . $mailConfig['host'] . ':' . $mailConfig['port'], $errorNumber, $errorMessage, 20, STREAM_CLIENT_CONNECT, $context ); if ($socket === false) { throw new RuntimeException('SMTP-Server nicht erreichbar.'); } stream_set_timeout($socket, 20); try { $response = smtpResponse($socket); if ((int) substr($response, 0, 3) !== 220) { throw new RuntimeException('SMTP-Verbindung wurde abgelehnt.'); } smtpCommand($socket, 'EHLO localhost', [250]); smtpCommand($socket, 'STARTTLS', [220]); if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { throw new RuntimeException('STARTTLS konnte nicht aktiviert werden.'); } smtpCommand($socket, 'EHLO localhost', [250]); smtpCommand($socket, 'AUTH LOGIN', [334]); smtpCommand($socket, base64_encode($mailConfig['username']), [334]); smtpCommand($socket, base64_encode($mailConfig['password']), [235]); smtpCommand($socket, 'MAIL FROM:<' . $mailConfig['from_address'] . '>', [250]); smtpCommand($socket, 'RCPT TO:<' . $recipient . '>', [250, 251]); smtpCommand($socket, 'DATA', [354]); fwrite($socket, preg_replace('/^\./m', '..', $message) . "\r\n.\r\n"); $response = smtpResponse($socket); if ((int) substr($response, 0, 3) !== 250) { throw new RuntimeException('E-Mail konnte nicht angenommen werden.'); } fwrite($socket, "QUIT\r\n"); } finally { fclose($socket); } } $name = postString('name'); $email = postString('email'); $under27 = isset($_POST['under_27']) && $_POST['under_27'] === '1'; $guestType = postString('guest_type'); $roomType = postString('room_type'); $roommates = postString('roommates'); $earlyDeparture = postString('early_departure'); $allergies = postString('allergies'); $otherNotes = postString('other_notes'); $days = postStringList('days'); $mealsByDay = postMealSelectionsByDay(); $errors = []; $allowedDays = ['1', '2', '3']; $allowedEarlyDepartures = [ 'sat_breakfast', 'sat_lunch', 'sat_coffee', 'sat_dinner', 'sun_breakfast', ]; $allowedMealsByDay = [ '1' => ['dinner'], '2' => ['breakfast', 'lunch', 'coffee', 'dinner'], '3' => ['breakfast', 'lunch'], ]; $days = array_values(array_unique($days)); foreach ($mealsByDay as $day => $dayMeals) { $mealsByDay[$day] = array_values(array_unique($dayMeals)); } if (count($days) > count($allowedDays) || array_diff($days, $allowedDays) !== []) { $errors[] = 'Bitte nur gültige Anwesenheitstage auswählen.'; } if (array_diff(array_keys($mealsByDay), $allowedDays) !== []) { $errors[] = 'Bitte nur gültige Mahlzeitentage auswählen.'; } foreach ($mealsByDay as $day => $dayMeals) { $allowedMeals = $allowedMealsByDay[(string) $day] ?? []; if (array_diff($dayMeals, $allowedMeals) !== []) { $errors[] = 'Bitte nur gültige Mahlzeiten auswählen.'; } if (!in_array((string) $day, $days, true) && $dayMeals !== []) { $errors[] = 'Mahlzeiten dürfen nur für ausgewählte Anwesenheitstage angegeben werden.'; } } foreach ($days as $day) { $mealsByDay[$day] ??= []; } if (mb_strlen($roommates) > 1000 || mb_strlen($allergies) > 1000) { $errors[] = 'Zimmerwunsch und Allergieangaben dürfen höchstens 1000 Zeichen enthalten.'; } if ($name === '' || mb_strlen($name) > 255) { $errors[] = 'Bitte einen gültigen Namen eingeben.'; } if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 255) { $errors[] = 'Bitte eine gültige E-Mail-Adresse eingeben.'; } if ($otherNotes !== '' && mb_strlen($otherNotes) > 5000) { $errors[] = 'Das Feld Sonstiges darf höchstens 5000 Zeichen enthalten.'; } if (!in_array($guestType, ['day_guest', 'regular_guest'], true)) { $errors[] = 'Bitte einen gültigen Gasttyp auswählen.'; } if ($guestType === 'regular_guest' && !in_array($roomType, ['single', 'double', 'multi'], true)) { $errors[] = 'Bitte eine gültige Zimmerart auswählen.'; } if ($earlyDeparture !== '' && ($guestType !== 'regular_guest' || !in_array($earlyDeparture, $allowedEarlyDepartures, true))) { $errors[] = 'Bitte einen gültigen Abreisezeitpunkt auswählen.'; } if ($errors !== []) { http_response_code(422); exit(implode("\n", $errors)); } $roommateEligible = $guestType === 'regular_guest' && in_array($roomType, ['double', 'multi'], true); if (!$roommateEligible) { $roommates = ''; } $role = $guestType === 'day_guest' ? 'Tagesgast' : 'Dauergast'; $bookingType = $guestType === 'day_guest' ? 'Day' : 'Stay'; $accommodationType = $guestType === 'regular_guest' ? $roomType : null; $roommateRequest = $roommateEligible && $roommates !== '' ? $roommates : null; $earlyDeparture = $guestType === 'regular_guest' && in_array($earlyDeparture, $allowedEarlyDepartures, true) ? $earlyDeparture : null; $mealPreferences = null; if ($guestType === 'day_guest') { $mealPreferences = json_encode( [ 'days' => $days, 'meals_by_day' => $mealsByDay, ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR ); } $participantId = sprintf( '%s-%s-%s-%s-%s', bin2hex(random_bytes(4)), bin2hex(random_bytes(2)), bin2hex(random_bytes(2)), bin2hex(random_bytes(2)), bin2hex(random_bytes(6)) ); $participantPassword = bin2hex(random_bytes(8)); $participantPasswordHash = password_hash($participantPassword, PASSWORD_DEFAULT); try { $dsn = sprintf( 'mysql:host=%s;port=%d;dbname=%s;charset=%s', $dbConfig['host'], $dbConfig['port'], $dbConfig['database'], $dbConfig['charset'] ); $pdo = new PDO($dsn, $dbConfig['username'], $dbConfig['password'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, ]); $totalPrice = calculateBookingTotal($pdo, $under27, $guestType, $accommodationType, $mealsByDay); $pdo->beginTransaction(); $participantStatement = $pdo->prepare( 'INSERT INTO participants (id, name, email, allergies, role, under_27, password_hash) VALUES (:id, :name, :email, :allergies, :role, :under_27, :password_hash)' ); $participantStatement->execute([ ':id' => $participantId, ':name' => $name, ':email' => $email, ':allergies' => $allergies !== '' ? $allergies : null, ':role' => $role, ':under_27' => $under27 ? 1 : 0, ':password_hash' => $participantPasswordHash, ]); $bookingStatement = $pdo->prepare( 'INSERT INTO bookings (participant_id, type, accommodation_type, meal_preferences, roommate_requests, early_departure, other_notes, total_price) VALUES (:participant_id, :type, :accommodation_type, :meal_preferences, :roommate_requests, :early_departure, :other_notes, :total_price)' ); $bookingStatement->execute([ ':participant_id' => $participantId, ':type' => $bookingType, ':accommodation_type' => $accommodationType, ':meal_preferences' => $mealPreferences, ':roommate_requests' => $roommateRequest, ':early_departure' => $earlyDeparture, ':other_notes' => $otherNotes !== '' ? $otherNotes : null, ':total_price' => number_format($totalPrice, 2, '.', ''), ]); $pdo->commit(); $mailError = ''; try { sendRegistrationEmail( $mailConfig, $email, $name, $participantPassword, $guestType, $under27, $roomType, $days, $mealsByDay, $allergies, $roommates, $otherNotes ); } catch (Throwable $exception) { error_log('Registrierung gespeichert, E-Mail konnte nicht versendet werden: ' . $exception->getMessage()); $mailError = '

Die Registrierung wurde gespeichert, aber die Bestätigungs-E-Mail konnte nicht versendet werden. Bitte bewahre die Zugangsdaten auf dieser Seite auf.

'; } $roomLabels = ['single' => 'Einzelzimmer', 'double' => 'Doppelzimmer', 'multi' => 'Mehrbettzimmer']; $dayLabels = ['1' => 'Fr.', '2' => 'Sa.', '3' => 'So.']; $mealLabels = [ 'breakfast' => 'Frühstück', 'lunch' => 'Mittagessen', 'coffee' => 'Kaffee', 'drinks' => 'Getränkepauschale', 'dinner' => 'Abendessen', 'rehearsal_room' => 'Proberaumpauschale', ]; $guestLabel = $guestType === 'day_guest' ? 'Tagesgast' : 'Dauergast'; $ageLabel = $under27 ? 'Unter 27' : 'Ab 27'; $roomLabel = $roomLabels[$roomType] ?? '–'; $dayText = implode(', ', array_map(static fn (string $day): string => $dayLabels[$day] ?? $day, $days)) ?: '–'; $mealText = formatMealSummary($days, $mealsByDay); $adminEmails = $pdo->query( "SELECT email FROM admin_users WHERE is_active = 1 AND email <> ''" )->fetchAll(PDO::FETCH_COLUMN); foreach ($adminEmails as $adminEmail) { if (!is_string($adminEmail) || !filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) { continue; } try { sendAdminNotificationEmail( $mailConfig, (string) $adminEmail, $name, $email, $guestLabel, $ageLabel, $roomLabel, $dayText, $mealText, $roommates !== '' ? $roommates : '–', $allergies !== '' ? $allergies : '–', $otherNotes !== '' ? $otherNotes : '–' ); } catch (Throwable $exception) { error_log('Admin-Benachrichtigung konnte nicht versendet werden: ' . $exception->getMessage()); } } http_response_code(201); $safeEmail = escapeHtml($email); $safePassword = escapeHtml($participantPassword); exit(<< Registrierung erfolgreich

Registrierung erfolgreich

{$mailError}

Bitte bewahre die folgenden Zugangsdaten auf. Du benötigst sie, um deine Registrierung später zu ändern oder zu löschen.

E-Mail-Adresse
{$safeEmail}
Passwort
{$safePassword}

Registrierung ändern oder löschen

Zur Startseite

HTML); } catch (Throwable $exception) { if (isset($pdo) && $pdo->inTransaction()) { $pdo->rollBack(); } error_log($exception->getMessage()); if ($exception instanceof PDOException && $exception->getCode() === '23000' && str_contains($exception->getMessage(), "for key 'email'")) { http_response_code(409); exit('Diese E-Mail-Adresse ist bereits registriert. Bitte nutze die Änderungs- oder Löschfunktion deiner bestehenden Registrierung.'); } http_response_code(500); exit('Die Registrierung konnte nicht gespeichert werden.'); }