time() + 86400, 'path' => '/', 'secure' => isset($_SERVER['HTTPS']), 'httponly' => true, 'samesite' => 'Lax', ]); // The raw token rides in a readable cookie so server-side templates // can echo it into the tag without keeping // the raw token in `index.php` per-request state. HttpOnly off so the // server can read it via the request cookie jar; security comes from // the HMAC verification, not from hiding the raw token (which the // browser would already see in the meta tag anyway). setcookie('__csrf_token', $token, [ 'expires' => time() + 86400, 'path' => '/', 'secure' => isset($_SERVER['HTTPS']), 'httponly' => false, 'samesite' => 'Lax', ]); $existingCookie = $cookie; $existingRawToken = $token; $cookies['__csrf'] = $cookie; $cookies['__csrf_token'] = $token; } // 3. Compute the upload-endpoint user token (derived from the same secret). $uploadsToken = hash_hmac('sha256', $csrfSecret, 'bf-uploads'); // 4. Determine the request method, path, and content type. $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); $uri = (string) ($_SERVER['REQUEST_URI'] ?? '/'); $path = parse_url($uri, PHP_URL_PATH) ?: '/'; $queryString = (string) (parse_url($uri, PHP_URL_QUERY) ?? ''); $contentType = isset($_SERVER['CONTENT_TYPE']) ? (string) $_SERVER['CONTENT_TYPE'] : null; if ($contentType !== null) { $semicolon = strpos($contentType, ';'); if ($semicolon !== false) { $contentType = substr($contentType, 0, $semicolon); } $contentType = trim($contentType); } // 5. Read the raw body for fetch POSTs that submit JSON. $rawBody = (string) ($_SERVER['__raw_body'] ?? file_get_contents('php://input')); // 6. Build a Request with the request data, the CSRF secret, and the upload token. $server = $_SERVER; if (!isset($server['__csrf_secret'])) { $server['__csrf_secret'] = $csrfSecret; } $server['__uploads_token'] = $uploadsToken; $request = new Request( get: $_GET, post: $_POST, files: $_FILES, cookies: $cookies, server: $server, queryString: $queryString, method: $method, path: $path, contentType: $contentType, rawBody: $rawBody, ); // 7. Configure the router. $uploadsRoot = __DIR__ . '/../var/uploads'; $placeholderDir = __DIR__ . '/assets/placeholders'; $scenariosDir = __DIR__ . '/assets/scenarios'; $homePage = static function (Request $r, array $p) use ($scenariosDir): Response { return (new GetHomePage($scenariosDir))->handle($r, $p); }; $getTeam = static fn (Request $r, array $p): Response => (new GetTeamEditor())->handle($r, $p); $postTeam = static fn (Request $r, array $p): Response => (new PostTeamEditor())->handle($r, $p); $getBattlefield = static fn (Request $r, array $p): Response => (new GetBattlefieldEditor())->handle($r, $p); $postBattlefield = static fn (Request $r, array $p): Response => (new PostBattlefieldEditor())->handle($r, $p); $postStart = static fn (Request $r, array $p): Response => (new PostStartMatch())->handle($r, $p); $postUpload = static fn (Request $r, array $p): Response => (new PostImageUpload($uploadsRoot))->handle($r, $p); $getAssets = static function (Request $r, array $p) use ($placeholderDir, $uploadsRoot): Response { return (new GetAssets($placeholderDir, $uploadsRoot))->handle($r, $p); }; $getUploadedAsset = static function (Request $r, array $p) use ($placeholderDir, $uploadsRoot): Response { $handler = new GetAssets($placeholderDir, $uploadsRoot); return $handler->handle( $r, ['kind' => 'uploads', 'userToken' => $p['userToken'] ?? '', 'filename' => $p['filename'] ?? ''], ); }; $getBundledList = static function (Request $r, array $p) use ($scenariosDir): Response { return (new GetBundledScenarios($scenariosDir))->handle($r, $p); }; $getBundledOne = static function (Request $r, array $p) use ($scenariosDir): Response { return (new GetBundledScenario($scenariosDir))->handle($r, $p); }; $getMatchView = static fn (Request $r, array $p): Response => (new GetMatchView())->handle($r, $p); $postMatchMove = static function (Request $r, array $p) use ($csrfSecret): Response { return (new PostMatchMove($csrfSecret))->handle($r, $p); }; $postMatchAttack = static function (Request $r, array $p) use ($csrfSecret): Response { return (new PostMatchAttack($csrfSecret))->handle($r, $p); }; $postMatchAbility = static function (Request $r, array $p) use ($csrfSecret): Response { return (new PostMatchAbility($csrfSecret))->handle($r, $p); }; $postMatchEndTurn = static function (Request $r, array $p) use ($csrfSecret): Response { return (new PostMatchEndTurn($csrfSecret))->handle($r, $p); }; $router = new Router(); $router->add('GET', '/', $homePage); $router->add('GET', '/scenarios/bundled', $getBundledList); $router->add('GET', '/scenarios/bundled/{id}', $getBundledOne); $router->add('GET', '/matches/current', $getMatchView); $router->add('POST', '/matches/current/move', $postMatchMove); $router->add('POST', '/matches/current/attack', $postMatchAttack); $router->add('POST', '/matches/current/ability', $postMatchAbility); $router->add('POST', '/matches/current/end-turn', $postMatchEndTurn); $router->add('GET', '/scenarios/{id}/edit/team', $getTeam); $router->add('POST', '/scenarios/{id}/edit/team', $postTeam); $router->add('GET', '/scenarios/{id}/edit/battlefield', $getBattlefield); $router->add('POST', '/scenarios/{id}/edit/battlefield', $postBattlefield); $router->add('POST', '/scenarios/{id}/start', $postStart); $router->add('POST', '/assets/upload', $postUpload); $router->add('GET', '/assets/uploads/{userToken}/{filename}', $getUploadedAsset); $router->add('GET', '/assets/{kind}/{filename}', $getAssets); // 8. Dispatch and emit. $response = $router->dispatch($request); http_response_code($response->status); foreach ($response->headers as $name => $value) { header($name . ': ' . $value); } echo $response->body; return $response->status;