0, 'path' => '/', 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Strict', ]); session_start(); } // ─── CSRF PROTECTION ────────────────────────────────────────────────────────── // Token init + csrf_token()/csrf_field()/csrf_verify() helpers, shared with the // admin Mods (which are POSTed to directly). See GameEngine/Admin/csrf.php. include_once("../GameEngine/Admin/csrf.php"); // ─── CORE INCLUDES ─────────────────────────────────────────────────────────── include_once("../GameEngine/config.php"); include_once("../GameEngine/Database.php"); include_once("../GameEngine/Lang/" . LANG . ".php"); include_once("../GameEngine/Admin/database.php"); include_once("../GameEngine/Data/buidata.php"); include_once("../GameEngine/Artifacts.php"); // ─── SECURITY HELPERS ──────────────────────────────────────────────────────── /** * Return a sanitised integer from a superglobal key, or null if missing/invalid. * Replaces direct (int) casts on $_GET inside switch — ensures 0 is treated as * absent (IDs are always >= 1 in TravianZ). */ function admin_input_id(array $source, string $key): ?int { if (!isset($source[$key]) || !ctype_digit((string)$source[$key])) { return null; } $v = (int)$source[$key]; return $v > 0 ? $v : null; } /** * HTML-escape a value for safe output inside HTML attributes or text nodes. */ function e(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } /** * Whitelist-validate the ?p= parameter. * Returns the validated page string, or '' if not in the whitelist. * * SECURITY: This is the primary defence against path-traversal in the * include('Templates/'.$p.'.tpl') call below. Only values present in this * array are ever passed to include(). */ function admin_validated_page(string $raw): string { static $whitelist = [ 'server_info', 'online', 'notregistered', 'inactive', 'report', 'message', 'massmessage', 'sysmessage', 'map', 'map_tile', 'natars', 'search', 'ban', 'maintenance', 'cleanban', 'gold', 'usergold', 'maintenenceResetGold', 'delmedal', 'delallymedal', 'givePlus', 'maintenenceResetPlus', 'givePlusRes', 'maintenenceResetPlusBonus', 'addUsers', 'users', 'admin_log', 'config', 'debug_log', 'editServerSet', 'editPlusSet', 'editLogSet', 'editNewsboxSet', 'editExtraSet', 'editAdminInfo', 'resetServer', 'player', 'editUser', 'deletion', 'Newmessage', 'editPlus', 'editSitter', 'editOverall', 'editWeek', 'userlogin', 'userillegallog', 'editHero', 'editAdditional', 'village', 'editResources', 'addTroops', 'addABTroops', 'editVillage', 'villagelog', 'techlog', 'msg', ]; return in_array($raw, $whitelist, true) ? $raw : ''; } // CSRF helpers — csrf_token() / csrf_field() / csrf_verify() — are defined in // GameEngine/Admin/csrf.php (included above), shared with the admin Mods. /** * Look up a user row by ID using a prepared statement. * Replaces the two raw mysqli_query() calls for userlogin / userillegallog. * * Returns the associative row, or null on failure / not found. */ function admin_get_user_by_id(int $uid): ?array { $link = $GLOBALS['link']; $stmt = mysqli_prepare($link, "SELECT * FROM `" . TB_PREFIX . "users` WHERE `id` = ?"); if (!$stmt) { return null; } mysqli_stmt_bind_param($stmt, 'i', $uid); mysqli_stmt_execute($stmt); $result = mysqli_stmt_get_result($stmt); $row = $result ? mysqli_fetch_assoc($result) : null; mysqli_stmt_close($stmt); return $row ?: null; } // ─── PAGE ROUTING ───────────────────────────────────────────────────────────── // Read and whitelist the ?p= parameter once; all branching below uses $page. $rawPage = isset($_GET['p']) ? trim((string)$_GET['p']) : ''; $page = admin_validated_page($rawPage); $subpage = 'Login'; $not_include_mootools_js = false; if ($page !== '') { switch ($page) { // ── Simple label-only pages ────────────────────────────────────────── case 'server_info': $subpage = 'Server Info'; break; case 'online': $subpage = 'Online Users'; break; case 'notregistered': $subpage = 'Players Not Activated'; break; case 'inactive': $subpage = 'Players Inactivate'; break; case 'report': $subpage = 'Players Report'; break; case 'message': // NOTE: original code had this case duplicated (second occurrence // overrode with 'Search IGMs/Reports'). The first definition // ('Players Message') is intentional for the ?p=message route. // The 'Search IGMs/Reports' label belongs to ?p=search sub-section // which is already covered by the search template include logic. $subpage = 'Players Message'; break; case 'msg': $subpage = 'Search IGMs/Reports'; break; case 'massmessage': $subpage = 'Mass Message'; break; case 'sysmessage': $subpage = 'System Message'; break; case 'map': $subpage = 'Map'; break; case 'map_tile': $subpage = 'Map Tile'; $not_include_mootools_js = true; break; case 'natars': $subpage = 'Natars Management'; break; case 'search': $subpage = 'General Search'; break; case 'ban': $subpage = 'Ban/Unban Players'; break; case 'maintenance': $subpage = 'Server Maintenance'; break; case 'cleanban': $subpage = 'Clean Banlist Data'; break; case 'gold': $subpage = 'Give All Free Gold'; break; case 'usergold': $subpage = 'Give Free Gold To Specific User'; break; case 'maintenenceResetGold': $subpage = 'Reset Gold'; break; case 'delmedal': $subpage = 'Delete Player Medals'; break; case 'delallymedal': $subpage = 'Delete Ally Medals'; break; case 'givePlus': $subpage = 'Give All Plus'; break; case 'maintenenceResetPlus': $subpage = 'Reset Plus'; break; case 'givePlusRes': $subpage = 'Give All Res Bonus'; break; case 'maintenenceResetPlusBonus': $subpage = 'Reset Res Bonus'; break; case 'addUsers': $subpage = 'Create Users'; break; case 'users': $subpage = 'Users List'; break; case 'admin_log': $subpage = 'Admin Log'; break; case 'config': $subpage = 'Server Settings'; break; case 'debug_log': $subpage = 'Debug Error Log'; break; case 'editServerSet': $subpage = 'Server Configuration'; break; case 'editPlusSet': $subpage = 'PLUS Settings'; break; case 'editLogSet': $subpage = 'Log Settings'; break; case 'editNewsboxSet': $subpage = 'NewsBox Settings'; break; case 'editExtraSet': $subpage = 'Extra Settings'; break; case 'editAdminInfo': $subpage = 'Edit Admin Information'; break; case 'resetServer': $subpage = 'Server Resetting'; break; // ── User-context pages (require a valid ?uid=) ─────────────────────── case 'player': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $displayarray = $database->getUserArray($uid, 1); $user = $displayarray; $subpage = 'Player Details (' . e($user['username']) . ')'; } else { $subpage = 'Player Details (no player)'; } break; case 'editUser': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $user = $database->getUserArray($uid, 1); $subpage = 'Edit Player (' . e($user['username']) . ')'; } else { $subpage = 'Edit Player (no player)'; } break; case 'deletion': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $user = $database->getUserArray($uid, 1); $subpage = 'Delete Player (' . e($user['username']) . ')'; } else { $subpage = 'Delete Player (no player)'; } break; case 'Newmessage': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $user = $database->getUserArray($uid, 1); $subpage = 'Compose Message (' . e($user['username']) . ')'; } else { $subpage = 'Compose Message'; } break; case 'editPlus': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $user = $database->getUserArray($uid, 1); $subpage = 'Edit Plus & Resources (' . e($user['username']) . ')'; } else { $subpage = 'Edit Plus & Resources'; } break; case 'editSitter': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $user = $database->getUserArray($uid, 1); $subpage = 'Edit Sitters (' . e($user['username']) . ')'; } else { $subpage = 'Edit Sitters'; } break; case 'editOverall': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $user = $database->getUserArray($uid, 1); $subpage = 'Edit Off & Def (' . e($user['username']) . ')'; } else { $subpage = 'Edit Off & Def'; } break; case 'editWeek': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $user = $database->getUserArray($uid, 1); $subpage = 'Edit Weekly Off & Def (' . e($user['username']) . ')'; } else { $subpage = 'Edit Weekly Off & Def'; } break; case 'userlogin': // SECURITY FIX: was raw mysqli_query with direct $_GET interpolation. // Now uses admin_get_user_by_id() which internally uses a prepared statement. $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $player = admin_get_user_by_id($uid); $subpage = $player ? 'User Logins (' . e($player['username']) . ')' : 'User Logins (player not found)'; } else { $subpage = 'User Logins (no player)'; } break; case 'userillegallog': // SECURITY FIX: same as userlogin above. $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $player = admin_get_user_by_id($uid); $subpage = $player ? 'User Illegals Log (' . e($player['username']) . ')' : 'User Illegals Log (player not found)'; } else { $subpage = 'User Illegals Log (no player)'; } break; case 'editHero': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $user = $database->getUserArray($uid, 1); $subpage = 'Edit Hero (' . e($user['username']) . ')'; } else { $subpage = 'Edit Hero'; } break; case 'editAdditional': $uid = admin_input_id($_GET, 'uid'); if ($uid !== null) { $user = $database->getUserArray($uid, 1); $subpage = 'Edit Additional Info (' . e($user['username']) . ')'; } else { $subpage = 'Edit Additional Info'; } break; // ── Village-context pages (require a valid ?did=) ──────────────────── case 'village': $did = admin_input_id($_GET, 'did'); if ($did !== null) { $village = $database->getVillage($did); if ($village) { $user = $database->getUserArray($village['owner'], 1); $subpage = 'Edit Village (' . e($village['name']) . ' » ' . e($user['username'] ?? '?') . ')'; } else { $subpage = 'Edit Village (ID ' . $did . ' not found)'; $village = null; } } else { $subpage = 'Edit Village (no village)'; } break; case 'editResources': $did = admin_input_id($_GET, 'did'); if ($did !== null) { $village = $database->getVillage($did); if ($village) { $user = $database->getUserArray($village['owner'], 1); $subpage = 'Edit Resources (' . e($village['name']) . ' » ' . e($user['username']) . ')'; } else { // BUGFIX: original used $did which was only set in 'village' case, // causing an undefined variable notice here. Now always defined above. $subpage = 'Edit Resources (ID ' . $did . ' not found)'; $village = null; } } else { $subpage = 'Edit Resources (no village)'; } break; case 'addTroops': $did = admin_input_id($_GET, 'did'); if ($did !== null) { $village = $database->getVillage($did); $user = $database->getUserArray($village['owner'], 1); $subpage = 'Edit Troops (' . e($village['name']) . ' » ' . e($user['username']) . ')'; } else { $subpage = 'Edit Troops (no village)'; } break; case 'addABTroops': $did = admin_input_id($_GET, 'did'); if ($did !== null) { $village = $database->getVillage($did); $user = $database->getUserArray($village['owner'], 1); $subpage = 'Upgrade Troops (' . e($village['name']) . ' » ' . e($user['username']) . ')'; } else { $subpage = 'Upgrade Troops (no village)'; } break; case 'editVillage': $did = admin_input_id($_GET, 'did'); if ($did !== null) { $village = $database->getVillage($did); $user = $database->getUserArray($village['owner'], 1); $subpage = 'Edit Village (' . e($village['name']) . ' » ' . e($user['username']) . ')'; } else { $subpage = 'Edit Village (no village)'; } break; case 'villagelog': $did = admin_input_id($_GET, 'did'); if ($did !== null) { $village = $database->getVillage($did); $user = $database->getUserArray($village['owner'], 1); $subpage = 'Build Log (' . e($village['name']) . ' » ' . e($user['username']) . ')'; } else { $subpage = 'Build Log (no village)'; } break; case 'techlog': $did = admin_input_id($_GET, 'did'); if ($did !== null) { $village = $database->getVillage($did); $user = $database->getUserArray($village['owner'], 1); $subpage = 'Research Log (' . e($village['name']) . ' » ' . e($user['username']) . ')'; } else { $subpage = 'Research Log (no village)'; } break; } } // ─── SECURITY HEADERS ───────────────────────────────────────────────────────── // Send headers before ANY output. These protect against common web attacks. // Intentionally NOT using header_remove() to avoid stripping headers set by // other TravianZ bootstrap code — we only add, never remove. if (!headers_sent()) { header('X-Frame-Options: DENY'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: strict-origin-when-cross-origin'); header("Content-Security-Policy: default-src 'self'; " . "script-src 'self' 'unsafe-inline' https://ajax.googleapis.com; " . "style-src 'self' 'unsafe-inline'; " . "img-src 'self' data:; " . "font-src 'self'; " . "connect-src 'self'; " . "frame-ancestors 'none';"); } ?>