This commit is contained in:
marijo 2026-06-29 08:55:40 +00:00
parent 3f670ee09f
commit ebb9c18177

142
api.php Normal file
View file

@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$secret = load_secret();
$requestSecret = $_SERVER['HTTP_X_ROUTES_SECRET'] ?? '';
if ($secret === '' || !hash_equals($secret, $requestSecret)) {
respond(401, ['error' => '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;
}