true, 'samesite' => 'Strict', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'use_strict_mode' => true, ]); session_start(); header('Cache-Control: no-store, no-cache, must-revalidate'); header('Pragma: no-cache'); 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'; 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 escapeHtml(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function earlyDepartureLabel(?string $value): string { return match ($value) { 'sat_breakfast' => 'Samstag nach dem Frühstück', 'sat_lunch' => 'Samstag nach dem Mittagessen', 'sat_coffee' => 'Samstag nach dem Kaffee', 'sat_dinner' => 'Samstag nach dem Abendessen', 'sun_breakfast' => 'Sonntag nach dem Frühstück', null, '' => 'Keine vorzeitige Abreise', default => $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 createPdo(array $dbConfig): PDO { $dsn = sprintf( 'mysql:host=%s;port=%d;dbname=%s;charset=%s', $dbConfig['host'], $dbConfig['port'], $dbConfig['database'], $dbConfig['charset'] ); return new PDO($dsn, $dbConfig['username'], $dbConfig['password'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, ]); } function editSmtpResponse($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 editSmtpCommand($socket, string $command, array $expectedCodes): void { fwrite($socket, $command . "\r\n"); $response = editSmtpResponse($socket); if (!in_array((int) substr($response, 0, 3), $expectedCodes, true)) { throw new RuntimeException('SMTP-Befehl wurde abgelehnt.'); } } function editEncodedHeader(string $value): string { return '=?UTF-8?B?' . base64_encode($value) . '?='; } function sendDeletionAdminEmail(array $mailConfig, string $recipient, array $booking): void { $message = 'From: ' . editEncodedHeader($mailConfig['from_name']) . ' <' . $mailConfig['from_address'] . ">\r\n" . 'To: ' . $recipient . "\r\n" . 'Subject: ' . editEncodedHeader('Registrierung gelöscht – Chorwochenende') . "\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" . "Eine Teilnehmerregistrierung wurde selbstständig gelöscht.\n\n" . "Name: {$booking['name']}\n" . "E-Mail: {$booking['email']}\n" . "Gasttyp: {$booking['role']}\n" . "Zimmerart: {$booking['accommodation_type']}\n" . "Zimmerpartner-Wunsch: {$booking['roommate_requests']}\n" . "Sonstiges: {$booking['other_notes']}\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 { if ((int) substr(editSmtpResponse($socket), 0, 3) !== 220) { throw new RuntimeException('SMTP-Verbindung wurde abgelehnt.'); } editSmtpCommand($socket, 'EHLO localhost', [250]); editSmtpCommand($socket, 'STARTTLS', [220]); if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { throw new RuntimeException('STARTTLS konnte nicht aktiviert werden.'); } editSmtpCommand($socket, 'EHLO localhost', [250]); editSmtpCommand($socket, 'AUTH LOGIN', [334]); editSmtpCommand($socket, base64_encode($mailConfig['username']), [334]); editSmtpCommand($socket, base64_encode($mailConfig['password']), [235]); editSmtpCommand($socket, 'MAIL FROM:<' . $mailConfig['from_address'] . '>', [250]); editSmtpCommand($socket, 'RCPT TO:<' . $recipient . '>', [250, 251]); editSmtpCommand($socket, 'DATA', [354]); fwrite($socket, preg_replace('/^\./m', '..', $message) . "\r\n.\r\n"); if ((int) substr(editSmtpResponse($socket), 0, 3) !== 250) { throw new RuntimeException('E-Mail konnte nicht angenommen werden.'); } fwrite($socket, "QUIT\r\n"); } finally { fclose($socket); } } function redirectToEdit(): never { header('Location: edit_registration.php'); exit; } function editCsrfToken(): string { if (!isset($_SESSION['edit_csrf'])) { $_SESSION['edit_csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['edit_csrf']; } function requireEditCsrf(): void { $submitted = postString('csrf_token'); $stored = $_SESSION['edit_csrf'] ?? ''; if ($stored === '' || !hash_equals($stored, $submitted)) { http_response_code(403); exit('Ungültige Anfrage.'); } } function validateBookingInput(): array { $data = [ 'name' => postString('name'), 'email' => postString('email'), 'guest_type' => postString('guest_type'), 'room_type' => postString('room_type'), 'roommates' => postString('roommates'), 'early_departure' => postString('early_departure'), 'allergies' => postString('allergies'), 'other_notes' => postString('other_notes'), 'days' => postStringList('days'), 'meals_by_day' => postMealSelectionsByDay(), ]; $errors = []; if ($data['name'] === '' || mb_strlen($data['name']) > 255) { $errors[] = 'Bitte einen gültigen Namen eingeben.'; } if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL) || mb_strlen($data['email']) > 255) { $errors[] = 'Bitte eine gültige E-Mail-Adresse eingeben.'; } if (!in_array($data['guest_type'], ['day_guest', 'regular_guest'], true)) { $errors[] = 'Bitte einen gültigen Gasttyp auswählen.'; } if ($data['guest_type'] === 'regular_guest' && !in_array($data['room_type'], ['single', 'double', 'multi'], true)) { $errors[] = 'Bitte eine gültige Zimmerart auswählen.'; } $allowedEarlyDepartures = ['sat_breakfast', 'sat_lunch', 'sat_coffee', 'sat_dinner', 'sun_breakfast']; if ($data['early_departure'] !== '' && ($data['guest_type'] !== 'regular_guest' || !in_array($data['early_departure'], $allowedEarlyDepartures, true))) { $errors[] = 'Bitte einen gültigen Abreisezeitpunkt auswählen.'; } if (mb_strlen($data['other_notes']) > 5000) { $errors[] = 'Das Feld Sonstiges darf höchstens 5000 Zeichen enthalten.'; } if (mb_strlen($data['roommates']) > 1000 || mb_strlen($data['allergies']) > 1000) { $errors[] = 'Zimmerwunsch und Allergieangaben dürfen höchstens 1000 Zeichen enthalten.'; } $allowedDays = ['1', '2', '3']; $allowedMealsByDay = [ '1' => ['dinner'], '2' => ['breakfast', 'lunch', 'coffee', 'dinner'], '3' => ['breakfast', 'lunch'], ]; $data['days'] = array_values(array_unique($data['days'])); foreach ($data['meals_by_day'] as $day => $dayMeals) { $data['meals_by_day'][$day] = array_values(array_unique($dayMeals)); } if (count($data['days']) > count($allowedDays) || array_diff($data['days'], $allowedDays) !== []) { $errors[] = 'Bitte nur gültige Anwesenheitstage auswählen.'; } if (array_diff(array_keys($data['meals_by_day']), $allowedDays) !== []) { $errors[] = 'Bitte nur gültige Mahlzeitentage auswählen.'; } foreach ($data['meals_by_day'] 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, $data['days'], true) && $dayMeals !== []) { $errors[] = 'Mahlzeiten dürfen nur für ausgewählte Anwesenheitstage angegeben werden.'; } } foreach ($data['days'] as $day) { $data['meals_by_day'][$day] ??= []; } if ($errors !== []) { throw new InvalidArgumentException(implode(' ', $errors)); } return $data; } $pdo = createPdo($dbConfig); $message = $_SESSION['edit_message'] ?? ''; unset($_SESSION['edit_message']); if (postString('action') === 'login') { $email = postString('email'); $password = postString('password'); $lockedUntil = (int) ($_SESSION['edit_locked_until'] ?? 0); if ($lockedUntil > time()) { $message = 'Zu viele Fehlversuche. Bitte später erneut versuchen.'; } else { $statement = $pdo->prepare( 'SELECT p.id AS participant_id, b.id AS booking_id, p.password_hash FROM participants p INNER JOIN bookings b ON b.participant_id = p.id WHERE p.email = :email LIMIT 1' ); $statement->execute([':email' => $email]); $participant = $statement->fetch(PDO::FETCH_ASSOC); if ($participant !== false && password_verify($password, $participant['password_hash'])) { unset($_SESSION['edit_login_failures'], $_SESSION['edit_locked_until']); session_regenerate_id(true); $_SESSION['edit_participant_id'] = $participant['participant_id']; $_SESSION['edit_booking_id'] = (int) $participant['booking_id']; $_SESSION['edit_csrf'] = bin2hex(random_bytes(32)); $_SESSION['edit_last_activity'] = time(); redirectToEdit(); } $failures = (int) ($_SESSION['edit_login_failures'] ?? 0) + 1; $_SESSION['edit_login_failures'] = $failures; if ($failures >= 5) { $_SESSION['edit_locked_until'] = time() + 300; } usleep(500000); $message = 'E-Mail-Adresse oder Passwort ist ungültig.'; } } if (isset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'])) { $lastActivity = (int) ($_SESSION['edit_last_activity'] ?? 0); if ($lastActivity > 0 && $lastActivity < time() - 1800) { unset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'], $_SESSION['edit_csrf']); session_regenerate_id(true); } else { $_SESSION['edit_last_activity'] = time(); } } if (!isset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'])) { ?> Registrierung ändern

Registrierung ändern

Gib die E-Mail-Adresse und das Passwort ein, die du nach der Registrierung erhalten hast.

Zur Startseite

time() - 42000, 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Strict', ]); session_destroy(); redirectToEdit(); } if ($action === 'delete_booking') { $deletedStatement = $pdo->prepare( 'SELECT p.name, p.email, p.role, b.accommodation_type, b.roommate_requests, b.early_departure, b.other_notes FROM participants p INNER JOIN bookings b ON b.participant_id = p.id WHERE p.id = :participant_id AND b.id = :booking_id' ); $deletedStatement->execute([ ':participant_id' => $participantId, ':booking_id' => $bookingId, ]); $deletedBooking = $deletedStatement->fetch(PDO::FETCH_ASSOC); if ($deletedBooking === false) { throw new InvalidArgumentException('Registrierung nicht gefunden.'); } $deletedBooking['accommodation_type'] = match ($deletedBooking['accommodation_type']) { 'single' => 'Einzelzimmer', 'double' => 'Doppelzimmer', 'multi' => 'Mehrbettzimmer', default => '–', }; $deletedBooking['roommate_requests'] = $deletedBooking['roommate_requests'] ?: '–'; $deletedBooking['other_notes'] = $deletedBooking['other_notes'] ?: '–'; $pdo->beginTransaction(); $deleteBooking = $pdo->prepare( 'DELETE FROM bookings WHERE id = :booking_id AND participant_id = :participant_id' ); $deleteBooking->execute([ ':booking_id' => $bookingId, ':participant_id' => $participantId, ]); $deleteParticipant = $pdo->prepare('DELETE FROM participants WHERE id = :participant_id'); $deleteParticipant->execute([':participant_id' => $participantId]); $pdo->commit(); $adminEmails = $pdo->query( "SELECT email FROM admin_users WHERE is_active = 1 AND email <> ''" )->fetchAll(PDO::FETCH_COLUMN); foreach ($adminEmails as $adminEmail) { try { sendDeletionAdminEmail($mailConfig, (string) $adminEmail, $deletedBooking); } catch (Throwable $exception) { error_log('Admin-Benachrichtigung über Löschung konnte nicht versendet werden: ' . $exception->getMessage()); } } unset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'], $_SESSION['edit_csrf']); $_SESSION['edit_message'] = 'Die Registrierung wurde gelöscht.'; redirectToEdit(); } if ($action === 'update_booking') { $data = validateBookingInput(); $role = $data['guest_type'] === 'day_guest' ? 'Tagesgast' : 'Dauergast'; $bookingType = $data['guest_type'] === 'day_guest' ? 'Day' : 'Stay'; $accommodationType = $data['guest_type'] === 'regular_guest' ? $data['room_type'] : null; $roommateRequest = $data['guest_type'] === 'regular_guest' && $data['roommates'] !== '' ? $data['roommates'] : null; $allowedEarlyDepartures = ['sat_breakfast', 'sat_lunch', 'sat_coffee', 'sat_dinner', 'sun_breakfast']; $earlyDeparture = $data['guest_type'] === 'regular_guest' && in_array($data['early_departure'], $allowedEarlyDepartures, true) ? $data['early_departure'] : null; $mealPreferences = $data['guest_type'] === 'day_guest' ? json_encode(['days' => $data['days'], 'meals_by_day' => $data['meals_by_day']], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) : null; $pdo->beginTransaction(); $existingPriceStatement = $pdo->prepare( 'SELECT manual_total_price FROM bookings WHERE id = :booking_id AND participant_id = :participant_id FOR UPDATE' ); $existingPriceStatement->execute([ ':booking_id' => $bookingId, ':participant_id' => $participantId, ]); $existingManualPrice = $existingPriceStatement->fetchColumn(); if ($existingManualPrice === false) { throw new InvalidArgumentException('Registrierung nicht gefunden.'); } $participantStatement = $pdo->prepare( 'UPDATE participants SET name = :name, email = :email, allergies = :allergies, role = :role, under_27 = :under_27 WHERE id = :participant_id' ); $participantStatement->execute([ ':name' => $data['name'], ':email' => $data['email'], ':allergies' => $data['allergies'] !== '' ? $data['allergies'] : null, ':role' => $role, ':under_27' => isset($_POST['under_27']) && $_POST['under_27'] === '1' ? 1 : 0, ':participant_id' => $participantId, ]); $totalPrice = calculateBookingTotal( $pdo, isset($_POST['under_27']) && $_POST['under_27'] === '1', $data['guest_type'], $accommodationType, $data['meals_by_day'] ); $bookingStatement = $pdo->prepare( 'UPDATE bookings SET type = :type, accommodation_type = :accommodation_type, meal_preferences = :meal_preferences, roommate_requests = :roommate_requests, early_departure = :early_departure, other_notes = :other_notes, total_price = :total_price WHERE id = :booking_id AND participant_id = :participant_id' ); $bookingStatement->execute([ ':type' => $bookingType, ':accommodation_type' => $accommodationType, ':meal_preferences' => $mealPreferences, ':roommate_requests' => $roommateRequest, ':early_departure' => $earlyDeparture, ':other_notes' => $data['other_notes'] !== '' ? $data['other_notes'] : null, ':total_price' => $existingManualPrice !== null ? number_format((float) $existingManualPrice, 2, '.', '') : number_format($totalPrice, 2, '.', ''), ':booking_id' => $bookingId, ':participant_id' => $participantId, ]); $pdo->commit(); $_SESSION['edit_message'] = 'Die Registrierung wurde aktualisiert.'; redirectToEdit(); } } catch (InvalidArgumentException | JsonException $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } $message = $exception->getMessage(); } catch (PDOException $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } error_log($exception->getMessage()); $message = 'Die Änderung konnte nicht gespeichert werden. Prüfe bitte deine Eingaben.'; } } $statement = $pdo->prepare( 'SELECT p.name, p.email, p.role, p.under_27, p.allergies, b.type, b.accommodation_type, b.meal_preferences, b.roommate_requests, b.early_departure, b.other_notes FROM participants p INNER JOIN bookings b ON b.participant_id = p.id WHERE p.id = :participant_id AND b.id = :booking_id' ); $statement->execute([':participant_id' => $participantId, ':booking_id' => $bookingId]); $booking = $statement->fetch(PDO::FETCH_ASSOC); if ($booking === false) { unset($_SESSION['edit_participant_id'], $_SESSION['edit_booking_id'], $_SESSION['edit_csrf']); http_response_code(404); exit('Registrierung nicht gefunden.'); } $mealData = $booking['meal_preferences'] !== null ? json_decode($booking['meal_preferences'], true) : []; $mealData = is_array($mealData) ? $mealData : []; $selectedMealsByDay = is_array($mealData['meals_by_day'] ?? null) ? $mealData['meals_by_day'] : []; if ($selectedMealsByDay === [] && is_array($mealData['meals'] ?? null)) { foreach ($mealData['days'] ?? [] as $day) { $selectedMealsByDay[(string) $day] = $mealData['meals']; } } $guestType = $booking['type'] === 'Day' ? 'day_guest' : 'regular_guest'; $csrf = editCsrfToken(); ?> Registrierung ändern

Registrierung ändern

Zur Startseite