From 70c3ff8979b8ddb151245e91bd3dfc6df1f72a50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 23:03:03 +0000 Subject: [PATCH] fix: billing game images - dropdown selector, auto-guess, proper URL resolution Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/28b4019a-734d-418e-8002-8c1ff0c0f564 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- modules/billing/adminserverlist.php | 158 ++++++++++++++++++++++++++-- modules/billing/bootstrap.php | 31 ++++++ modules/billing/order.php | 4 +- modules/billing/serverlist.php | 14 ++- 4 files changed, 193 insertions(+), 14 deletions(-) diff --git a/modules/billing/adminserverlist.php b/modules/billing/adminserverlist.php index 860bd438..46b73f49 100644 --- a/modules/billing/adminserverlist.php +++ b/modules/billing/adminserverlist.php @@ -15,6 +15,8 @@ .slot-input { width: 60px; } .desc-input { width: 160px; } .img-input { width: 160px; } + .img-select { max-width: 180px; } + .img-fallback { display: none; max-width: 180px; margin-top: 4px; } .muted { color: #999; font-size: 0.85em; } .flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; color: #155724; } .flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; color: #721c24; } @@ -56,6 +58,94 @@ function h(mixed $s): string return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } +/** + * Return a sorted list of image filenames available in /images/games/. + * Only files with recognised image extensions are included. + */ +function list_game_images(): array +{ + $dir = __DIR__ . '/../../images/games'; + if (!is_dir($dir)) { + return []; + } + $exts = ['jpg', 'jpeg', 'png', 'webp', 'gif']; + $files = []; + foreach (scandir($dir) as $f) { + if ($f === '.' || $f === '..') continue; + $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION)); + if (in_array($ext, $exts, true)) { + $files[] = $f; + } + } + natcasesort($files); + return array_values($files); +} + +/** + * Normalize a game name or filename stem so that platform/architecture + * suffixes are stripped before comparison. + * + * Examples: + * "7 Days to Die linux64" → "7daystodie" + * "arma3_win64" → "arma3" + * "dayz_epoch_mod_win32" → "dayzepochmod" + */ +function normalize_game_name(string $name): string +{ + $name = strtolower($name); + // Strip extension if present + $name = preg_replace('/\.[a-z]{2,4}$/', '', $name); + // Strip common platform/arch suffixes (as whole words or underscore-delimited tokens) + $name = preg_replace('/[\s_\-]*(linux64|linux32|linux|win64|win32|windows|win|x64|x86|32|64)/', '', $name); + // Remove punctuation, spaces and underscores + $name = preg_replace('/[^a-z0-9]/', '', $name); + return $name; +} + +/** + * Given a game name (from config_homes.game_name or home_cfg_file), try to find + * a matching image filename from the list of available game images. + * Returns the filename (e.g. "arma_3.jpg") or '' if nothing suitable is found. + */ +function guess_game_image(string $gameName, string $cfgFile, array $availableImages): string +{ + if (empty($availableImages)) { + return ''; + } + + // Build a normalised→filename map for available images + $normMap = []; + foreach ($availableImages as $imgFile) { + $stem = pathinfo($imgFile, PATHINFO_FILENAME); + $key = normalize_game_name($stem); + if ($key !== '') { + // Keep the first match for duplicate normalised keys + $normMap[$key] = $normMap[$key] ?? $imgFile; + } + } + + // Candidates to try, in priority order: game display name, then cfg file stem + $candidates = [$gameName]; + if ($cfgFile !== '') { + $candidates[] = pathinfo($cfgFile, PATHINFO_FILENAME); + } + + foreach ($candidates as $candidate) { + $key = normalize_game_name($candidate); + if ($key !== '' && isset($normMap[$key])) { + return $normMap[$key]; + } + // Also try prefix matching: game "dayz epoch" → find "dayz_epochmod" + foreach ($normMap as $normImgKey => $imgFile) { + if (str_starts_with($normImgKey, $key) || str_starts_with($key, $normImgKey)) { + return $imgFile; + } + } + } + + return ''; +} + $db = billing_get_db(); if (!($db instanceof mysqli)) { die("Database connection failed."); @@ -142,11 +232,15 @@ function sync_billing_services(mysqli $db, string $prefix): array // Insert a new row for every config_homes entry not yet in billing_services. // Admin-editable fields (prices, slots, enabled, etc.) get safe defaults so // the service is visible to the admin but not yet live in the store. + $availableImages = list_game_images(); foreach ($configHomes as $homeCfgId => $ch) { if (isset($existing[$homeCfgId])) { continue; } $svcName = $db->real_escape_string($ch['game_name']); + $guessedImg = $db->real_escape_string( + guess_game_image((string)$ch['game_name'], (string)($ch['home_cfg_file'] ?? ''), $availableImages) + ); $db->query( "INSERT INTO `{$tableName}` (home_cfg_id, mod_cfg_id, service_name, description, @@ -159,9 +253,13 @@ function sync_billing_services(mysqli $db, string $prefix): array '', 0, 0.00, 0.00, 0.00, 1, 100, - '', '', 'steamcmd', '', '')" + '{$guessedImg}', '', 'steamcmd', '', '')" ); - $messages[] = "Added new service: " . $ch['game_name']; + $msg = "Added new service: " . $ch['game_name']; + if ($guessedImg !== '') { + $msg .= " (image auto-set: {$guessedImg})"; + } + $messages[] = $msg; } // Soft-disable billing_services whose home_cfg_id no longer appears in config_homes. @@ -214,7 +312,14 @@ if (isset($_POST['save_services'])) { $slotMax = max(1, (int)($svcData['slot_max_qty'] ?? 1)); if ($slotMax < $slotMin) { $slotMax = $slotMin; } $description = $db->real_escape_string(substr((string)($svcData['description'] ?? ''), 0, 1000)); - $imgUrl = $db->real_escape_string(substr((string)($svcData['img_url'] ?? ''), 0, 255)); + // Merge dropdown and fallback text input: + // - dropdown value "__other__" means use the text fallback field + // - otherwise use the dropdown value (bare filename or '') + $rawImgUrl = (string)($svcData['img_url'] ?? ''); + if ($rawImgUrl === '__other__') { + $rawImgUrl = (string)($svcData['img_url_other'] ?? ''); + } + $imgUrl = $db->real_escape_string(substr($rawImgUrl, 0, 255)); // Build comma-separated remote_server_id from checkboxes, validating each ID $checkedIds = []; @@ -306,12 +411,14 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) { Price / Month ($) Price / Year ($) Description - Image URL + Image Available Servers - fetch_assoc())) { - + + + + diff --git a/modules/billing/bootstrap.php b/modules/billing/bootstrap.php index 49c9b637..3f13eff9 100644 --- a/modules/billing/bootstrap.php +++ b/modules/billing/bootstrap.php @@ -108,4 +108,35 @@ if (!isset($db) || !($db instanceof mysqli)) { } } +/** + * Resolve a billing_services.img_url value to a browser-safe URL. + * + * Rules: + * - Empty string → '' (caller should skip the tag). + * - Full URL (http:// or https://) → returned as-is. + * - Bare filename (e.g. "dayz.jpg") → "/images/games/{filename}". + * - Anything else treated as a bare filename for safety. + * + * Output is NOT htmlspecialchars'd here; callers must escape for HTML context. + */ +if (!function_exists('billing_image_url')) { + function billing_image_url(string $imgUrl): string + { + $imgUrl = trim($imgUrl); + if ($imgUrl === '') { + return ''; + } + // Keep full external URLs intact + if (str_starts_with($imgUrl, 'http://') || str_starts_with($imgUrl, 'https://')) { + return $imgUrl; + } + // Strip any leading path separators/directories so we always get a bare filename + $filename = basename($imgUrl); + if ($filename === '') { + return ''; + } + return '/images/games/' . $filename; + } +} + // End bootstrap diff --git a/modules/billing/order.php b/modules/billing/order.php index fabaa410..c5db8340 100644 --- a/modules/billing/order.php +++ b/modules/billing/order.php @@ -106,7 +106,7 @@ THIS IS WHAT WE DISPLAY ON THE SHOP PAGE AT THE TOP - +

@@ -143,7 +143,7 @@ if ($row['price_monthly'] == 0.0) { ?>
- +
-
-
+ + +
+ +
@@ -81,8 +84,11 @@ include(__DIR__ . '/includes/menu.php');
-
-
+ + +
+ +