From 5fae4a2dd559dcbe55ec4de1641aade3cc41e8ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 22:06:10 +0000 Subject: [PATCH 1/3] feat: reuse gamemanager update logic and enforce monthly billing pricing Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/f982c3a1-c9ae-4c5b-9fb6-2941d0e5b7c1 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- modules/billing/add_to_cart.php | 38 +---- modules/billing/adminserverlist.php | 41 +----- modules/billing/api/capture_order.php | 14 +- modules/billing/checkout_free.php | 14 +- modules/billing/classes/BillingService.php | 28 +--- modules/billing/create_servers.php | 154 +++------------------ modules/billing/payment_success.php | 2 +- modules/gamemanager/update_actions.php | 131 ++++++++++++++++++ modules/gamemanager/update_server.php | 119 +++------------- modules/litefm/fm_dir.php | 26 +++- 10 files changed, 212 insertions(+), 355 deletions(-) create mode 100644 modules/gamemanager/update_actions.php diff --git a/modules/billing/add_to_cart.php b/modules/billing/add_to_cart.php index 83606b46..4f36f8b4 100644 --- a/modules/billing/add_to_cart.php +++ b/modules/billing/add_to_cart.php @@ -29,19 +29,7 @@ function billing_generate_password(int $bytes = 12): string function billing_normalize_duration(string $duration): array { - $duration = strtolower(trim($duration)); - switch ($duration) { - case 'day': - case 'daily': - return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1]; - case 'year': - case 'yearly': - return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365]; - case 'month': - case 'monthly': - default: - return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31]; - } + return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31]; } function billing_money_to_cents(float $amount): int @@ -60,23 +48,17 @@ function billing_rate_from_service(mysqli $db, string $table_prefix, int $servic return 0.0; } - $stmt = $db->prepare("SELECT price_daily, price_monthly, price_year FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1"); + $stmt = $db->prepare("SELECT price_monthly FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1"); if (!$stmt) { return 0.0; } $stmt->bind_param('i', $service_id); $stmt->execute(); - $stmt->bind_result($price_daily, $price_monthly, $price_year); + $stmt->bind_result($price_monthly); $rate = 0.0; if ($stmt->fetch()) { - if ($rate_type === 'daily') { - $rate = floatval($price_daily); - } elseif ($rate_type === 'yearly') { - $rate = floatval($price_year); - } else { - $rate = floatval($price_monthly); - } + $rate = floatval($price_monthly); } $stmt->close(); @@ -179,19 +161,13 @@ $slot_min_qty = 1; $slot_max_qty = 1; $durationInfo = billing_normalize_duration($invoice_duration); if ($service_id > 0) { - $stmt = $db->prepare("SELECT service_name, price_daily, price_monthly, price_year, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1"); + $stmt = $db->prepare("SELECT service_name, price_monthly, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1"); if ($stmt) { $stmt->bind_param('i', $service_id); $stmt->execute(); - $stmt->bind_result($service_name, $price_daily, $price_monthly, $price_year, $slot_min_qty, $slot_max_qty); + $stmt->bind_result($service_name, $price_monthly, $slot_min_qty, $slot_max_qty); if ($stmt->fetch()) { - if ($durationInfo['rate_type'] === 'daily') { - $base_rate = floatval($price_daily); - } elseif ($durationInfo['rate_type'] === 'yearly') { - $base_rate = floatval($price_year); - } else { - $base_rate = floatval($price_monthly); - } + $base_rate = floatval($price_monthly); // constrain slots if ($max_players < $slot_min_qty) $max_players = $slot_min_qty; if ($max_players > $slot_max_qty) $max_players = $slot_max_qty; diff --git a/modules/billing/adminserverlist.php b/modules/billing/adminserverlist.php index b22367c3..a0bca5a9 100644 --- a/modules/billing/adminserverlist.php +++ b/modules/billing/adminserverlist.php @@ -21,8 +21,8 @@ .muted { color: #999; font-size: 0.85em; } .flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #155724; } .flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #721c24; } - .servers-cell { text-align: left; } - .server-cb-label { display: block; white-space: nowrap; margin: 2px 0; } + .servers-cell { text-align: left; min-width: 240px; max-width: 280px; } + .server-cb-label { display: block; white-space: normal; margin: 2px 0; } .action-cell { text-align: center; min-width: 120px; } .btn-row-save, .btn-save-all { border: 1px solid #3e7ab8; @@ -65,7 +65,7 @@ * * Columns that are admin-editable and NEVER overwritten by sync: * enabled, slot_min_qty, slot_max_qty, - * price_daily, price_monthly, price_year, + * price_monthly, * remote_server_id, description, img_url */ @@ -310,7 +310,7 @@ $flashType = 'ok'; $sort = strtolower((string)($_GET['sort'] ?? $_POST['sort'] ?? 'game')); $dir = strtolower((string)($_GET['dir'] ?? $_POST['dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc'; $gameMode = strtolower((string)($_GET['game_mode'] ?? $_POST['game_mode'] ?? 'name')); -if (!in_array($sort, ['game', 'config', 'enabled', 'day', 'month', 'year', 'servers'], true)) { +if (!in_array($sort, ['game', 'config', 'enabled', 'month', 'servers'], true)) { $sort = 'game'; } if (!in_array($gameMode, ['name', 'enabled'], true)) { @@ -367,9 +367,7 @@ if (isset($_POST['save_services']) || isset($_POST['save_row'])) { continue; } $enabled = isset($svcData['enabled']) ? 1 : 0; - $priceDaily = number_format((float)($svcData['price_daily'] ?? 0), 2, '.', ''); $priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 2, '.', ''); - $priceYear = number_format((float)($svcData['price_year'] ?? 0), 2, '.', ''); $slotMin = max(1, (int)($svcData['slot_min_qty'] ?? 1)); $slotMax = max(1, (int)($svcData['slot_max_qty'] ?? 1)); if ($slotMax < $slotMin) { $slotMax = $slotMin; } @@ -396,9 +394,7 @@ if (isset($_POST['save_services']) || isset($_POST['save_row'])) { $ok = $db->query( "UPDATE `{$table_prefix}billing_services` SET enabled = {$enabled}, - price_daily = '{$priceDaily}', price_monthly = '{$priceMonthly}', - price_year = '{$priceYear}', slot_min_qty = {$slotMin}, slot_max_qty = {$slotMax}, description = '{$description}', @@ -453,7 +449,7 @@ while ($rsRes && ($row = $rsRes->fetch_assoc())) { $services = []; $svcRes = $db->query( "SELECT bs.service_id, bs.service_name, bs.enabled, - bs.price_daily, bs.price_monthly, bs.price_year, + bs.price_monthly, bs.slot_min_qty, bs.slot_max_qty, bs.remote_server_id, bs.description, bs.img_url, ch.home_cfg_file @@ -474,15 +470,9 @@ if (!empty($services)) { case 'enabled': $cmp = ((int)($a['enabled'] ?? 0)) <=> ((int)($b['enabled'] ?? 0)); break; - case 'day': - $cmp = ((float)($a['price_daily'] ?? 0)) <=> ((float)($b['price_daily'] ?? 0)); - break; case 'month': $cmp = ((float)($a['price_monthly'] ?? 0)) <=> ((float)($b['price_monthly'] ?? 0)); break; - case 'year': - $cmp = ((float)($a['price_year'] ?? 0)) <=> ((float)($b['price_year'] ?? 0)); - break; case 'servers': $countA = trim((string)($a['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$a['remote_server_id']), 'strlen')); $countB = trim((string)($b['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$b['remote_server_id']), 'strlen')); @@ -549,18 +539,10 @@ if (!empty($services)) { Min Slots Max Slots - - - Price / Day ($) - Price / Month ($) - - - Price / Year ($) - Description Image @@ -615,24 +597,12 @@ if (!empty($services)) { value=""> - - - - - - - -
  • A service will only appear in the store when Enabled is checked and at least one server is selected.
  • +
  • Price / Month ($) is the canonical billing price used by cart, checkout, and provisioning.
  • The Game Name and Config XML columns are sourced from and are read-only here. To change them, update the game XML config in the panel.
  • diff --git a/modules/billing/api/capture_order.php b/modules/billing/api/capture_order.php index 1d5f6a68..a0f6b2cd 100644 --- a/modules/billing/api/capture_order.php +++ b/modules/billing/api/capture_order.php @@ -124,19 +124,7 @@ function cap_invoice_ids_from_custom_id(?string $customId): array { } function cap_get_duration_metadata(array $invoice): array { - $duration = strtolower((string)($invoice['invoice_duration'] ?? $invoice['rate_type'] ?? 'month')); - switch ($duration) { - case 'day': - case 'daily': - return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1]; - case 'year': - case 'yearly': - return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365]; - case 'month': - case 'monthly': - default: - return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31]; - } + return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31]; } function cap_get_end_date(array $invoice, ?string $fromDate = null): string { diff --git a/modules/billing/checkout_free.php b/modules/billing/checkout_free.php index 6c9b8f03..b5453fbd 100644 --- a/modules/billing/checkout_free.php +++ b/modules/billing/checkout_free.php @@ -111,19 +111,7 @@ require_once __DIR__ . '/classes/BillingService.php'; $repo = new BillingRepository($mysqli, $table_prefix); $newOrderIds = []; $duration_meta = static function (array $invoice): array { - $duration = strtolower((string)($invoice['invoice_duration'] ?? $invoice['rate_type'] ?? 'month')); - switch ($duration) { - case 'day': - case 'daily': - return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1]; - case 'year': - case 'yearly': - return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365]; - case 'month': - case 'monthly': - default: - return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31]; - } + return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31]; }; foreach ($invoices as $inv) { diff --git a/modules/billing/classes/BillingService.php b/modules/billing/classes/BillingService.php index eeee8aac..bca672ad 100644 --- a/modules/billing/classes/BillingService.php +++ b/modules/billing/classes/BillingService.php @@ -33,23 +33,9 @@ class BillingService { $qty = max(1, $qty); $players = max(1, $players); - - switch ($rateType) { - case 'daily': - $basePrice = (float)($service['price_daily'] ?? 0); - $periodDays = $qty; - break; - case 'yearly': - $basePrice = (float)($service['price_year'] ?? 0); - $periodDays = $qty * 365; - break; - case 'monthly': - default: - $rateType = 'monthly'; - $basePrice = (float)($service['price_monthly'] ?? 0); - $periodDays = $qty * 31; - break; - } + $rateType = 'monthly'; + $basePrice = (float)($service['price_monthly'] ?? 0); + $periodDays = $qty * 31; // price_monthly etc is the per-player per-period rate $ratePerPlayer = $basePrice; @@ -163,9 +149,7 @@ class BillingService $periodEnd = $invoiceRow['period_end'] ?? null; if (!$periodEnd) { - $rateType = $invoiceRow['rate_type'] ?? 'monthly'; - $periodMap = ['daily' => '+1 day', 'monthly' => '+31 days', 'yearly' => '+365 days']; - $periodEnd = date('Y-m-d H:i:s', strtotime($periodMap[$rateType] ?? '+31 days')); + $periodEnd = date('Y-m-d H:i:s', strtotime('+31 days')); } // If current expiry is in the future, extend from it; otherwise reset from period_end @@ -177,9 +161,7 @@ class BillingService if ($periodStart && $periodEndVal) { $currentPeriodSecs = strtotime($periodEndVal) - strtotime($periodStart); } else { - $rateType2 = $invoiceRow['rate_type'] ?? 'monthly'; - $periodSecMap = ['daily' => 86400, 'monthly' => 31 * 86400, 'yearly' => 365 * 86400]; - $currentPeriodSecs = $periodSecMap[$rateType2] ?? (31 * 86400); + $currentPeriodSecs = 31 * 86400; } $newExpiry = date('Y-m-d H:i:s', strtotime($currentExpiry) + max(86400, $currentPeriodSecs)); } else { diff --git a/modules/billing/create_servers.php b/modules/billing/create_servers.php index f304d505..46c8a8c5 100644 --- a/modules/billing/create_servers.php +++ b/modules/billing/create_servers.php @@ -1,6 +1,7 @@ mods->mod[0])) { - $mod_xml = $server_xml->mods->mod[0]; - $modkey = (string)$mod_xml['key']; - } - if ($mod_xml === false) { - $order_failed = true; - $order_failure_reason = "No installable mod profile exists in XML for home #{$home_id}."; - } else { - $installer_name = (string)$mod_xml->installer_name; - $resolved_mod_cfg_id = intval($selected_mod['mod_cfg_id'] ?? $resolved_mod_cfg_id); - } - } - } - - //Get Preinstall commands from xml - $precmd = !$order_failed ? $server_xml->pre_install : ''; - - - //Get Postinstall commands from xml - $postcmd = !$order_failed ? $server_xml->post_install : ''; - - //Enable FTP account in remote server if (!$order_failed && $ftp == "enabled") { $remote->ftp_mgr("useradd", $home_info['home_id'], $home_info['ftp_password'], $home_info['home_path']); $db->changeFtpStatus('enabled',$home_info['home_id']); } - - //Install files for this service in the remote server - if (!$order_failed) { - $exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location ); - $exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name ); - } - if (!$order_failed && (string)$server_xml->installer === "steamcmd" && !empty((string)$installer_name) ) - { - if( preg_match("/win32/", $server_xml->game_key) OR preg_match("/win64/", $server_xml->game_key) ) - $cfg_os = "windows"; - elseif( preg_match("/linux/", $server_xml->game_key) ) - $cfg_os = "linux"; - - // Some games like L4D2 require anonymous login - if(!empty($mod_xml->installer_login)){ - $login = $mod_xml->installer_login; - $pass = ''; - }else{ - $login = $settings['steam_user']; - $pass = $settings['steam_pass']; - } - - $modname = ( $installer_name == '90' and !preg_match("/(cstrike|valve)/", $modkey) ) ? $modkey : ''; - $betaname = isset($mod_xml->betaname) ? $mod_xml->betaname : ''; - $betapwd = isset($mod_xml->betapwd) ? $mod_xml->betapwd : ''; - $arch = isset($mod_xml->steam_bitness) ? $mod_xml->steam_bitness : ''; - - $remote->steam_cmd( $home_id,$home_info['home_path'],$installer_name,$modname, - $betaname,$betapwd,$login,$pass,$settings['steam_guard'], - $exec_folder_path,$exec_path,$precmd,$postcmd,$cfg_os,'',$arch); - } - elseif (!$order_failed) - { - // No SteamCMD installer — run pre/post install scripts only. - if (!empty((string)$precmd)) { - $result = $remote->exec((string)$precmd); - if ($result === NULL) - $db->logger("Script-only install: pre_install script returned no output for home_id $home_id"); - } - if (!empty((string)$postcmd)) { - $result = $remote->exec((string)$postcmd); - if ($result === NULL) - $db->logger("Script-only install: post_install script returned no output for home_id $home_id"); + if (!$order_failed) { + $autoInstall = gamemanager_trigger_update_install( + $db, + $home_info, + intval($mod_id), + array('settings' => $settings) + ); + $mod_id = intval($autoInstall['mod_id'] ?? $mod_id); + if (empty($autoInstall['ok'])) { + $order_failed = true; + $order_failure_reason = "Server files have not been installed yet. " . ($autoInstall['message'] ?? 'Auto install could not be started.'); } } if (!$order_failed) { @@ -529,54 +455,18 @@ function exec_ogp_module() } $end_date_str = date('Y-m-d H:i:s', $existing_end); } - elseif ($order['invoice_duration'] == "day") + else { - - if(empty($order['end_date']) || $order['end_date'] === NULL){ - $end_date = strtotime('+'.$order['qty'].' day'); + $qty_days = max(1, intval($order['qty'])) * 31; + if (empty($order['end_date']) || $order['end_date'] === NULL) { + $end_date = strtotime('+' . $qty_days . ' day'); + } else { + $current_end = strtotime($order['end_date']); + if ($current_end === false) { + $current_end = time(); + } + $end_date = strtotime('+' . $qty_days . ' day', $current_end); } - else{ - //this is a renewel, start from end of previous order - $current_end = strtotime($order['end_date']); - if ($current_end === false) { - $current_end = time(); // fallback to now if date is invalid - } - $end_date = strtotime('+'.$order['qty'].' day', $current_end); - } - - } - elseif ($order['invoice_duration'] == "month") - { - // this is a new order - if(empty($order['end_date']) || $order['end_date'] === NULL){ - $end_date = strtotime('+'.(intval($order['qty']) * 31).' day'); - - } - else{ - //this is a renewel, start from end of previous order - $current_end = strtotime($order['end_date']); - if ($current_end === false) { - $current_end = time(); // fallback to now if date is invalid - } - $end_date = strtotime('+'.(intval($order['qty']) * 31).' day', $current_end); - } - } - elseif ($order['invoice_duration'] == "year") - { - // this is a new order - if(empty($order['end_date']) || $order['end_date'] === NULL){ - $end_date = strtotime('+'.$order['qty'].' year'); - } - else{ - //this is a renewel, start from end of previous order - $current_end = strtotime($order['end_date']); - if ($current_end === false) { - $current_end = time(); // fallback to now if date is invalid - } - $end_date = strtotime('+'.$order['qty'].' year', $current_end); - - } - } if (!isset($end_date_str)) { $end_date_str = date('Y-m-d H:i:s', $end_date); diff --git a/modules/billing/payment_success.php b/modules/billing/payment_success.php index f04de5f4..18ed9dce 100644 --- a/modules/billing/payment_success.php +++ b/modules/billing/payment_success.php @@ -176,7 +176,7 @@ if ($db && $user_id > 0) {

    What Happens Next?

    diff --git a/modules/gamemanager/update_actions.php b/modules/gamemanager/update_actions.php new file mode 100644 index 00000000..d24b779b --- /dev/null +++ b/modules/gamemanager/update_actions.php @@ -0,0 +1,131 @@ + 0 && isset($mods[$preferred_mod_id])) { + return $preferred_mod_id; + } + $keys = array_keys($mods); + return intval(reset($keys)); + } +} + +if (!function_exists('gamemanager_trigger_update_install')) { + function gamemanager_trigger_update_install($db, array $home_info, int $mod_id, array $options = array()): array + { + $home_id = intval($home_info['home_id'] ?? 0); + $mod_id = gamemanager_choose_mod_id($home_info, $mod_id); + if ($home_id <= 0) { + return array('ok' => false, 'pending' => true, 'message' => 'Invalid home_id.', 'mod_id' => $mod_id); + } + if ($mod_id <= 0 || empty($home_info['mods'][$mod_id])) { + return array('ok' => false, 'pending' => true, 'message' => "No mod profile configured for home #{$home_id}.", 'mod_id' => $mod_id); + } + + $server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']); + if (!$server_xml) { + return array('ok' => false, 'pending' => true, 'message' => "Could not read server config XML for home #{$home_id}.", 'mod_id' => $mod_id); + } + + $remote = new OGPRemoteLibrary($home_info['agent_ip'], $home_info['agent_port'], $home_info['encryption_key'], $home_info['timeout']); + if ($remote->status_chk() === 0) { + return array('ok' => false, 'pending' => true, 'message' => 'Agent is offline.', 'mod_id' => $mod_id); + } + if ($remote->is_screen_running(OGP_SCREEN_TYPE_HOME, $home_id) == 1) { + return array('ok' => false, 'pending' => false, 'message' => 'Server is running and cannot be updated.', 'mod_id' => $mod_id); + } + + $log_txt = ''; + $update_active = $remote->get_log(OGP_SCREEN_TYPE_UPDATE, $home_id, clean_path($home_info['home_path']), $log_txt); + if ($update_active == 1) { + return array('ok' => true, 'started' => true, 'already_running' => true, 'message' => 'Update already in progress.', 'mod_id' => $mod_id); + } + + $modkey = $home_info['mods'][$mod_id]['mod_key'] ?? ''; + $mod_xml = xml_get_mod($server_xml, $modkey); + if (!$mod_xml) { + return array('ok' => false, 'pending' => true, 'message' => "Mod key '{$modkey}' not found in XML.", 'mod_id' => $mod_id); + } + + $installer_name = isset($mod_xml->installer_name) ? (string)$mod_xml->installer_name : (string)$modkey; + $precmd = $home_info['mods'][$mod_id]['precmd'] == "" + ? ($home_info['mods'][$mod_id]['def_precmd'] == "" ? $server_xml->pre_install : $home_info['mods'][$mod_id]['def_precmd']) + : $home_info['mods'][$mod_id]['precmd']; + $postcmd = $home_info['mods'][$mod_id]['postcmd'] == "" + ? ($home_info['mods'][$mod_id]['def_postcmd'] == "" ? $server_xml->post_install : $home_info['mods'][$mod_id]['def_precmd']) + : $home_info['mods'][$mod_id]['postcmd']; + $exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location); + $exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name); + + $master_server_home_id = intval($options['master_server_home_id'] ?? 0); + if ($master_server_home_id > 0) { + if ($db->getMasterServer($home_info['remote_server_id'], $home_info['home_cfg_id']) != $master_server_home_id) { + return array('ok' => false, 'pending' => false, 'message' => 'Attempting update from non-master server.', 'mod_id' => $mod_id); + } + if ($master_server_home_id == $home_id) { + return array('ok' => false, 'pending' => false, 'message' => 'Cannot update from own self.', 'mod_id' => $mod_id); + } + $ms_info = $db->getGameHome($master_server_home_id); + $steam_out = $remote->masterServerUpdate($home_id, $home_info['home_path'], $master_server_home_id, $ms_info['home_path'], $exec_folder_path, $exec_path, $precmd, $postcmd); + if ($steam_out === 0) { + return array('ok' => false, 'pending' => true, 'message' => 'Failed to start master update.', 'mod_id' => $mod_id); + } + return array('ok' => true, 'started' => true, 'message' => 'Update started.', 'mod_id' => $mod_id); + } + + $use_steamcmd = ((string)$server_xml->installer === "steamcmd"); + if ($use_steamcmd && !empty((string)$installer_name)) { + $cfg_os = ''; + if (preg_match("/win32/", $server_xml->game_key) || preg_match("/win64/", $server_xml->game_key)) { + $cfg_os = "windows"; + } elseif (preg_match("/linux/", $server_xml->game_key)) { + $cfg_os = "linux"; + } + + $settings = is_array($options['settings'] ?? null) ? $options['settings'] : $db->getSettings(); + if (!empty($mod_xml->installer_login)) { + $login = (string)$mod_xml->installer_login; + $pass = ''; + } else { + $login = (string)($settings['steam_user'] ?? ''); + $pass = (string)($settings['steam_pass'] ?? ''); + } + + $modname = ($installer_name == '90') ? $modkey : ''; + $betaname = isset($mod_xml->betaname) ? (string)$mod_xml->betaname : ''; + $betapwd = isset($mod_xml->betapwd) ? (string)$mod_xml->betapwd : ''; + $arch = isset($mod_xml->steam_bitness) ? (string)$mod_xml->steam_bitness : ''; + $lockFiles = (isset($server_xml->lock_files) && !empty($server_xml->lock_files)) ? trim((string)$server_xml->lock_files) : ""; + $steam_out = $remote->steam_cmd($home_id, $home_info['home_path'], $installer_name, $modname, + $betaname, $betapwd, $login, $pass, $settings['steam_guard'] ?? '', + $exec_folder_path, $exec_path, $precmd, $postcmd, $cfg_os, $lockFiles, $arch); + if ($steam_out === 0) { + return array('ok' => false, 'pending' => true, 'message' => 'Failed to start SteamCMD update.', 'mod_id' => $mod_id); + } + return array('ok' => true, 'started' => true, 'message' => 'Update started.', 'mod_id' => $mod_id); + } + + $ran_scripts = false; + if (!empty((string)$precmd)) { + $remote->exec((string)$precmd); + $ran_scripts = true; + } + if (!empty((string)$postcmd)) { + $remote->exec((string)$postcmd); + $ran_scripts = true; + } + return array( + 'ok' => true, + 'started' => $ran_scripts, + 'completed' => !$ran_scripts, + 'message' => $ran_scripts ? 'Script install started.' : 'No installer command was required.', + 'mod_id' => $mod_id + ); + } +} + diff --git a/modules/gamemanager/update_server.php b/modules/gamemanager/update_server.php index 53a1382c..799598d7 100644 --- a/modules/gamemanager/update_server.php +++ b/modules/gamemanager/update_server.php @@ -24,6 +24,7 @@ require_once("includes/lib_remote.php"); require_once("modules/config_games/server_config_parser.php"); +require_once("modules/gamemanager/update_actions.php"); function exec_ogp_module() { @@ -90,111 +91,27 @@ function exec_ogp_module() { // Start update. else if ($_GET['update'] == 'update' && $update_active != 1) { - $installer_name = $modkey; - - if ( isset( $mod_xml->installer_name ) ) - { - $installer_name = $mod_xml->installer_name; + $start_result = gamemanager_trigger_update_install( + $db, + $home_info, + intval($mod_id), + array( + 'master_server_home_id' => isset($_REQUEST['master_server_home_id']) ? intval($_REQUEST['master_server_home_id']) : 0, + 'settings' => $db->getSettings(), + ) + ); + $mod_id = intval($start_result['mod_id'] ?? $mod_id); + if (empty($start_result['ok'])) { + print_failure(!empty($start_result['message']) ? $start_result['message'] : get_lang("failed_to_start_steam_update")); + return; } - - $precmd = $home_info['mods'][$mod_id]['precmd'] == "" ? - ( $home_info['mods'][$mod_id]['def_precmd'] == "" ? $server_xml->pre_install : - $home_info['mods'][$mod_id]['def_precmd'] ) : $home_info['mods'][$mod_id]['precmd']; - - $postcmd = $home_info['mods'][$mod_id]['postcmd'] == "" ? - ( $home_info['mods'][$mod_id]['def_postcmd'] == "" ? $server_xml->post_install : - $home_info['mods'][$mod_id]['def_precmd'] ) : $home_info['mods'][$mod_id]['postcmd']; - - $exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location ); - $exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name ); - - if( isset( $_REQUEST['master_server_home_id'] ) ) - { - $ms_home_id = $_REQUEST['master_server_home_id']; - - if ($db->getMasterServer($home_info['remote_server_id'], $home_info['home_cfg_id']) == $ms_home_id) { - if ($ms_home_id !== $home_id) { - $ms_info = $db->getGameHome($ms_home_id); - $steam_out = $remote->masterServerUpdate( $home_id,$home_info['home_path'],$ms_home_id,$ms_info['home_path'],$exec_folder_path,$exec_path,$precmd,$postcmd ); - } else { - print_failure(get_lang('cannot_update_from_own_self')); - $view->refresh('?m=gamemanager&p=game_monitor', 2); - - return; - } - } else { - $db->logger(get_lang_f('update_attempt_from_nonmaster_server', $_SESSION['users_login'], $home_id, $ms_home_id)); - print_failure(get_lang('attempting_nonmaster_update')); - $view->refresh('?m=gamemanager&p=game_monitor', 2); - - return; - } - - } - elseif ($use_steamcmd && !empty((string)$installer_name)) - { - if( preg_match("/win32/", $server_xml->game_key) OR preg_match("/win64/", $server_xml->game_key) ) - $cfg_os = "windows"; - elseif( preg_match("/linux/", $server_xml->game_key) ) - $cfg_os = "linux"; - - $settings = $db->getSettings(); - - // Some games like L4D2 require anonymous login - if($mod_xml->installer_login){ - $login = $mod_xml->installer_login; - $pass = ''; - }else{ - $login = $settings['steam_user']; - $pass = $settings['steam_pass']; - } - - $modname = ( $installer_name == '90' ) ? $modkey : ''; - $betaname = isset($mod_xml->betaname) ? $mod_xml->betaname : ''; - $betapwd = isset($mod_xml->betapwd) ? $mod_xml->betapwd : ''; - $arch = isset($mod_xml->steam_bitness) ? $mod_xml->steam_bitness : ''; - - // Additional files to lock - if(isset($server_xml->lock_files) && !empty($server_xml->lock_files)){ - $lockFiles = trim($server_xml->lock_files); - }else{ - $lockFiles = ""; - } - - $steam_out = $remote->steam_cmd( $home_id,$home_info['home_path'],$installer_name,$modname, - $betaname,$betapwd,$login,$pass,$settings['steam_guard'], - $exec_folder_path,$exec_path,$precmd,$postcmd,$cfg_os,$lockFiles,$arch); - } - else - { - // No SteamCMD installer — run pre/post install scripts only. - $ran_scripts = false; - if (!empty((string)$precmd)) { - $remote->exec((string)$precmd); - $ran_scripts = true; - } - if (!empty((string)$postcmd)) { - $remote->exec((string)$postcmd); - $ran_scripts = true; - } - if ($ran_scripts) { - print_success( get_lang("update_started") ); - } else { - print_success( get_lang("update_completed") ); - } + if (!empty($start_result['started'])) { + print_success(get_lang("update_started")); + } else { + print_success(get_lang("update_completed")); $view->refresh("?m=gamemanager&p=game_monitor&home_id=$home_id", 3); return; } - - if( $steam_out === 0 ) - { - print_failure( get_lang("failed_to_start_steam_update") ); - return; - } - else if ( $steam_out === 1 ) - { - print_success( get_lang("update_started") ); - } } // Refresh update page. else diff --git a/modules/litefm/fm_dir.php b/modules/litefm/fm_dir.php index 1f623ab6..d2862bb6 100644 --- a/modules/litefm/fm_dir.php +++ b/modules/litefm/fm_dir.php @@ -64,16 +64,30 @@ function exec_ogp_module() // We must always add the home directory to the fm_cwd so that user // can not go out of the homedir. - $path = clean_path($home_cfg['home_path']."/".@$_SESSION['fm_cwd_'.$home_id]); + $cwd_session_key = 'fm_cwd_' . $home_id; + if (!isset($_SESSION[$cwd_session_key]) || !is_string($_SESSION[$cwd_session_key])) { + $_SESSION[$cwd_session_key] = ''; + } + $path = clean_path($home_cfg['home_path']."/".$_SESSION[$cwd_session_key]); if (!$remote->rfile_exists($path)) { while(!$remote->rfile_exists($path)) { - $_SESSION['fm_cwd_'.$home_id] = dirname($_SESSION['fm_cwd_'.$home_id]); - $path = clean_path($home_cfg['home_path']."/".@$_SESSION['fm_cwd_'.$home_id]); + $current_cwd = isset($_SESSION[$cwd_session_key]) ? (string)$_SESSION[$cwd_session_key] : ''; + if ($current_cwd === '' || $current_cwd === '.' || $current_cwd === DIRECTORY_SEPARATOR) { + print_failure("Server files have not been installed yet."); + echo "
    << ".get_lang('back')."
    "; + return; + } + $parent_cwd = dirname($current_cwd); + if (!is_string($parent_cwd) || $parent_cwd === '.' || $parent_cwd === DIRECTORY_SEPARATOR) { + $parent_cwd = ''; + } + $_SESSION[$cwd_session_key] = $parent_cwd; + $path = clean_path($home_cfg['home_path']."/".$_SESSION[$cwd_session_key]); if($path == clean_path($home_cfg['home_path']."/")) { - print_failure(get_lang_f("dir_not_found",$path)); + print_failure("Server files have not been installed yet."); echo "
    << ".get_lang('back')."
    "; return; } @@ -214,7 +228,7 @@ function exec_ogp_module() { $remote->shell_action('remove_recursive', $files); $files = str_replace('" "','"
    "',$files); - $db->logger( get_lang("remove") . ": ${files}" ); + $db->logger( get_lang("remove") . ": {$files}" ); } } } @@ -349,7 +363,7 @@ function exec_ogp_module() if($items != '') { $retval = $remote->compress_files($items,$path,$archive_name,$archive_type); - $archive = clean_path( "${path}/${archive_name}.${archive_type}" ); + $archive = clean_path( "{$path}/{$archive_name}.{$archive_type}" ); if( $retval == 0 ) { do{ From 75aff133926d14e57d2e41e97aeb3a550ba303ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 22:06:51 +0000 Subject: [PATCH 2/3] docs: update changelog, todo, and billing timestamp metadata Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/f982c3a1-c9ae-4c5b-9fb6-2941d0e5b7c1 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- CHANGELOG.md | 2 ++ docs/COPILOT_TODO.md | 1 + modules/billing/timestamp.txt | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 530024f0..09be2c47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## 2026-05-08 +- **Auto-install trigger + monthly-only billing pricing:** Refactored Game Monitor update/install into a shared callable (`modules/gamemanager/update_actions.php`) reused by billing provisioning so new paid/free/admin-created homes auto-trigger the same install/update path used by `m=gamemanager&p=update&update=refresh` without manual clicks. Billing now treats monthly pricing (`price_monthly`) as canonical across admin service config, add-to-cart, free checkout, PayPal capture, and provisioning end-date math (31-day months), while preserving legacy daily/yearly columns for backward compatibility. +- **LiteFM PHP 8.3 compatibility and install-pending UX:** Removed deprecated `${var}` interpolation usage, guarded missing `fm_cwd_*` session keys and `dirname()` null paths, and replaced directory-not-found warning output with a clear message when server files are not installed yet. - **Provisioning + billing admin UX reliability pass:** Hardened automatic server provisioning to reserve ports from `arrange_ports` (exact `home_cfg_id`, then `home_cfg_id=0` fallback), prevent duplicate `home_ip_ports` assignment, keep order/invoice/home linkage intact even when install is pending, and apply safe default mod resolution so base installs are not blocked by missing explicit mod choices. Refreshed billing admin service management with row-level save actions, sortable columns (including Game Name enabled-first toggle), clearer save feedback, cleaner unstable-update caution styling, login theme polish, and updated storefront timestamp footer metadata. - **Steam Workshop reliability + UI simplification:** Removed customer CLI/update scripting instructions from Workshop user flow, reduced per-server behavior options to supported modes only, switched remaining Steam Workshop SQL references to prefix helpers (no `OGP_DB_PREFIX` strings), hardened queued-update agent processing (`queued → updating → installed/failed`) with clearer error persistence, and refreshed monitor/support documentation links to open game-specific docs (fallback to docs index) in a new tab. - **Billing docs routing refresh:** Updated docs browser links/icons to root-relative storefront paths (`/docs.php`, `/docs/...`) and removed stale hardcoded panel host guidance from getting-started documentation. diff --git a/docs/COPILOT_TODO.md b/docs/COPILOT_TODO.md index 08262d02..ba91d94f 100644 --- a/docs/COPILOT_TODO.md +++ b/docs/COPILOT_TODO.md @@ -9,3 +9,4 @@ - Add a storefront visual-regression check at 375px and 430px breakpoints covering login, order, and cart pages to prevent mobile overflow regressions. - Complete a full pass over all `modules/billing/docs/*` game guides to standardize OS/Workshop/RCON capability statements against current XML-backed server support. - Add an automated billing provisioning integration test fixture that verifies arrange_ports exact/fallback allocation, duplicate-port protection, and home_id linkage after paid/free checkout. +- Add a billing UI badge/filter that distinguishes "pending install" vs "installed" states directly in customer/server order views. diff --git a/modules/billing/timestamp.txt b/modules/billing/timestamp.txt index cca16ba0..11522bc6 100644 --- a/modules/billing/timestamp.txt +++ b/modules/billing/timestamp.txt @@ -1 +1 @@ -Last Updated at 9:18pm on 2026-08-05 +Last Updated at 10:02pm on 2026-05-08 From f84fba7001aa882d05888b591be9f4b229bdfcf3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 22:07:32 +0000 Subject: [PATCH 3/3] fix: use correct default post-install command fallback Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/f982c3a1-c9ae-4c5b-9fb6-2941d0e5b7c1 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- modules/gamemanager/update_actions.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/gamemanager/update_actions.php b/modules/gamemanager/update_actions.php index d24b779b..45376a7a 100644 --- a/modules/gamemanager/update_actions.php +++ b/modules/gamemanager/update_actions.php @@ -57,7 +57,7 @@ if (!function_exists('gamemanager_trigger_update_install')) { ? ($home_info['mods'][$mod_id]['def_precmd'] == "" ? $server_xml->pre_install : $home_info['mods'][$mod_id]['def_precmd']) : $home_info['mods'][$mod_id]['precmd']; $postcmd = $home_info['mods'][$mod_id]['postcmd'] == "" - ? ($home_info['mods'][$mod_id]['def_postcmd'] == "" ? $server_xml->post_install : $home_info['mods'][$mod_id]['def_precmd']) + ? ($home_info['mods'][$mod_id]['def_postcmd'] == "" ? $server_xml->post_install : $home_info['mods'][$mod_id]['def_postcmd']) : $home_info['mods'][$mod_id]['postcmd']; $exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location); $exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name); @@ -128,4 +128,3 @@ if (!function_exists('gamemanager_trigger_update_install')) { ); } } -