From ebb9c18177148fd82535ae75451423c684c42b28 Mon Sep 17 00:00:00 2001 From: marijo Date: Mon, 29 Jun 2026 08:55:40 +0000 Subject: [PATCH] api.php --- api.php | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 api.php diff --git a/api.php b/api.php new file mode 100644 index 0000000..e24769b --- /dev/null +++ b/api.php @@ -0,0 +1,142 @@ + '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; +}