Panel/modules/billing/adminserverlist.php
copilot-swe-agent[bot] bb77620796
fix: resolve 3 billing admin errors and normalize remote_servers query
- admin_config.php: guard session_start() — was firing Notice because
  admin_auth.php → session_bridge.php already started the session
- includes/menu.php: check mysqli_thread_id() before reusing $db so a
  closed handle does not cause 'mysqli object is already closed' fatal
- admin_invoices.php / admin_payments.php: set $db = null after
  mysqli_close() so menu.php's reuse-check correctly falls through to
  opening a fresh connection
- adminserverlist.php: use col_exists() to detect missing 'enabled'
  column in gsp_remote_servers; fall back to constant 1 and display a
  schema-notice banner; guard UPDATE accordingly; also add missing
  price_daily / price_year columns to the services SELECT; remove
  duplicate 'Update Enabled Servers' button
- add_remote_server_enabled_column.sql: idempotent migration to add the
  'enabled' INT column to gsp_remote_servers on older installs

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/988997ed-7568-48bf-96ef-889fb5d91fec

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
2026-05-02 13:40:10 +00:00

383 lines
15 KiB
PHP

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Server List - GameServers.World</title>
</head>
<body>
<?php
// gameservers.world admin — mysqli only, bulk + per-row update, image base URL + small button
/* === SITE_BASE_URL is loaded from includes/config.inc.php; leave empty to use relative paths === */
// Include billing bootstrap (loads config and DB helper)
require_once(__DIR__ . '/bootstrap.php');
$siteBaseUrl = isset($SITE_BASE_URL) ? trim((string)$SITE_BASE_URL) : '';
// Protect this page: require admin
require_once(__DIR__ . '/includes/admin_auth.php');
// Create database connection
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$db) {
die("Connection failed: " . mysqli_connect_error());
}
// Include top bar and menu
include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php');
echo "<div class='panel mb-12'><strong>Need the XML field reference?</strong> ";
echo "<a href=\"docs/xml_notes.php\" target=\"_blank\" rel=\"noopener\">Open XML Notes</a>";
echo "</div>";
/* show errors during setup */
@ini_set('display_errors','1');
error_reporting(E_ALL);
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function esc_mysqli($db, $v){ return $db->real_escape_string($v); }
function fetch_all_assoc($db, $sql){
$res = $db->query($sql);
return $res ? $res->fetch_all(MYSQLI_ASSOC) : [];
}
function col_exists($db, $table, $col){
$res = $db->query("SHOW COLUMNS FROM `$table` LIKE '".$db->real_escape_string($col)."'");
return ($res && $res->num_rows > 0);
}
function parse_id_list($s){
$tokens = preg_split('/\s+/', trim((string)$s));
$out = [];
foreach ((array)$tokens as $t) {
if ($t === '') continue;
if (preg_match('/^\d+$/', $t)) $out[] = (int)$t;
}
return array_values(array_unique($out));
}
/* URL helpers for image preview */
function is_abs_url($u){ return (bool)preg_match('~^(?:https?:)?//|^data:~i', (string)$u); }
function join_base($base, $path){
$base = rtrim((string)$base, '/');
$path = ltrim((string)$path, '/');
return $base !== '' ? $base.'/'.$path : $path;
}
/* which column holds space-separated locations */
$locationCol = col_exists($db, "{$table_prefix}billing_services", 'remote_server_id') ? 'remote_server_id' :
(col_exists($db, "{$table_prefix}billing_services", 'remote_server') ? 'remote_server' : 'remote_server_id');
/* whether gsp_remote_servers has an 'enabled' column (may be missing on older installs) */
$rsHasEnabled = col_exists($db, "{$table_prefix}remote_servers", 'enabled');
$flash = [];
/* A) Update global server location enable flags */
if (isset($_POST['update_remote_servers'])) {
$enabledIds = array_map('intval', $_POST['rs'] ?? []);
$enabledSet = array_flip($enabledIds);
$allIds = fetch_all_assoc($db, "SELECT remote_server_id FROM {$table_prefix}remote_servers");
foreach ((array)$allIds as $row) {
$id = (int)$row['remote_server_id'];
$e = isset($enabledSet[$id]) ? 1 : 0;
if ($rsHasEnabled) {
$db->query("UPDATE {$table_prefix}remote_servers SET enabled={$e} WHERE remote_server_id={$id}");
}
}
$flash[] = $rsHasEnabled ? "Server locations updated." : "Server locations updated (note: 'enabled' column missing from remote_servers — run add_remote_server_enabled_column.sql migration).";
}
/* helper: update one service row from posted array */
function update_service_row(mysqli $db, string $locationCol, int $sid, array $svc){
$name = esc_mysqli($db, trim($svc['service_name'] ?? ''));
$priceMonthly = number_format((float)($svc['price_monthly'] ?? 0), 2, '.', '');
$priceYearly = number_format((float)($svc['price_year'] ?? 0), 2, '.', '');
$priceDaily = number_format((float)($svc['price_daily'] ?? 0), 2, '.', '');
$priceMonthEsc = esc_mysqli($db, $priceMonthly);
$priceYearEsc = esc_mysqli($db, $priceYearly);
$priceDailyEsc = esc_mysqli($db, $priceDaily);
$img = esc_mysqli($db, trim($svc['img_url'] ?? ''));
$en = !empty($svc['enabled']) ? 1 : 0;
$minSlots = max(1, (int)($svc['slot_min_qty'] ?? 1));
$maxSlots = max($minSlots, (int)($svc['slot_max_qty'] ?? $minSlots));
$selected = [];
if (!empty($svc['locations']) && is_array($svc['locations'])) {
$selected = array_map('intval', $svc['locations']);
$selected = array_values(array_unique($selected));
}
$primary = isset($svc['primary_location']) ? (int)$svc['primary_location'] : 0;
if ($primary && in_array($primary, $selected, true)) {
$selected = array_values(array_diff($selected, [$primary]));
array_unshift($selected, $primary);
}
$locList = implode(' ', $selected);
$locListEsc = esc_mysqli($db, $locList);
$sql = "UPDATE {$table_prefix}billing_services
SET service_name='{$name}',
`{$locationCol}`='{$locListEsc}',
slot_min_qty={$minSlots},
slot_max_qty={$maxSlots},
price_daily='{$priceDailyEsc}',
price_monthly='{$priceMonthEsc}',
price_year='{$priceYearEsc}',
img_url='{$img}',
enabled={$en}
WHERE service_id={$sid}";
$db->query($sql);
}
/* B1) PER-ROW UPDATE */
if (isset($_POST['update_single']) && isset($_POST['service']) && is_array($_POST['service'])) {
$sid = (int)$_POST['update_single'];
if (isset($_POST['service'][$sid])) {
update_service_row($db, $locationCol, $sid, $_POST['service'][$sid]);
$flash[] = "Service #{$sid} updated.";
}
}
/* B2) BULK UPDATE (single button at bottom) */
if (isset($_POST['bulk_update']) && !empty($_POST['service']) && is_array($_POST['service'])) {
foreach ((array)$_POST['service'] as $sid => $svc) {
update_service_row($db, $locationCol, (int)$sid, (array)$svc);
}
$flash[] = "All edited services have been updated.";
}
/* C) Remove a service (separate small form) */
if (isset($_POST['remove_service'], $_POST['service_id_remove'])) {
$sid = (int)$_POST['service_id_remove'];
$db->query("DELETE FROM {$table_prefix}billing_services WHERE service_id={$sid}");
$flash[] = "Service #{$sid} removed.";
}
/* fetch data for UI */
$rsEnabledExpr = $rsHasEnabled ? ', enabled' : ', 1 AS enabled';
$remoteServers = fetch_all_assoc($db, "SELECT remote_server_id, remote_server_name{$rsEnabledExpr} FROM {$table_prefix}remote_servers ORDER BY remote_server_name");
$services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locationCol}` AS locs, slot_min_qty, slot_max_qty, price_daily, price_monthly, price_year, img_url, enabled FROM {$table_prefix}billing_services ORDER BY service_name");
?>
<?php if ($flash): ?>
<div class="panel" style="margin-bottom:12px"><?php foreach ((array)$flash as $m) echo "<div>".h($m)."</div>"; ?></div>
<?php endif; ?>
<?php if (!$rsHasEnabled): ?>
<div class="panel" style="margin-bottom:12px;background:#fff3cd;border:1px solid #ffc107;">
<strong>⚠ Schema notice:</strong> The <code><?php echo h("{$table_prefix}remote_servers"); ?></code> table is missing the <code>enabled</code> column.
Server location enable/disable is currently non-functional.
Run <code>modules/billing/add_remote_server_enabled_column.sql</code> to add the column.
</div>
<?php endif; ?>
<h2>Enable/Disable Server Locations (Global)</h2>
<form method="post" action="">
<input type="hidden" name="update_remote_servers" value="1">
<div style="display:flex;flex-wrap:wrap;gap:10px;">
<?php foreach ((array)$remoteServers as $rs): ?>
<label class="loc-label min-w-240">
<input type="checkbox" name="rs[]" value="<?php echo (int)$rs['remote_server_id']; ?>" <?php echo ((int)$rs['enabled']===1?'checked':''); ?>>
<b><?php echo h($rs['remote_server_name']); ?></b>
<small class="muted">(ID: <?php echo (int)$rs['remote_server_id']; ?>)</small>
</label>
<?php endforeach; ?>
</div>
<div style="margin-top:10px;"><button type="submit">Update Enabled Servers</button></div>
</form>
<hr>
<h2>Current Services</h2>
<?php if (!$services): ?>
<p>No services found.</p>
<?php else: ?>
<!-- SINGLE BULK FORM FOR ALL SERVICES -->
<form method="post" action="">
<table class="center" style="text-align:center;width:100%;border-collapse:collapse;">
<thead>
<tr>
<th>Enabled</th>
<th>Service Name <small class="muted">(ID below)</small></th>
<th>Min Slots</th>
<th>Max Slots</th>
<th>Price (Daily)</th>
<th>Price (Monthly)</th>
<th>Price (Year)</th>
<th>Thumbnail URL</th>
<th>Preview</th>
<th>Update Row</th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$services as $row): ?>
<?php
$sid = (int)$row['service_id'];
$selected = parse_id_list($row['locs'] ?? '');
$primary = $selected[0] ?? 0; // first ID is "primary"
$selSet = array_flip($selected);
$imgUrl = trim((string)$row['img_url']);
$displayUrl = '';
if ($imgUrl !== '') {
if (is_abs_url($imgUrl)) {
$displayUrl = $imgUrl;
} elseif ($siteBaseUrl !== '') {
$displayUrl = join_base($siteBaseUrl, $imgUrl);
} else {
// Use relative path (local folder)
$displayUrl = $imgUrl;
}
}
?>
<!-- MAIN ROW (no bottom border) -->
<tr>
<!-- Enabled first -->
<td>
<input type="hidden" name="service[<?php echo $sid; ?>][enabled]" value="0">
<input type="checkbox" name="service[<?php echo $sid; ?>][enabled]" value="1" <?php echo ((int)$row['enabled']===1?'checked':''); ?>>
</td>
<!-- Service name (with tiny ID under it) -->
<td>
<input type="text" name="service[<?php echo $sid; ?>][service_name]" value="<?php echo h($row['service_name']); ?>" class="min-w-260">
<div class="small-muted">ID: <?php echo $sid; ?></div>
</td>
<td>
<input type="number" name="service[<?php echo $sid; ?>][slot_min_qty]" value="<?php echo (int)$row['slot_min_qty']; ?>" min="1" step="1" class="w-90">
</td>
<td>
<input type="number" name="service[<?php echo $sid; ?>][slot_max_qty]" value="<?php echo (int)$row['slot_max_qty']; ?>" min="1" step="1" class="w-90">
</td>
<td>
<input type="text" name="service[<?php echo $sid; ?>][price_daily]" value="<?php echo h(number_format((float)$row['price_daily'], 2, '.', '')); ?>" size="8">
</td>
<td>
<input type="text" name="service[<?php echo $sid; ?>][price_monthly]" value="<?php echo h($row['price_monthly']); ?>" size="8">
</td>
<td>
<input type="text" name="service[<?php echo $sid; ?>][price_year]" value="<?php echo h(number_format((float)$row['price_year'], 2, '.', '')); ?>" size="8">
</td>
<!-- Thumbnail URL input -->
<td>
<input type="text" name="service[<?php echo $sid; ?>][img_url]" value="<?php echo h($row['img_url']); ?>" class="min-w-240">
</td>
<!-- Preview (uses BASE + relative path) -->
<td>
<?php if ($displayUrl !== ''): ?>
<img src="<?php echo h($displayUrl); ?>" alt="preview" loading="lazy" class="img-preview" onerror="this.style.display='none'">
<?php else: ?>
<span class="muted">(no image)</span>
<?php endif; ?>
</td>
<!-- Per-row Update (smaller) -->
<td>
<button type="submit" name="update_single" value="<?php echo $sid; ?>" class="btn-small">Update Row</button>
</td>
</tr>
<!-- LOCATIONS ROW (single bottom divider) -->
<tr>
<td colspan="8" style="border-bottom:1px solid #f0f0f0; padding:8px 6px; text-align:left;">
<div class="locs-box" data-sid="<?php echo $sid; ?>" style="display:flex; flex-wrap:wrap; gap:8px;">
<?php foreach ((array)$remoteServers as $rs): ?>
<?php
$rid = (int)$rs['remote_server_id'];
$isChecked = isset($selSet[$rid]);
$isPrimary = ($primary === $rid);
?>
<label class="loc-label">
<input type="checkbox" class="locchk" data-sid="<?php echo $sid; ?>"
name="service[<?php echo $sid; ?>][locations][]" value="<?php echo $rid; ?>"
<?php echo $isChecked ? 'checked' : ''; ?> class="mr-6">
<?php echo h($rs['remote_server_name']); ?> (<?php echo $rid; ?>)
<span style="margin-left:10px;">
<input type="radio" class="locprim" data-sid="<?php echo $sid; ?>"
name="service[<?php echo $sid; ?>][primary_location]" value="<?php echo $rid; ?>"
<?php echo $isPrimary ? 'checked' : ''; ?> <?php echo $isChecked ? '' : 'disabled'; ?>>
<small>Primary</small>
</span>
<?php if ((int)$rs['enabled'] === 0): ?>
<small class="text-danger ml-8">[Globally disabled]</small>
<?php endif; ?>
</label>
<?php endforeach; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div style="margin-top:14px; text-align:right;">
<button type="submit" name="bulk_update" value="1">Update All</button>
</div>
</form>
<h3 style="margin-top:20px;">Remove a Service</h3>
<form method="post" action="" style="display:flex;gap:8px;align-items:center;">
<input type="hidden" name="remove_service" value="1">
<select name="service_id_remove">
<?php foreach ((array)$services as $s): ?>
<option value="<?php echo (int)$s['service_id']; ?>">
<?php echo h($s['service_name']); ?> (ID: <?php echo (int)$s['service_id']; ?>)
</option>
<?php endforeach; ?>
</select>
<button type="submit" onclick="return confirm('Remove this service? This cannot be undone.')">Remove</button>
</form>
<?php endif; ?>
<div class="panel" style="margin-top:20px;">
<h3>Environment</h3>
<table class="cart-table">
<tr><th>Site Base URL</th><td><?php echo $siteBaseUrl !== '' ? h($siteBaseUrl) : '(empty — using relative paths)'; ?></td></tr>
<tr><th>Data directory</th><td><?php echo isset($SITE_DATA_DIR) ? h($SITE_DATA_DIR) : '(unset)'; ?></td></tr>
<tr><th>PHP SAPI</th><td><?php echo h(PHP_SAPI); ?></td></tr>
<tr><th>Writable?</th><td><?php echo (isset($SITE_DATA_DIR) && is_writable($SITE_DATA_DIR)) ? 'yes' : 'no'; ?></td></tr>
<tr><th>XML Reference</th><td><a href="/modules/billing/docs/xml_notes.php" target="_blank" rel="noopener">Open XML Notes</a></td></tr>
</table>
</div>
<!-- JS: Per-row: enable/disable Primary radios based on whether that location is checked -->
<script>
document.querySelectorAll('.locs-box').forEach(function(box){
const sid = box.getAttribute('data-sid');
const checks = box.querySelectorAll('input.locchk[data-sid="'+sid+'"]');
function refreshRadios() {
checks.forEach(function(chk){
const rid = chk.value;
const rad = box.querySelector('input.locprim[data-sid="'+sid+'"][value="'+rid+'"]');
if (!rad) return;
if (chk.checked) {
rad.disabled = false;
} else {
if (rad.checked) rad.checked = false;
rad.disabled = true;
}
});
}
checks.forEach(chk => chk.addEventListener('change', refreshRadios));
refreshRadios();
});
</script>
<?php
// Close database connection safely
billing_maybe_close_db($db);
?>
</body>
</html>