PHP Classes

File: routes/webdoor-routes.php

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   routes/webdoor-routes.php   Download  
File: routes/webdoor-routes.php
Role: Example script
Content type: text/plain
Description: Example script
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 39,596 bytes
 

Contents

Class file image Download
<?php /** * WebDoor Routes * * Web pages and REST API endpoints for WebDoor (HTML5 BBS games) */ use BinktermPHP\ActivityTracker; use BinktermPHP\Auth; use BinktermPHP\BbsConfig; use BinktermPHP\DoorManager; use BinktermPHP\GameConfig; use BinktermPHP\JsdosDoorConfig; use BinktermPHP\JsdosDoorManifest; use BinktermPHP\JsdosDoorSupport; use BinktermPHP\Template; use BinktermPHP\WebDoorController; use BinktermPHP\WebDoorManifest; use Pecee\SimpleRouter\SimpleRouter; function webdoorApiError(string $errorCode, string $message, int $status = 400): void { http_response_code($status); echo json_encode([ 'success' => false, 'error_code' => $errorCode, 'error' => $message, ]); } /** * Helper function to get available WebDoor features */ function getAvailableWebDoorFeatures(): array { $features = []; // Storage and leaderboard are always available via WebDoor API $features[] = 'storage'; $features[] = 'leaderboard'; // Check if credits system is enabled $bbsConfig = BbsConfig::getConfig(); $creditsConfig = $bbsConfig['credits'] ?? []; if (!empty($creditsConfig['enabled'])) { $features[] = 'credits'; } return $features; } /** * Helper function to check if manifest requirements are met */ function checkManifestRequirements(array $manifest): bool { $requirements = $manifest['requirements'] ?? []; $requiredFeatures = $requirements['features'] ?? []; if (empty($requiredFeatures)) { return true; // No requirements, always met } $availableFeatures = getAvailableWebDoorFeatures(); // Check if all required features are available foreach ($requiredFeatures as $required) { if (!in_array($required, $availableFeatures, true)) { return false; } } return true; } /** * Resolve a JS-DOS game + mode request with access checks and normalized manifest data. * * @param array<string, mixed>|null $user * @return array<string, mixed>|null */ function resolveJsdosDoorRequest(string $gameId, string $modeId = 'play', ?array $user = null): ?array { $entry = JsdosDoorManifest::getManifest($gameId); if (!$entry || !JsdosDoorConfig::isEnabled($entry['id'])) { return null; } $manifest = JsdosDoorSupport::normalizeManifest($entry['manifest']); $mode = JsdosDoorSupport::getMode($manifest, $modeId); if (!$mode || !JsdosDoorSupport::canUserAccessMode($mode, $user)) { return null; } return [ 'entry' => $entry, 'manifest' => $manifest, 'mode' => $mode, 'mode_id' => $modeId, ]; } /** * Load a JS-DOS session for the current user. * * @return array<string, mixed>|null */ function loadJsdosSessionForUser(string $sessionId, int $userId): ?array { $db = \BinktermPHP\Database::getInstance()->getPdo(); $stmt = $db->prepare(" SELECT session_id, door_id, user_id, door_type, user_data, ended_at, expires_at FROM door_sessions WHERE session_id = ? AND user_id = ? AND door_type = 'jsdos' AND ended_at IS NULL LIMIT 1 "); $stmt->execute([$sessionId, $userId]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); if (!$row) { return null; } $userData = []; if (!empty($row['user_data'])) { if (is_array($row['user_data'])) { $userData = $row['user_data']; } elseif (is_string($row['user_data'])) { $decoded = json_decode($row['user_data'], true); if (is_array($decoded)) { $userData = $decoded; } } } $row['user_data'] = $userData; $row['mode'] = (string)($userData['mode'] ?? 'play'); return $row; } /** * Web Routes for WebDoor */ // GET /games - List available games SimpleRouter::get('/games', function() { $auth = new Auth(); $user = $auth->getCurrentUser(); if (!$user) { return SimpleRouter::response()->redirect('/login'); } if(GameConfig::isGameSystemEnabled()==false){ $template = new Template(); $template->renderResponse('error.twig', [ 'error_code' => 'ui.webdoors.errors.system_disabled' ]); exit; } $games = []; // Get Web Doors foreach (WebDoorManifest::listManifests() as $entry) { $manifest = $entry['manifest']; if (!isset($manifest['game'])) { continue; } // Check if all required features are available if (!checkManifestRequirements($manifest)) { continue; } $game = $manifest['game']; $game['path'] = $entry['path']; $game['icon_url'] = "/webdoors/{$entry['path']}/" . ($game['icon'] ?? 'icon.png'); $game['type'] = 'webdoor'; if(GameConfig::isEnabled($entry['id'])){ // Check for display_name and display_description overrides in configuration $gameConfig = GameConfig::getGameConfig($entry['id']); if ($gameConfig) { // Check top level first (config/webdoors.json uses flat structure) $displayName = $gameConfig['display_name'] ?? null; $displayDesc = $gameConfig['display_description'] ?? null; if (!empty($displayName)) { $game['name'] = $displayName; } if (!empty($displayDesc)) { $game['description'] = $displayDesc; } } $games[] = $game; } } // Get DOS Doors $doorManager = new DoorManager(); $dosDoors = $doorManager->getEnabledDoors(); foreach ($dosDoors as $doorId => $door) { // Skip admin-only doors for non-admin users if (!empty($door['admin_only']) && empty($user['is_admin'])) { continue; } // Check if door has a custom icon in manifest $iconUrl = '/images/dos-door-icon.png'; // Default icon if (!empty($door['icon'])) { // Use asset endpoint (manifest declares the actual filename) $iconUrl = "/door-assets/{$doorId}/icon"; } $games[] = [ 'id' => $doorId, 'name' => $door['name'], 'description' => $door['description'] ?? '', 'author' => $door['author'] ?? null, 'version' => $door['game_version'] ?? null, 'path' => $doorId, // Will become /games/{doorid} (uses iframe wrapper) 'icon_url' => $iconUrl, 'type' => 'dosdoor', 'genre' => $door['genre'] ?? [], 'players' => $door['players'] ?? null ]; } // Get Native Doors $nativeDoorManager = new \BinktermPHP\NativeDoorManager(); $nativeDoors = $nativeDoorManager->getEnabledDoors(); foreach ($nativeDoors as $doorId => $door) { // Skip admin-only doors for non-admin users if (!empty($door['admin_only']) && empty($user['is_admin'])) { continue; } $iconUrl = '/images/dos-door-icon.png'; // Default icon if (!empty($door['icon'])) { $iconUrl = "/door-assets/{$doorId}/icon"; } $games[] = [ 'id' => $doorId, 'name' => $door['name'], 'description' => $door['description'] ?? '', 'author' => $door['author'] ?? null, 'version' => $door['game_version'] ?? null, 'path' => $doorId, // Will become /games/{doorid} (uses iframe wrapper) 'icon_url' => $iconUrl, 'type' => 'nativedoor', 'genre' => $door['genre'] ?? [], 'players' => $door['players'] ?? null ]; } // Get JS-DOS Doors if (JsdosDoorConfig::isConfigPresent()) { foreach (JsdosDoorManifest::listManifests() as $entry) { $manifest = $entry['manifest']; $gameId = $entry['id']; if (!JsdosDoorConfig::isEnabled($gameId)) { continue; } $gameConfig = JsdosDoorConfig::getGameConfig($gameId); $name = $gameConfig['display_name'] ?? $manifest['name'] ?? $gameId; $description = $gameConfig['display_description'] ?? $manifest['description'] ?? ''; $iconUrl = "/jsdos-doors/{$entry['path']}/" . ($manifest['icon'] ?? 'icon.png'); $games[] = [ 'id' => $gameId, 'name' => $name, 'description' => $description, 'author' => $manifest['author'] ?? null, 'version' => $manifest['version'] ?? null, 'path' => $gameId, 'icon_url' => $iconUrl, 'type' => 'jsdosdoor', ]; } } // Sort all games by name usort($games, function($a, $b) { return strcasecmp($a['name'], $b['name']); }); // Build game lookup table for leaderboard (includes display_name overrides) $gameLookup = []; foreach ($games as $game) { $gameLookup[$game['id']] = [ 'name' => $game['name'], // Already has display_name override applied if configured 'path' => $game['path'] ]; } $monthOffset = 0; if (isset($_GET['month_offset'])) { $monthOffset = (int)$_GET['month_offset']; if ($monthOffset < 0) { $monthOffset = 0; } if ($monthOffset > 120) { $monthOffset = 120; } } $monthStart = new \DateTimeImmutable('first day of this month 00:00:00'); if ($monthOffset > 0) { $monthStart = $monthStart->modify("-{$monthOffset} months"); } $monthEnd = $monthStart->modify('+1 month'); $leaderboardMonthLabel = $monthStart->format('F Y'); $leaderboard = []; try { $db = \BinktermPHP\Database::getInstance()->getPdo(); $limit = 10; $stmt = $db->prepare(' WITH best_scores AS ( SELECT DISTINCT ON (l.user_id, l.game_id, l.board) l.user_id, l.game_id, l.board, l.score, l.created_at FROM webdoor_leaderboards l WHERE l.created_at >= ? AND l.created_at < ? ORDER BY l.user_id, l.game_id, l.board, l.score DESC, l.created_at DESC ) SELECT b.game_id, b.board, u.real_name, u.username, b.score, b.created_at FROM best_scores b JOIN users u ON b.user_id = u.id ORDER BY b.score DESC, b.created_at DESC LIMIT ? '); $stmt->bindValue(1, $monthStart->format('Y-m-d H:i:s')); $stmt->bindValue(2, $monthEnd->format('Y-m-d H:i:s')); $stmt->bindValue(3, $limit, \PDO::PARAM_INT); $stmt->execute(); $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); foreach ($rows as $index => $row) { $displayName = $row['username']; $gameInfo = $gameLookup[$row['game_id']] ?? null; $leaderboard[] = [ 'rank' => $index + 1, 'display_name' => $displayName, 'score' => (int)$row['score'], 'game_id' => $row['game_id'], 'game_name' => $gameInfo['name'] ?? ucfirst($row['game_id']), 'game_path' => $gameInfo['path'] ?? null, 'board' => $row['board'], 'date' => substr($row['created_at'], 0, 10) ]; } } catch (\Throwable $e) { getServerLogger()->error('Failed to load WebDoor leaderboard: ' . $e->getMessage()); } $template = new Template(); $template->renderResponse('webdoors.twig', [ 'games' => $games, 'leaderboard' => $leaderboard, 'leaderboard_month_label' => $leaderboardMonthLabel, 'leaderboard_month_offset' => $monthOffset ]); }); // GET /games/dosdoors/{doorid} - Play a DOS door game SimpleRouter::get('/games/dosdoors/{doorid}', function($doorid) { $auth = new Auth(); $user = $auth->getCurrentUser(); if (!$user) { return SimpleRouter::response()->redirect('/login'); } if(GameConfig::isGameSystemEnabled()==false){ $template = new Template(); $template->renderResponse('error.twig', [ 'error_code' => 'ui.webdoors.errors.system_disabled' ]); exit; } // Verify door exists and is enabled $doorManager = new DoorManager(); $door = $doorManager->getDoor($doorid); if (!$door || empty($door['config']['enabled'])) { http_response_code(404); $template = new Template(); $template->renderResponse('404.twig', [ 'requested_url' => "/games/dosdoors/{$doorid}" ]); return; } // Block admin-only doors for non-admins if (!empty($door['admin_only']) && empty($user['is_admin'])) { http_response_code(403); $template = new Template(); $template->renderResponse('error.twig', [ 'error_title_code' => 'ui.error.access_error', 'error_code' => 'ui.webdoors.errors.admin_only' ]); return; } // Include the DOS door player $doorId = $doorid; // For the player script require __DIR__ . '/../public_html/webdoors/dosdoors/index.php'; }); // GET /games/nativedoors/{doorid} - Play a native Linux door game SimpleRouter::get('/games/nativedoors/{doorid}', function($doorid) { $auth = new Auth(); $user = $auth->getCurrentUser(); if (!$user) { return SimpleRouter::response()->redirect('/login'); } if (GameConfig::isGameSystemEnabled() == false) { $template = new Template(); $template->renderResponse('error.twig', [ 'error_code' => 'ui.webdoors.errors.system_disabled' ]); exit; } // Verify door exists and is enabled $nativeDoorManager = new \BinktermPHP\NativeDoorManager(); $door = $nativeDoorManager->getDoor($doorid); if (!$door || empty($door['config']['enabled'])) { http_response_code(404); $template = new Template(); $template->renderResponse('404.twig', [ 'requested_url' => "/games/nativedoors/{$doorid}" ]); return; } // Block admin-only doors for non-admins if (!empty($door['admin_only']) && empty($user['is_admin'])) { http_response_code(403); $template = new Template(); $template->renderResponse('error.twig', [ 'error_title_code' => 'ui.error.access_error', 'error_code' => 'ui.webdoors.errors.admin_only' ]); return; } // Reuse the DOS door terminal player (same WebSocket protocol) $doorId = $doorid; require __DIR__ . '/../public_html/webdoors/dosdoors/index.php'; }); // GET /games/jsdos/{gameId} - JS-DOS door player (or coming-soon for custom emulators) SimpleRouter::get('/games/jsdos/{gameId}', function(string $gameId) { $auth = new Auth(); $user = $auth->getCurrentUser(); if (!$user) { return SimpleRouter::response()->redirect('/login'); } if (GameConfig::isGameSystemEnabled() == false) { $template = new Template(); $template->renderResponse('error.twig', [ 'error_code' => 'ui.webdoors.errors.system_disabled' ]); exit; } $requestedMode = trim((string)($_GET['mode'] ?? 'play')); if ($requestedMode === '') { $requestedMode = 'play'; } $entry = JsdosDoorManifest::getManifest($gameId); if (!$entry || !JsdosDoorConfig::isEnabled($entry['id'])) { http_response_code(404); $template = new Template(); $template->renderResponse('404.twig', [ 'requested_url' => "/games/jsdos/{$gameId}" ]); return; } $resolved = resolveJsdosDoorRequest($gameId, $requestedMode, $user); if (!$resolved) { http_response_code(403); $template = new Template(); $template->renderResponse('error.twig', [ 'error_title_code' => 'ui.error.access_error', 'error_code' => 'ui.webdoors.errors.admin_only' ]); return; } $entry = $resolved['entry']; $manifest = $resolved['manifest']; $mode = $resolved['mode']; $gameConfig = JsdosDoorConfig::getGameConfig($entry['id']); $name = $gameConfig['display_name'] ?? $manifest['name'] ?? $entry['id']; $description = $gameConfig['display_description'] ?? $manifest['description'] ?? ''; $gameData = array_merge($manifest, ['name' => $name, 'description' => $description]); $emulator = $mode['emulator'] ?? ($manifest['emulator'] ?? 'jsdos'); $template = new Template(); if ($emulator === 'jsdos') { $template->renderResponse('jsdosdoor_play.twig', [ 'game' => $gameData, 'game_id' => $entry['id'], 'game_path' => $entry['path'], 'mode' => $mode, 'mode_id' => $requestedMode, ]); } else { // Phase 5: custom emulator support ? show placeholder for now $template->renderResponse('jsdosdoor_coming_soon.twig', [ 'game' => $gameData, 'game_path' => $entry['path'], ]); } }); // POST /api/jsdoor/session - Create a JS-DOS game session SimpleRouter::post('/api/jsdoor/session', function() { header('Content-Type: application/json'); $auth = new Auth(); $user = $auth->getCurrentUser(); if (!$user) { http_response_code(401); webdoorApiError('errors.auth.authentication_required', 'Authentication required', 401); return; } if (!GameConfig::isGameSystemEnabled()) { webdoorApiError('errors.webdoor.feature_disabled', 'Game system is not enabled', 500); return; } $body = json_decode(file_get_contents('php://input'), true); $gameId = trim((string)($body['game_id'] ?? '')); $requestedMode = trim((string)($body['mode'] ?? 'play')); if ($gameId === '') { http_response_code(400); webdoorApiError('errors.jsdosdoor.game_not_found', 'Game ID required', 400); return; } if ($requestedMode === '') { $requestedMode = 'play'; } $entry = JsdosDoorManifest::getManifest($gameId); if (!$entry || !JsdosDoorConfig::isEnabled($entry['id'])) { http_response_code(404); webdoorApiError('errors.jsdosdoor.game_not_found', 'Game not found', 404); return; } $resolved = resolveJsdosDoorRequest($gameId, $requestedMode, $user); if (!$resolved) { http_response_code(403); webdoorApiError('errors.door.admin_only', 'This door is restricted to administrators', 403); return; } $entry = $resolved['entry']; $manifest = $resolved['manifest']; $sessionCost = (int)($manifest['credits']['session_cost'] ?? 0); if ($sessionCost > 0) { http_response_code(400); webdoorApiError('errors.jsdosdoor.session_create_failed', 'Credit-gated sessions are not yet supported', 400); return; } $userId = (int)($user['user_id'] ?? $user['id'] ?? 0); $sessionId = bin2hex(random_bytes(16)); $expiresAt = date('Y-m-d H:i:s', time() + 14400); // 4 hours try { $db = \BinktermPHP\Database::getInstance()->getPdo(); // Close any existing active jsdos session for this user + game $stmt = $db->prepare(" UPDATE door_sessions SET ended_at = NOW() WHERE user_id = ? AND door_id = ? AND door_type = 'jsdos' AND ended_at IS NULL "); $stmt->execute([$userId, $entry['id']]); // Create new session record $stmt = $db->prepare(" INSERT INTO door_sessions (session_id, user_id, door_id, expires_at, door_type, user_data) VALUES (?, ?, ?, ?, 'jsdos', ?::jsonb) "); $userDataJson = json_encode(['mode' => $requestedMode]); $stmt->execute([$sessionId, $userId, $entry['id'], $expiresAt, $userDataJson]); echo json_encode([ 'success' => true, 'session_id' => $sessionId, 'expires_at' => $expiresAt, 'mode' => $requestedMode, ]); } catch (\Throwable $e) { getServerLogger()->error('Failed to create jsdos session: ' . $e->getMessage()); http_response_code(500); webdoorApiError('errors.jsdosdoor.session_create_failed', 'Failed to create session', 500); } }); // POST /api/jsdoor/session/{sessionId}/end - End a JS-DOS game session SimpleRouter::post('/api/jsdoor/session/{sessionId}/end', function(string $sessionId) { header('Content-Type: application/json'); $auth = new Auth(); $user = $auth->getCurrentUser(); if (!$user) { http_response_code(401); webdoorApiError('errors.auth.authentication_required', 'Authentication required', 401); return; } $userId = (int)($user['user_id'] ?? $user['id'] ?? 0); try { $db = \BinktermPHP\Database::getInstance()->getPdo(); $stmt = $db->prepare(" UPDATE door_sessions SET ended_at = NOW() WHERE session_id = ? AND user_id = ? AND door_type = 'jsdos' AND ended_at IS NULL "); $stmt->execute([$sessionId, $userId]); echo json_encode(['success' => true]); } catch (\Throwable $e) { getServerLogger()->error('Failed to end jsdos session: ' . $e->getMessage()); http_response_code(500); webdoorApiError('errors.jsdosdoor.session_end_failed', 'Failed to end session', 500); } }); // GET /api/jsdoor/files/{gameId} - Load JS-DOS synced files for the active session SimpleRouter::get('/api/jsdoor/files/{gameId}', function(string $gameId) { header('Content-Type: application/json'); $auth = new Auth(); $user = $auth->getCurrentUser(); if (!$user) { http_response_code(401); webdoorApiError('errors.auth.authentication_required', 'Authentication required', 401); return; } $sessionId = trim((string)($_GET['session_id'] ?? '')); if ($sessionId === '') { http_response_code(400); webdoorApiError('errors.jsdosdoor.session_create_failed', 'Session ID required', 400); return; } $userId = (int)($user['user_id'] ?? $user['id'] ?? 0); $session = loadJsdosSessionForUser($sessionId, $userId); if (!$session || (string)$session['door_id'] !== $gameId) { http_response_code(404); webdoorApiError('errors.jsdosdoor.game_not_found', 'Active session not found', 404); return; } $resolved = resolveJsdosDoorRequest($gameId, (string)$session['mode'], $user); if (!$resolved) { http_response_code(403); webdoorApiError('errors.door.admin_only', 'This door is restricted to administrators', 403); return; } $manifest = $resolved['manifest']; $mode = $resolved['mode']; $modeSaveConfig = JsdosDoorSupport::getSaveConfig($mode); $files = []; foreach (JsdosDoorSupport::getSharedSaveModes($manifest) as $sharedMode) { $sharedFiles = JsdosDoorSupport::loadStoredFiles( JsdosDoorSupport::getSharedStorageDirectory($gameId), $sharedMode['saves'] ); foreach ($sharedFiles as $dosPath => $contents) { $files[$dosPath] = $contents; } } if (!empty($modeSaveConfig['enabled'])) { $baseDir = $modeSaveConfig['scope'] === 'shared' ? JsdosDoorSupport::getSharedStorageDirectory($gameId) : JsdosDoorSupport::getUserStorageDirectory($userId, $gameId); $modeFiles = JsdosDoorSupport::loadStoredFiles($baseDir, $modeSaveConfig); foreach ($modeFiles as $dosPath => $contents) { $files[$dosPath] = $contents; } } echo json_encode([ 'success' => true, 'files' => array_map(static function(string $dosPath, string $contents): array { return [ 'dos_path' => $dosPath, 'content_b64' => base64_encode($contents), ]; }, array_keys($files), array_values($files)), ]); }); // POST /api/jsdoor/files/{gameId} - Save modified JS-DOS files for the active session SimpleRouter::post('/api/jsdoor/files/{gameId}', function(string $gameId) { header('Content-Type: application/json'); $auth = new Auth(); $user = $auth->getCurrentUser(); if (!$user) { http_response_code(401); webdoorApiError('errors.auth.authentication_required', 'Authentication required', 401); return; } $input = json_decode(file_get_contents('php://input'), true); $sessionId = trim((string)($input['session_id'] ?? '')); $payloadFiles = is_array($input['files'] ?? null) ? $input['files'] : []; $endSession = !empty($input['end_session']); if ($sessionId === '') { http_response_code(400); webdoorApiError('errors.jsdosdoor.session_create_failed', 'Session ID required', 400); return; } $userId = (int)($user['user_id'] ?? $user['id'] ?? 0); $session = loadJsdosSessionForUser($sessionId, $userId); if (!$session || (string)$session['door_id'] !== $gameId) { http_response_code(404); webdoorApiError('errors.jsdosdoor.game_not_found', 'Active session not found', 404); return; } $resolved = resolveJsdosDoorRequest($gameId, (string)$session['mode'], $user); if (!$resolved) { http_response_code(403); webdoorApiError('errors.door.admin_only', 'This door is restricted to administrators', 403); return; } $modeSaveConfig = JsdosDoorSupport::getSaveConfig($resolved['mode']); if (empty($modeSaveConfig['enabled'])) { if ($endSession) { $db = \BinktermPHP\Database::getInstance()->getPdo(); $stmt = $db->prepare(" UPDATE door_sessions SET ended_at = NOW() WHERE session_id = ? AND user_id = ? AND door_type = 'jsdos' AND ended_at IS NULL "); $stmt->execute([$sessionId, $userId]); } echo json_encode(['success' => true, 'saved' => 0]); return; } $isSharedScope = $modeSaveConfig['scope'] === 'shared'; // Shared saves may only come from admin users operating an admin_only mode. // Non-admin callers or non-admin modes are blocked regardless of the scope value. if ($isSharedScope && (empty($resolved['mode']['admin_only']) || empty($user['is_admin']))) { http_response_code(403); webdoorApiError('errors.door.admin_only', 'Shared saves require an admin mode', 403); return; } $baseDir = $isSharedScope ? '' : JsdosDoorSupport::getUserStorageDirectory($userId, $gameId); try { $saved = 0; $daemonClient = $isSharedScope ? new \BinktermPHP\Admin\AdminDaemonClient() : null; foreach ($payloadFiles as $payloadFile) { if (!is_array($payloadFile)) { continue; } $dosPath = trim((string)($payloadFile['dos_path'] ?? '')); if ($dosPath === '') { continue; } if ($isSharedScope) { $contentB64 = (string)($payloadFile['content_b64'] ?? ''); $daemonClient->saveJsdosSharedFile($gameId, $dosPath, $contentB64, !empty($payloadFile['deleted'])); $saved++; continue; } if (!empty($payloadFile['deleted'])) { JsdosDoorSupport::deleteStoredFile($baseDir, $modeSaveConfig, $dosPath); $saved++; continue; } $contentB64 = (string)($payloadFile['content_b64'] ?? ''); $contents = base64_decode($contentB64, true); if ($contents === false) { throw new \RuntimeException('Invalid base64 file payload'); } JsdosDoorSupport::writeStoredFile($baseDir, $modeSaveConfig, $dosPath, $contents); $saved++; } if ($endSession) { $db = \BinktermPHP\Database::getInstance()->getPdo(); $stmt = $db->prepare(" UPDATE door_sessions SET ended_at = NOW() WHERE session_id = ? AND user_id = ? AND door_type = 'jsdos' AND ended_at IS NULL "); $stmt->execute([$sessionId, $userId]); } echo json_encode(['success' => true, 'saved' => $saved]); } catch (\Throwable $e) { getServerLogger()->error('Failed to save jsdos files: ' . $e->getMessage()); http_response_code(400); webdoorApiError('errors.jsdosdoor.session_end_failed', 'Failed to save files', 400); } }); // GET /games/{game} - Play a specific game (DOS door or web door) SimpleRouter::get('/games/{game}', function($game) { $auth = new Auth(); $user = $auth->getCurrentUser(); if (!$user) { return SimpleRouter::response()->redirect('/login'); } if(GameConfig::isGameSystemEnabled()==false){ $template = new Template(); $template->renderResponse('error.twig', [ 'error_code' => 'ui.webdoors.errors.system_disabled' ]); exit; } // Check if this is a DOS door first $doorManager = new DoorManager(); $door = $doorManager->getDoor($game); if ($door && !empty($door['config']['enabled'])) { // Block admin-only doors for non-admins if (!empty($door['admin_only']) && empty($user['is_admin'])) { http_response_code(403); $template = new Template(); $template->renderResponse('error.twig', [ 'error_title_code' => 'ui.error.access_error', 'error_code' => 'ui.webdoors.errors.admin_only' ]); return; } // This is a DOS door - render with embedded player $template = new Template(); $template->renderResponse('dosdoor_play.twig', [ 'door' => $door, 'door_id' => $game, 'player_url' => "/games/dosdoors/{$game}" ]); return; } // Check if this is a native door $nativeDoorManager = new \BinktermPHP\NativeDoorManager(); $nativeDoor = $nativeDoorManager->getDoor($game); if ($nativeDoor && !empty($nativeDoor['config']['enabled'])) { // Block admin-only doors for non-admins if (!empty($nativeDoor['admin_only']) && empty($user['is_admin'])) { http_response_code(403); $template = new Template(); $template->renderResponse('error.twig', [ 'error_title_code' => 'ui.error.access_error', 'error_code' => 'ui.webdoors.errors.admin_only' ]); return; } $template = new Template(); $template->renderResponse('dosdoor_play.twig', [ 'door' => $nativeDoor, 'door_id' => $game, 'player_url' => "/games/nativedoors/{$game}" ]); return; } // Check if this is a JS-DOS door $jsdosEntry = JsdosDoorManifest::getManifest($game); if ($jsdosEntry && JsdosDoorConfig::isEnabled($jsdosEntry['id'])) { $requestedMode = trim((string)($_GET['mode'] ?? 'play')); if ($requestedMode === '') { $requestedMode = 'play'; } $resolved = resolveJsdosDoorRequest($game, $requestedMode, $user); if (!$resolved) { http_response_code(403); $template = new Template(); $template->renderResponse('error.twig', [ 'error_title_code' => 'ui.error.access_error', 'error_code' => 'ui.webdoors.errors.admin_only' ]); return; } $manifest = $resolved['manifest']; $mode = $resolved['mode']; $gameConfig = JsdosDoorConfig::getGameConfig($jsdosEntry['id']); $name = $gameConfig['display_name'] ?? $manifest['name'] ?? $jsdosEntry['id']; $description = $gameConfig['display_description'] ?? $manifest['description'] ?? ''; $modeLabel = (string)($mode['label'] ?? ''); $playerUrl = "/games/jsdos/{$jsdosEntry['id']}" . ($requestedMode !== 'play' ? ('?mode=' . urlencode($requestedMode)) : ''); $secondaryUrl = null; $secondaryLabelKey = null; if (!empty($user['is_admin']) && JsdosDoorSupport::hasMode($manifest, 'config')) { if ($requestedMode === 'config') { $secondaryUrl = "/games/{$game}"; $secondaryLabelKey = 'ui.webdoor_play.play_mode'; } else { $secondaryUrl = "/games/{$game}?mode=config"; $secondaryLabelKey = 'ui.webdoor_play.admin_config'; } } $template = new Template(); $template->renderResponse('dosdoor_play.twig', [ 'door' => [ 'name' => $requestedMode === 'play' || $modeLabel === '' ? $name : ($name . ' - ' . $modeLabel), 'description' => $description, ], 'door_id' => $game, 'player_url' => $playerUrl, 'secondary_url' => $secondaryUrl, 'secondary_label_key' => $secondaryLabelKey, 'show_fullscreen_button' => false, ]); return; } // Check if this is a web door $gameDir = __DIR__ . '/../public_html/webdoors/' . basename($game); $manifestPath = $gameDir . '/webdoor.json'; if (!file_exists($manifestPath)) { http_response_code(404); $template = new Template(); $template->renderResponse('404.twig', [ 'requested_url' => "/games/{$game}" ]); return; } $manifest = json_decode(file_get_contents($manifestPath), true); // Check if all required features are available if (!checkManifestRequirements($manifest)) { $template = new Template(); $template->renderResponse('error.twig', [ 'error_code' => 'ui.webdoors.errors.requirements_not_met' ]); return; } $entryPoint = $manifest['game']['entry_point'] ?? 'index.html'; $gameUrl = "/webdoors/{$game}/{$entryPoint}"; // Apply display_name and display_description overrides if configured $gameData = $manifest['game']; $gameConfig = GameConfig::getGameConfig($game); if ($gameConfig) { // Check top level (config/webdoors.json uses flat structure) $displayName = $gameConfig['display_name'] ?? null; $displayDesc = $gameConfig['display_description'] ?? null; if (!empty($displayName)) { $gameData['name'] = $displayName; } if (!empty($displayDesc)) { $gameData['description'] = $displayDesc; } } $template = new Template(); $template->renderResponse('webdoor_play.twig', [ 'game' => $gameData, 'game_url' => $gameUrl, 'game_id' => $game ]); }); /** * WebDoor API Routes */ // GET /api/webdoor/session - Get or create session SimpleRouter::get('/api/webdoor/session', function() { header('Content-Type: application/json'); if(GameConfig::isGameSystemEnabled()==false){ webdoorApiError('errors.webdoor.feature_disabled', 'Game system is not enabled', 500); exit; } $controller = new WebDoorController(); $result = $controller->getSession(); // Track webdoor play session (only when the session endpoint returns successfully) if (!empty($result['session_id'])) { $auth = new Auth(); $user = $auth->getCurrentUser(); if ($user) { $userId = $user['user_id'] ?? $user['id'] ?? null; $gameId = $result['game']['id'] ?? null; ActivityTracker::track($userId, ActivityTracker::TYPE_WEBDOOR_PLAY, null, $gameId); } } echo json_encode($result); }); // POST /api/webdoor/session/end - End session SimpleRouter::post('/api/webdoor/session/end', function() { header('Content-Type: application/json'); if(GameConfig::isGameSystemEnabled()==false){ webdoorApiError('errors.webdoor.feature_disabled', 'Game system is not enabled', 500); exit; } $controller = new WebDoorController(); $result = $controller->endSession(); echo json_encode($result); }); // GET /api/webdoor/storage - List all saves SimpleRouter::get('/api/webdoor/storage', function() { header('Content-Type: application/json'); if(GameConfig::isGameSystemEnabled()==false){ webdoorApiError('errors.webdoor.feature_disabled', 'Game system is not enabled', 500); exit; } $controller = new WebDoorController(); $result = $controller->listSaves(); echo json_encode($result); }); // GET /api/webdoor/storage/{slot} - Load specific save SimpleRouter::get('/api/webdoor/storage/{slot}', function($slot) { header('Content-Type: application/json'); if(GameConfig::isGameSystemEnabled()==false){ webdoorApiError('errors.webdoor.feature_disabled', 'Game system is not enabled', 500); exit; } $controller = new WebDoorController(); $result = $controller->loadSave((int)$slot); if ($result === null) { webdoorApiError('errors.webdoor.save_not_found', 'Save not found', 404); return; } echo json_encode($result); }); // PUT /api/webdoor/storage/{slot} - Save game data SimpleRouter::put('/api/webdoor/storage/{slot}', function($slot) { header('Content-Type: application/json'); if(GameConfig::isGameSystemEnabled()==false){ webdoorApiError('errors.webdoor.feature_disabled', 'Game system is not enabled', 500); exit; } $controller = new WebDoorController(); $result = $controller->saveGame((int)$slot); echo json_encode($result); }); // DELETE /api/webdoor/storage/{slot} - Delete save SimpleRouter::delete('/api/webdoor/storage/{slot}', function($slot) { header('Content-Type: application/json'); if(GameConfig::isGameSystemEnabled()==false){ webdoorApiError('errors.webdoor.feature_disabled', 'Game system is not enabled', 500); exit; } $controller = new WebDoorController(); $result = $controller->deleteSave((int)$slot); echo json_encode($result); }); // GET /api/webdoor/leaderboard/{board} - Get leaderboard SimpleRouter::get('/api/webdoor/leaderboard/{board}', function($board) { header('Content-Type: application/json'); if(GameConfig::isGameSystemEnabled()==false){ webdoorApiError('errors.webdoor.feature_disabled', 'Game system is not enabled', 500); exit; } $controller = new WebDoorController(); $result = $controller->getLeaderboard($board); echo json_encode($result); }); // POST /api/webdoor/leaderboard/{board} - Submit score SimpleRouter::post('/api/webdoor/leaderboard/{board}', function($board) { header('Content-Type: application/json'); if(GameConfig::isGameSystemEnabled()==false){ webdoorApiError('errors.webdoor.feature_disabled', 'Game system is not enabled', 500); exit; } $controller = new WebDoorController(); $result = $controller->submitScore($board); echo json_encode($result); });