'Unauthorized']); } $dataDir = __DIR__ . '/data'; $dataFile = $dataDir . '/routes.json'; if ($_SERVER['REQUEST_METHOD'] === 'GET') { respond(200, ['routes' => read_routes($dataFile)]); } if ($_SERVER['REQUEST_METHOD'] === 'PUT' || $_SERVER['REQUEST_METHOD'] === 'POST') { $payload = json_decode((string) file_get_contents('php://input'), true); if (!is_array($payload) || !isset($payload['routes']) || !is_array($payload['routes'])) { respond(400, ['error' => 'Invalid payload']); } $routes = array_map('sanitize_route', $payload['routes']); write_routes($dataDir, $dataFile, $routes); respond(200, ['ok' => true, 'routes' => $routes]); } respond(405, ['error' => 'Method not allowed']); function load_secret(): string { $configFile = __DIR__ . '/config.js'; if (!is_file($configFile)) { return ''; } $config = (string) file_get_contents($configFile); if (preg_match('/ROUTES_SECRET\s*=\s*(["\'])(.*?)\1\s*;?/', $config, $matches)) { return stripcslashes($matches[2]); } return ''; } function read_routes(string $dataFile): array { if (!is_file($dataFile)) { return []; } $handle = fopen($dataFile, 'rb'); if ($handle === false) { respond(500, ['error' => 'Could not open routes file']); } flock($handle, LOCK_SH); $contents = stream_get_contents($handle) ?: '[]'; flock($handle, LOCK_UN); fclose($handle); $routes = json_decode($contents, true); return is_array($routes) ? $routes : []; } function write_routes(string $dataDir, string $dataFile, array $routes): void { if (!is_dir($dataDir) && !mkdir($dataDir, 0775, true) && !is_dir($dataDir)) { respond(500, ['error' => 'Could not create data directory']); } $tmpFile = $dataFile . '.tmp'; $json = json_encode($routes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($json === false) { respond(500, ['error' => 'Could not encode routes']); } $handle = fopen($tmpFile, 'wb'); if ($handle === false) { respond(500, ['error' => 'Could not write routes file']); } flock($handle, LOCK_EX); fwrite($handle, $json); fflush($handle); flock($handle, LOCK_UN); fclose($handle); if (!rename($tmpFile, $dataFile)) { @unlink($tmpFile); respond(500, ['error' => 'Could not save routes file']); } } function sanitize_route(array $route): array { $clean = [ 'id' => trim((string) ($route['id'] ?? '')), 'name' => trim((string) ($route['name'] ?? '')), 'link' => trim((string) ($route['link'] ?? '')), 'date' => trim((string) ($route['date'] ?? '')), 'archived' => (bool) ($route['archived'] ?? false), 'picture' => (string) ($route['picture'] ?? ''), 'description' => trim((string) ($route['description'] ?? '')), 'updatedAt' => trim((string) ($route['updatedAt'] ?? '')), ]; if ($clean['id'] === '' || $clean['name'] === '' || $clean['link'] === '') { respond(400, ['error' => 'Route id, name, and link are required']); } if (!filter_var($clean['link'], FILTER_VALIDATE_URL)) { respond(400, ['error' => 'Invalid route link']); } if ($clean['date'] !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $clean['date'])) { respond(400, ['error' => 'Invalid route date']); } if ($clean['picture'] !== '' && !str_starts_with($clean['picture'], 'data:image/') && !filter_var($clean['picture'], FILTER_VALIDATE_URL)) { respond(400, ['error' => 'Invalid route picture']); } return $clean; } function respond(int $status, array $payload): never { http_response_code($status); echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); exit; }