true, 'samesite' => 'Lax', 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')]); session_start(); } function csrfToken(): string { bookingSession(); return $_SESSION['csrf_token'] ??= bin2hex(random_bytes(32)); } function readPayload(): array { if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 32768) respond(413, ['error' => 'Die Anfrage ist zu groß.']); try { $payload = json_decode((string) file_get_contents('php://input'), true, 32, JSON_THROW_ON_ERROR); } catch (JsonException) { respond(400, ['error' => 'Die Anfrage konnte nicht verarbeitet werden.']); } if (!is_array($payload)) respond(400, ['error' => 'Die Anfrage konnte nicht verarbeitet werden.']); return $payload; } function ensureRateLimit(): void { bookingSession(); $now = time(); $recent = array_values(array_filter($_SESSION['booking_requests'] ?? [], static fn ($value): bool => is_int($value) && $value > $now - 900)); if (count($recent) >= 10) respond(429, ['error' => 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.']); $recent[] = $now; $_SESSION['booking_requests'] = $recent; } try { $database = bookingDatabase(); if ($_SERVER['REQUEST_METHOD'] === 'GET') { $project = (string) ($_GET['project'] ?? ''); $from = (string) ($_GET['from'] ?? ''); $to = (string) ($_GET['to'] ?? ''); $fromDate = DateTimeImmutable::createFromFormat('!Y-m-d', $from); $toDate = DateTimeImmutable::createFromFormat('!Y-m-d', $to); if (!bookingProjectIsValid($project) || !$fromDate || !$toDate || $fromDate >= $toDate || $fromDate->diff($toDate)->days > 93) respond(400, ['error' => 'Ungültige Kalenderanfrage.']); respond(200, ['csrfToken' => csrfToken(), 'events' => bookingListEvents($database, $project, $from, $to)]); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { ensureRateLimit(); if (!hash_equals(csrfToken(), (string) ($_SERVER['HTTP_X_SUFW_BOOKING_CSRF'] ?? ''))) respond(403, ['error' => 'Die Sitzung ist abgelaufen. Bitte laden Sie den Kalender neu.']); try { $registration = bookingCreateRegistration($database, readPayload()); } catch (InvalidArgumentException $exception) { respond(422, ['error' => $exception->getMessage()]); } catch (OutOfBoundsException $exception) { respond(404, ['error' => $exception->getMessage()]); } catch (LogicException|RuntimeException $exception) { respond(409, ['error' => $exception->getMessage()]); } respond(202, ['message' => 'Ihre Anfrage wurde gespeichert. Bitte bestätigen Sie den Link aus der E-Mail innerhalb von 30 Minuten.', 'expiresAt' => $registration['expiresAt']]); } header('Allow: GET, POST'); respond(405, ['error' => 'Diese Methode wird nicht unterstützt.']); } catch (Throwable $exception) { error_log('SUFW booking API: '.$exception->getMessage()); respond(500, ['error' => 'Der Kalender ist vorübergehend nicht verfügbar.']); }