77 lines
3.5 KiB
PHP
77 lines
3.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require __DIR__.'/backend.php';
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
function respond(int $status, array $payload): never
|
|
{
|
|
http_response_code($status);
|
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
|
exit;
|
|
}
|
|
|
|
function bookingSession(): void
|
|
{
|
|
if (session_status() === PHP_SESSION_ACTIVE) return;
|
|
session_name('sufw_booking');
|
|
session_set_cookie_params(['httponly' => 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.']);
|
|
}
|