From 8f8a2a4c06a0178a647292d79f1a588d57b10004 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 16:31:10 +0000 Subject: [PATCH] fix: auto-provision port/mod assignment, error logging, retry UI, GSP wording Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/98539de7-36c5-4a0e-962e-e30f5e4c9125 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- lang/English/modules/gamemanager.php | 2 +- modules/billing/admin_game_defaults.php | 209 +++++++++ modules/billing/admin_orders.php | 171 ++++++- modules/billing/create_servers.php | 578 ++++++++++++++++++++++++ modules/billing/module.php | 37 +- modules/billing/navigation.xml | 4 + 6 files changed, 974 insertions(+), 27 deletions(-) create mode 100644 modules/billing/admin_game_defaults.php diff --git a/lang/English/modules/gamemanager.php b/lang/English/modules/gamemanager.php index 9f119f5f..7aaa1830 100644 --- a/lang/English/modules/gamemanager.php +++ b/lang/English/modules/gamemanager.php @@ -24,7 +24,7 @@ define('LANG_no_games_to_monitor', "You do not have any games configured to you that can be monitored."); define('LANG_status', "Status"); -define('LANG_fail_no_mods', "No mods enabled for this game! You need to ask your OGP admin to add mod(s) for the game assigned for you."); +define('LANG_fail_no_mods', "No mods/builds are enabled for this game. Ask your GSP admin to enable a default mod/build."); define('LANG_no_game_homes_assigned', "You don't have any servers assigned to your account."); define('LANG_select_game_home_to_configure', "Select a game server that you want to configure"); define('LANG_file_manager', "File Manager"); diff --git a/modules/billing/admin_game_defaults.php b/modules/billing/admin_game_defaults.php new file mode 100644 index 00000000..7bda362f --- /dev/null +++ b/modules/billing/admin_game_defaults.php @@ -0,0 +1,209 @@ +isAdmin($user_id); + + if (!$isAdmin) { + echo "

Access Denied: Admin privileges required.

"; + return; + } + + // ------------------------------------------------------------------- + // Check whether is_default_for_billing column exists. + // It is added by db_version 8 migration; show a warning if missing. + // ------------------------------------------------------------------- + $colExists = false; + $colCheck = $db->resultQuery( + "SELECT COUNT(*) AS cnt + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$db_prefix}config_mods' + AND COLUMN_NAME = 'is_default_for_billing'" + ); + if (!empty($colCheck[0]['cnt']) && intval($colCheck[0]['cnt']) > 0) { + $colExists = true; + } + + if (!$colExists) { + echo "
" + . "

Database migration required. " + . "The is_default_for_billing column is not present in {$db_prefix}config_mods. " + . "Please run the billing module update (Admin → Module Manager → Update) to apply db_version 8.

" + . "
"; + return; + } + + // ------------------------------------------------------------------- + // Handle POST: save default mod for a game (home_cfg_id) + // ------------------------------------------------------------------- + $saveMsg = ''; + if (isset($_POST['save_default']) && isset($_POST['home_cfg_id'])) { + $save_home_cfg_id = intval($_POST['home_cfg_id']); + $save_mod_cfg_id = intval($_POST['mod_cfg_id'] ?? 0); + + // Clear all current defaults for this game first + $db->query( + "UPDATE `{$db_prefix}config_mods` + SET is_default_for_billing = 0 + WHERE home_cfg_id = " . $save_home_cfg_id + ); + + if ($save_mod_cfg_id > 0) { + // Set the selected mod as default (only if it belongs to this game) + $updated = $db->query( + "UPDATE `{$db_prefix}config_mods` + SET is_default_for_billing = 1 + WHERE mod_cfg_id = " . $save_mod_cfg_id . " + AND home_cfg_id = " . $save_home_cfg_id + ); + $saveMsg = $updated + ? "

Default mod/build updated for game config #{$save_home_cfg_id}.

" + : "

Failed to update default — mod may not belong to this game.

"; + } else { + $saveMsg = "

Default cleared for game config #{$save_home_cfg_id}. Billing will use the service-specific mod or fail with an admin-visible error if none is set.

"; + } + } + + echo $saveMsg; + echo "

Game Mod/Build Defaults for Billing

"; + echo "

Mark one mod/build per game as the auto-install default used when billing provisions a new server. " + . "This is used when a billing service does not specify its own mod (mod_cfg_id = 0).

"; + echo "

Priority order during provisioning: " + . "1) Service-specific mod_cfg_id → 2) is_default_for_billing here → " + . "3) Single available mod (auto-selected) → 4) Fail with admin-visible error.

"; + + // ------------------------------------------------------------------- + // Load all game configs that have at least one mod defined + // ------------------------------------------------------------------- + $games = $db->resultQuery( + "SELECT ch.home_cfg_id, ch.home_name + FROM `{$db_prefix}config_homes` ch + WHERE EXISTS ( + SELECT 1 FROM `{$db_prefix}config_mods` cm + WHERE cm.home_cfg_id = ch.home_cfg_id + ) + ORDER BY ch.home_name ASC" + ); + + if (empty($games)) { + echo "

No game configurations with mods found. Add mods via Admin → Game Manager → Configure Games.

"; + return; + } + + echo ""; + echo ""; + + foreach ((array)$games as $game) { + $hcfgid = intval($game['home_cfg_id']); + $gameName = htmlspecialchars($game['home_name'] ?? "Game #{$hcfgid}"); + + // Load mods for this game + $mods = $db->resultQuery( + "SELECT mod_cfg_id, mod_key, mod_name, is_default_for_billing + FROM `{$db_prefix}config_mods` + WHERE home_cfg_id = " . $hcfgid . " + ORDER BY mod_name ASC" + ); + + if (empty($mods)) { + continue; + } + + $currentDefault = null; + foreach ($mods as $m) { + if (!empty($m['is_default_for_billing'])) { + $currentDefault = htmlspecialchars($m['mod_name'] . ' (mod_cfg_id=' . $m['mod_cfg_id'] . ')'); + } + } + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + + echo "
Gamehome_cfg_idAvailable Mods/BuildsCurrent DefaultAction
{$gameName}{$hcfgid}"; + $modNames = array_map(fn($m) => htmlspecialchars($m['mod_name']), $mods); + echo implode('
', $modNames); + echo "
"; + echo $currentDefault + ? "✓ " . $currentDefault . "" + : "None"; + echo ""; + + // Form to set default + echo "
"; + echo ""; + echo ""; + echo " "; + echo ""; + echo "
"; + + echo "
"; + + // ------------------------------------------------------------------- + // Show billing_services with their current mod_cfg_id + // ------------------------------------------------------------------- + echo "
"; + echo "

Billing Services — Mod/Build Override

"; + echo "

Services below have an explicit mod_cfg_id set. This takes priority over the game default above. " + . "Set to 0 to fall back to the game default.

"; + + $services = $db->resultQuery( + "SELECT s.service_id, s.service_name, s.home_cfg_id, s.mod_cfg_id, + ch.home_name AS game_name, + cm.mod_name + FROM `{$db_prefix}billing_services` s + LEFT JOIN `{$db_prefix}config_homes` ch ON ch.home_cfg_id = s.home_cfg_id + LEFT JOIN `{$db_prefix}config_mods` cm ON cm.mod_cfg_id = s.mod_cfg_id + ORDER BY s.service_name ASC" + ); + + if (empty($services)) { + echo "

No billing services configured.

"; + } else { + echo ""; + echo ""; + foreach ((array)$services as $svc) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo "
ServiceGameCurrent mod_cfg_idMod Name
".htmlspecialchars($svc['service_name'] ?? '')." (#".intval($svc['service_id']).")".htmlspecialchars($svc['game_name'] ?? 'N/A')."".intval($svc['mod_cfg_id']).(intval($svc['mod_cfg_id']) === 0 ? " (use game default)" : "")."".htmlspecialchars($svc['mod_name'] ?? ($svc['mod_cfg_id'] == 0 ? '—' : 'mod not found'))."
"; + echo "

To change a service's mod, edit it in Admin → Billing → Services.

"; + } + + echo "
"; +} +?> diff --git a/modules/billing/admin_orders.php b/modules/billing/admin_orders.php index a47623ec..74109b98 100644 --- a/modules/billing/admin_orders.php +++ b/modules/billing/admin_orders.php @@ -6,7 +6,8 @@ function exec_ogp_module() { - global $db, $view; + global $db, $view, $table_prefix; + $db_prefix = isset($table_prefix) ? $table_prefix : ''; $user_id = $_SESSION['user_id']; $isAdmin = $db->isAdmin($user_id); @@ -14,7 +15,28 @@ function exec_ogp_module() echo "

Access Denied: Admin privileges required.

"; return; } - + + // ------------------------------------------------------------------- + // Handle "Retry Provisioning" for a single order + // ------------------------------------------------------------------- + if (isset($_POST['retry_provision_order']) && !empty($_POST['retry_order_id'])) { + $retry_id = intval($_POST['retry_order_id']); + require_once __DIR__ . '/create_servers.php'; + $retryResult = billing_invoke_provision([ + 'order_ids' => [$retry_id], + 'user_id' => $user_id, + 'is_admin' => true, + ]); + if (!empty($retryResult['provisioned_count'])) { + echo "

Retry provisioning succeeded for order #{$retry_id}.

"; + } elseif (!empty($retryResult['output'])) { + echo "

Retry provisioning for order #{$retry_id} did not succeed. See details below.

" + . "
" . htmlspecialchars($retryResult['output']) . "
"; + } else { + echo "

Retry provisioning for order #{$retry_id}: no result returned. Check server logs.

"; + } + } + // Handle bulk actions if (isset($_POST['bulk_action']) && isset($_POST['selected_orders'])) { $action = $_POST['bulk_action']; @@ -30,13 +52,13 @@ function exec_ogp_module() exit; break; case 'expire': - $db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='Expired' WHERE order_id=".$order_id); + $db->query("UPDATE `{$db_prefix}billing_orders` SET status='Expired' WHERE order_id=".$order_id); break; case 'activate': - $db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='Active' WHERE order_id=".$order_id); + $db->query("UPDATE `{$db_prefix}billing_orders` SET status='Active' WHERE order_id=".$order_id); break; case 'invoice': - $db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='Invoiced' WHERE order_id=".$order_id); + $db->query("UPDATE `{$db_prefix}billing_orders` SET status='Invoiced' WHERE order_id=".$order_id); break; } } @@ -66,9 +88,9 @@ function exec_ogp_module() // Build query $query = "SELECT o.*, s.service_name, u.users_login, u.users_email - FROM OGP_DB_PREFIXbilling_orders o - LEFT JOIN OGP_DB_PREFIXbilling_services s ON o.service_id = s.service_id - LEFT JOIN OGP_DB_PREFIXusers u ON o.user_id = u.user_id + FROM `{$db_prefix}billing_orders` o + LEFT JOIN `{$db_prefix}billing_services` s ON o.service_id = s.service_id + LEFT JOIN `{$db_prefix}users` u ON o.user_id = u.user_id WHERE 1=1"; if ($status_filter != 'all') { @@ -91,6 +113,17 @@ function exec_ogp_module() echo "

No orders found matching your filters.

"; return; } + + // Pre-fetch provisioning error counts per order for display + $errorCounts = []; + $errCountRows = $db->resultQuery( + "SELECT billing_order_id, COUNT(*) AS cnt + FROM `{$db_prefix}billing_provisioning_errors` + GROUP BY billing_order_id" + ); + foreach ((array)$errCountRows as $ecr) { + $errorCounts[intval($ecr['billing_order_id'])] = intval($ecr['cnt']); + } echo "
"; echo "
"; @@ -130,39 +163,83 @@ function exec_ogp_module() case 'Expired': $status_class = 'label-danger'; break; default: $status_class = 'label-info'; } + + $oid = intval($order['order_id']); + $errCount = $errorCounts[$oid] ?? 0; echo ""; - echo ""; - echo "".$order['order_id'].""; - echo "".$order['users_login']."
".$order['users_email'].""; - echo "".$order['home_name'].""; - echo "".$order['service_name'].""; + echo ""; + echo "".$oid.""; + echo "".htmlspecialchars($order['users_login'] ?? '')."
".htmlspecialchars($order['users_email'] ?? '').""; + echo "".htmlspecialchars($order['home_name'] ?? '').""; + echo "".htmlspecialchars($order['service_name'] ?? '').""; echo "".$order['max_players'].""; echo "$".number_format($order['price'], 2).""; echo "".$order['qty']." ".$order['invoice_duration']."(s)"; - echo "".$order['status'].""; + echo "".$order['status'].""; + if ($errCount > 0) { + echo " " . $errCount . " error(s)"; + } + echo ""; echo "".date('Y-m-d H:i', strtotime($order['order_date'])).""; echo "".($order['end_date'] ? date('Y-m-d', strtotime($order['end_date'])) : 'N/A').""; echo "".($order['home_id'] ? $order['home_id'] : 'N/A').""; echo ""; if ($order['status'] == 'Active' && !$order['home_id']) { - echo "Provision "; + echo "Provision "; + // Retry provisioning button (inline POST form) + echo ""; + echo ""; + echo ""; + echo ""; + echo " "; } if ($order['status'] == 'Active' && $order['home_id']) { echo "View Server "; } - - echo "Details"; + + if ($errCount > 0) { + echo "Errors "; + } + + echo "Details"; echo ""; echo ""; + + // Collapsible provisioning error rows + if ($errCount > 0) { + echo ""; + echo ""; + $errRows = $db->resultQuery( + "SELECT * FROM `{$db_prefix}billing_provisioning_errors` + WHERE billing_order_id=" . $oid . " + ORDER BY created_at DESC LIMIT 20" + ); + if (!empty($errRows)) { + echo ""; + echo ""; + foreach ($errRows as $er) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo "
TimeRemote SrvIP IDPortModMessage
".htmlspecialchars($er['created_at'])."".intval($er['remote_server_id'])."".intval($er['ip_id'])."".intval($er['attempted_port'])."".intval($er['mod_cfg_id'])."".htmlspecialchars($er['failure_message'])."
"; + } + echo ""; + } } echo ""; echo ""; - // JavaScript for checkbox toggle + // JavaScript for checkbox toggle and error panel echo ""; // Summary stats - $stats = $db->resultQuery("SELECT status, COUNT(*) as count, SUM(price) as total - FROM OGP_DB_PREFIXbilling_orders - GROUP BY status"); + $stats = $db->resultQuery( + "SELECT status, COUNT(*) as count, SUM(price) as total + FROM `{$db_prefix}billing_orders` + GROUP BY status" + ); echo "
"; echo "

Order Statistics

"; @@ -204,8 +291,8 @@ function exec_ogp_module() // are the reason the game monitor may show "No expiration date found". $orphans = $db->resultQuery( "SELECT o.order_id, o.user_id, o.home_name, o.home_id, o.status, o.end_date - FROM OGP_DB_PREFIXbilling_orders o - LEFT JOIN OGP_DB_PREFIXserver_homes sh ON sh.home_id = o.home_id + FROM `{$db_prefix}billing_orders` o + LEFT JOIN `{$db_prefix}server_homes` sh ON sh.home_id = o.home_id WHERE o.home_id != '0' AND o.home_id != '' AND sh.home_id IS NULL @@ -214,9 +301,9 @@ function exec_ogp_module() echo "
"; echo "

Orphaned home_id Diagnostics

"; - echo "

Billing orders that reference a home_id which no longer exists in gsp_server_homes. "; + echo "

Billing orders that reference a home_id which no longer exists in server_homes. "; echo "These orders will not show an expiration date on the game monitor. "; - echo "Reset home_id to 0 or re-provision these orders to fix them. "; + echo "Reset home_id to 0 and use the Retry Provisioning button to re-provision them. "; echo "Run normalize_billing_order_status.sql to standardize any legacy status values.

"; if (empty($orphans)) { @@ -237,5 +324,39 @@ function exec_ogp_module() echo ""; } echo "
"; + + // Recent provisioning errors (all orders) ———————————————————————————— + $recentErrors = $db->resultQuery( + "SELECT e.*, o.home_name, u.users_login + FROM `{$db_prefix}billing_provisioning_errors` e + LEFT JOIN `{$db_prefix}billing_orders` o ON o.order_id = e.billing_order_id + LEFT JOIN `{$db_prefix}users` u ON u.user_id = e.user_id + ORDER BY e.created_at DESC + LIMIT 50" + ); + + echo "
"; + echo "

Recent Provisioning Errors

"; + if (empty($recentErrors)) { + echo "

✓ No provisioning errors recorded.

"; + } else { + echo ""; + echo ""; + foreach ($recentErrors as $er) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo "
TimeOrder IDUserServer NameRemote SrvIP IDPortModMessage
".htmlspecialchars($er['created_at'])."".intval($er['billing_order_id'])."".htmlspecialchars($er['users_login'] ?? ('uid:'.intval($er['user_id'])))."".htmlspecialchars($er['home_name'] ?? '')."".intval($er['remote_server_id'])."".intval($er['ip_id'])."".intval($er['attempted_port'])."".intval($er['mod_cfg_id'])."".htmlspecialchars($er['failure_message'])."
"; + } + echo "
"; } ?> diff --git a/modules/billing/create_servers.php b/modules/billing/create_servers.php index 963832af..ffdefc16 100644 --- a/modules/billing/create_servers.php +++ b/modules/billing/create_servers.php @@ -27,6 +27,584 @@ if (!function_exists('billing_invoke_provision')) { } } +/** + * Log a provisioning failure to billing_provisioning_errors. + * All parameters are sanitised inside this function. + */ +if (!function_exists('billing_log_provision_error')) { + function billing_log_provision_error( + $db, + string $db_prefix, + int $billing_order_id, + int $home_id, + int $user_id, + int $remote_server_id, + int $ip_id, + int $attempted_port, + int $mod_cfg_id, + string $failure_message + ): void { + try { + $db->query( + "INSERT INTO `{$db_prefix}billing_provisioning_errors` + (`billing_order_id`,`home_id`,`user_id`,`remote_server_id`,`ip_id`,`attempted_port`,`mod_cfg_id`,`failure_message`,`created_at`) + VALUES (" + . intval($billing_order_id) . "," + . intval($home_id) . "," + . intval($user_id) . "," + . intval($remote_server_id) . "," + . intval($ip_id) . "," + . intval($attempted_port) . "," + . intval($mod_cfg_id) . "," + . "'" . $db->realEscapeSingle($failure_message) . "'," + . "NOW())" + ); + } catch (Throwable $e) { + // Never let logging itself break provisioning + error_log('billing_log_provision_error: ' . $e->getMessage()); + } + } +} + +function exec_ogp_module() +{ + global $db,$view,$settings,$table_prefix; + $db_prefix = isset($table_prefix) ? $table_prefix : ''; + + // $now is used in multiple branches below — define it once here so it is + // always a string that date() / strtotime() can handle safely (PHP 8 fix). + $now = date('Y-m-d H:i:s'); + + $override = isset($GLOBALS['BILLING_PROVISION_OVERRIDE']) ? $GLOBALS['BILLING_PROVISION_OVERRIDE'] : null; + $user_id = isset($override['user_id']) ? intval($override['user_id']) : (isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0); + $isAdmin = isset($override['is_admin']) ? (bool)$override['is_admin'] : $db->isAdmin($user_id); + $provision_all = $override ? !empty($override['provision_all']) : isset($_POST['provision_all']); + $orderIds = array(); + if ($override && !empty($override['order_ids'])) { + $orderIds = array_map('intval', (array)$override['order_ids']); + } + if (empty($orderIds)) { + $order_id = null; + if (isset($_POST['order_id'])) { + $order_id = $_POST['order_id']; + } + if(isset($_GET['order_id'])){ + $order_id = $_GET['order_id']; + } + if (!empty($order_id)) { + $orderIds = array(intval($order_id)); + } + } + + // Handle provision_all request - provision all Active (paid) orders for this user + if ($provision_all) { + if ( $isAdmin ){ + $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" ); + } else { + $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE user_id=".$db->realEscapeSingle($user_id)." AND status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" ); + } + } + // Handle provision_single or order_id parameter - provision specific order + else { + if (empty($orderIds)) { + echo "
No order ID specified.
"; + $GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array('provisioned_count'=>0,'failed_count'=>0,'orders'=>array()); + return; + } + $idList = implode(',', array_map('intval', $orderIds)); + if ( $isAdmin ){ + $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE order_id IN ($idList) AND status='Active'" ); + } else { + $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE order_id IN ($idList) AND user_id=".$db->realEscapeSingle($user_id)." AND status='Active'" ); + } + } + $processed_orders = array(); + if( !empty($orders) ) + { + $provisioned_count = 0; + $failed_count = 0; + + foreach ((array)$orders as $order) + { + $end_date = null; + $end_date_str = null; + $order_id = $order['order_id']; + $processed_orders[] = intval($order_id); + $service_id = $order['service_id']; + $home_name = $order['home_name']; + $remote_control_password = $order['remote_control_password']; + $ftp_password = $order['ftp_password']; + if ($remote_control_password === '' || strcasecmp((string)$remote_control_password, 'ChangeMe') === 0) { + $remote_control_password = billing_generate_provision_password(); + } + if ($ftp_password === '' || strcasecmp((string)$ftp_password, 'ChangeMe') === 0) { + $ftp_password = billing_generate_provision_password(); + } + $ip = $order['ip']; + $max_players = $order['max_players']; + $user_id = $order['user_id']; + $extended = isset($order['extended']) && $order['extended'] == "1" ? TRUE : FALSE; + $alreadyProvisioned = !$extended && intval($order['home_id'] ?? 0) > 0; + //Query service info + $service = $db->resultQuery( "SELECT * + FROM `{$db_prefix}billing_services` + WHERE service_id=".$db->realEscapeSingle($service_id) ); + + if( !empty( $service[0] ) ) + { + $home_cfg_id = $service[0]['home_cfg_id']; + $mod_cfg_id = $service[0]['mod_cfg_id']; + //remote_server_id has been stored in IP_ID + //$remote_server_id = $service[0]['remote_server_id']; + $remote_server_id = $order['ip']; + + $ftp = $service[0]['ftp']; + $install_method = $service[0]['install_method']; + $manual_url = $service[0]['manual_url']; + $access_rights = $service[0]['access_rights']; + } + else + return; + + if($alreadyProvisioned) + { + $home_id = intval($order['home_id']); + } + elseif($extended) + { + $home_id = $order['home_id']; + + //Get The home info without mods in 1 array (Necesary for remote connection). + $home_info = $db->getGameHomeWithoutMods($home_id); + + //Create the remote connection + $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); + + //Reassign the server + $db->assignHomeTo( "user", $user_id, $home_id, $access_rights ); + + //Reenable the FTP account + if ($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']); + } + echo "

Server Installed, Check your Email for Details


"; + +//Panel Log + $db->logger( "RENEWED SERVER " . $home_id); +// SEND EMAIL + $settings = $db->getSettings(); + $subject = "Gameserver Renewel at " . $settings['panel_name']; + $email = $db->resultQuery(" SELECT DISTINCT users_email + FROM {$table_prefix}users, {$table_prefix}billing_orders + WHERE {$table_prefix}users.user_id = $user_id")[0]["users_email"]; + + $message = "Your server, " . $home_name ." ID #". $home_id . " at " . $settings['panel_name'] . " has just been renewed.
+ Thank You for your continued support.
+ If you have any questions or requests, visit our website or contact us directly in our Discord Server."; + + $mail = mymail($email, $subject, $message, $settings); + $rundate = date('d/M/y G:i', is_numeric($now) ? (int)$now : strtotime($now)); + + if (!$mail) + $db->logger( "Email FAILED - Server Renewed " . $home_id); +// END EMAIL + + //WEBHOOK Discord + discordmsg(array('content' => "The ". $home_name ." server ID #". $home_id . " has just been renewed."), $settings['discord_webhook_main'] ?? ''); + //end WEBHOOK Discord + + } + else + { + //OPTIONS, change it at your choice; + $extra_params = "";//no extra params defined by default + $cpu_affinity = "NA";//Affinity to one core/thread of the cpu by number, use NA to disable it + $nice = "0";//Min priority=19 Max Priority=-19 + + // --------------------------------------------------------------- + // Resolve IP: find the first IP address configured for this + // remote server. The order.ip column stores remote_server_id. + // --------------------------------------------------------------- + $resolved_remote_server_id = intval($remote_server_id); + $ip_id = null; + $ipRows = $db->resultQuery( + "SELECT ip_id FROM `{$db_prefix}remote_server_ips` + WHERE remote_server_id=" . $resolved_remote_server_id . " + ORDER BY ip_id ASC LIMIT 1" + ); + if (!empty($ipRows[0]['ip_id'])) { + $ip_id = intval($ipRows[0]['ip_id']); + } + if ($ip_id === null) { + $errMsg = "No IP address configured for remote server ID {$resolved_remote_server_id} (order_id={$order_id}). " + . "Please add an IP to that remote server in the panel."; + billing_log_provision_error($db, $db_prefix, intval($order_id), 0, intval($user_id), $resolved_remote_server_id, 0, 0, intval($mod_cfg_id), $errMsg); + echo "

Provisioning failed for order #" . intval($order_id) . ": " . htmlspecialchars($errMsg) . "

"; + $failed_count++; + continue; + } + + // --------------------------------------------------------------- + // Resolve mod/build in priority order: + // 1. Explicit mod_cfg_id from billing_services (if > 0 and valid) + // 2. Admin-configured is_default_for_billing on config_mods + // 3. Only one mod available for this game — use it automatically + // 4. Fail gracefully with an admin-visible error + // --------------------------------------------------------------- + $resolved_mod_cfg_id = null; + + if (!empty($mod_cfg_id) && intval($mod_cfg_id) > 0) { + $modCheck = $db->resultQuery( + "SELECT mod_cfg_id FROM `{$db_prefix}config_mods` + WHERE mod_cfg_id=" . intval($mod_cfg_id) . " + AND home_cfg_id=" . intval($home_cfg_id) + ); + if (!empty($modCheck[0]['mod_cfg_id'])) { + $resolved_mod_cfg_id = intval($modCheck[0]['mod_cfg_id']); + } + } + + if ($resolved_mod_cfg_id === null) { + $defaultModRow = $db->resultQuery( + "SELECT mod_cfg_id FROM `{$db_prefix}config_mods` + WHERE home_cfg_id=" . intval($home_cfg_id) . " + AND is_default_for_billing=1 + LIMIT 1" + ); + if (!empty($defaultModRow[0]['mod_cfg_id'])) { + $resolved_mod_cfg_id = intval($defaultModRow[0]['mod_cfg_id']); + } + } + + if ($resolved_mod_cfg_id === null) { + $allMods = $db->resultQuery( + "SELECT mod_cfg_id FROM `{$db_prefix}config_mods` + WHERE home_cfg_id=" . intval($home_cfg_id) + ); + if (!empty($allMods) && count($allMods) === 1) { + $resolved_mod_cfg_id = intval($allMods[0]['mod_cfg_id']); + } + } + + if ($resolved_mod_cfg_id === null) { + $errMsg = "No default mod/build configured for game type (home_cfg_id={$home_cfg_id}, order_id={$order_id}). " + . "Visit Admin \u{2192} Game Defaults to mark a mod/build as the billing default."; + billing_log_provision_error($db, $db_prefix, intval($order_id), 0, intval($user_id), $resolved_remote_server_id, $ip_id, 0, 0, $errMsg); + echo "

Provisioning failed for order #" . intval($order_id) . ": " . htmlspecialchars($errMsg) . "

"; + $failed_count++; + continue; + } + + // Use resolved values for the rest of the provisioning flow + $mod_cfg_id = $resolved_mod_cfg_id; + + //Add Game home to database + //HARD CODE TO /home/gameserver/ + $rserver = $db->getRemoteServer($resolved_remote_server_id); + $game_path = "/home/gameserver/"; + $home_id = $db->addGameHome( $resolved_remote_server_id, $user_id, $home_cfg_id, $game_path, $home_name, $remote_control_password, $ftp_password); + + if (!$home_id) { + $errMsg = "Failed to create game home record for order_id={$order_id}, user_id={$user_id}, home_cfg_id={$home_cfg_id}."; + billing_log_provision_error($db, $db_prefix, intval($order_id), 0, intval($user_id), $resolved_remote_server_id, $ip_id, 0, $mod_cfg_id, $errMsg); + echo "

Provisioning failed for order #" . intval($order_id) . ": " . htmlspecialchars($errMsg) . "

"; + $failed_count++; + continue; + } + + // --------------------------------------------------------------- + // Assign next available port to the new server home. + // --------------------------------------------------------------- + $next_port = $db->getNextAvailablePort($ip_id, $home_cfg_id); + if ($next_port === false || $next_port === null) { + $errMsg = "No available port for ip_id={$ip_id}, home_cfg_id={$home_cfg_id} (order_id={$order_id}). " + . "Configure a port range for this IP/game type in the panel."; + $db->deleteGameHome($home_id); + billing_log_provision_error($db, $db_prefix, intval($order_id), 0, intval($user_id), $resolved_remote_server_id, $ip_id, 0, $mod_cfg_id, $errMsg); + echo "

Provisioning failed for order #" . intval($order_id) . ": " . htmlspecialchars($errMsg) . "

"; + $failed_count++; + continue; + } + + $add_port = $db->addGameIpPort($home_id, $ip_id, $next_port); + if (!$add_port) { + $errMsg = "Failed to assign port {$next_port} to home_id={$home_id} (ip_id={$ip_id}, order_id={$order_id})."; + $db->deleteGameHome($home_id); + billing_log_provision_error($db, $db_prefix, intval($order_id), 0, intval($user_id), $resolved_remote_server_id, $ip_id, $next_port, $mod_cfg_id, $errMsg); + echo "

Provisioning failed for order #" . intval($order_id) . ": " . htmlspecialchars($errMsg) . "

"; + $failed_count++; + continue; + } + + //Assign the Game Mod to the Game Home + $mod_id = $db->addModToGameHome( $home_id, $mod_cfg_id ); + if (!$mod_id) { + $errMsg = "Failed to assign mod_cfg_id={$mod_cfg_id} to home_id={$home_id} (order_id={$order_id}). The mod may already be assigned or does not exist."; + // Try to recover the mod_id if it already exists (e.g. duplicate provisioning attempt) + $existingMod = $db->resultQuery( + "SELECT mod_id FROM `{$db_prefix}game_mods` + WHERE home_id=" . intval($home_id) . " + AND mod_cfg_id=" . intval($mod_cfg_id) . " + LIMIT 1" + ); + if (!empty($existingMod[0]['mod_id'])) { + $mod_id = intval($existingMod[0]['mod_id']); + } else { + $db->delGameIpPort($home_id, $ip_id, $next_port); + $db->deleteGameHome($home_id); + billing_log_provision_error($db, $db_prefix, intval($order_id), intval($home_id), intval($user_id), $resolved_remote_server_id, $ip_id, $next_port, $mod_cfg_id, $errMsg); + echo "

Provisioning failed for order #" . intval($order_id) . ": " . htmlspecialchars($errMsg) . "

"; + $failed_count++; + continue; + } + } + $db->updateGameModParams( $max_players, $extra_params, $cpu_affinity, $nice, $home_id, $mod_cfg_id ); + $db->assignHomeTo( "user", $user_id, $home_id, $access_rights ); + + //Get The home info without mods in 1 array (Necesary for remote connection). + $home_info = $db->getGameHomeWithoutMods($home_id); + + //Create the remote connection + $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); + + //Get Full home info in 1 array + $home_info = $db->getGameHome($home_id); + + //Read the Game Config from the XML file + $server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']); + + //Get Values from XML + $modkey = $home_info['mods'][$mod_id]['mod_key']; + $mod_xml = xml_get_mod($server_xml, $modkey); + $installer_name = $mod_xml->installer_name; + $mod_cfg_id = $home_info['mods'][$mod_id]['mod_cfg_id']; + + //Get Preinstall commands from xml + $precmd = $server_xml->pre_install; + + + //Get Postinstall commands from xml + $postcmd = $server_xml->post_install; + + + //Enable FTP account in remote server + if ($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 + $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 ( (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($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); + } + else + { + // 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"); + } + } + echo "


".get_lang('starting_installations')."


"; + //PANEL LOG + $db->logger( "CREATED NEW SERVER " . $home_id); + // SEND EMAIL to new server only + if($order['end_date'] == 0){ + $settings = $db->getSettings(); + $subject = "New Gameserver installed at " . $settings['panel_name']; + $email = $db->resultQuery(" SELECT DISTINCT users_email + FROM {$table_prefix}users, {$table_prefix}billing_orders + WHERE {$table_prefix}users.user_id = $user_id")[0]["users_email"]; + + $message = "Your server, " . $home_name ." ID #". $home_id . " at " . $settings['panel_name'] . " has just been created.
+ Thank You for your continued support.
+ If you have any questions or requests, visit our website or contact us directly in our Discord Server. + You can login to the Game Panel and click on Game Monitor to see your server.

+ Thank you!
"; + $mail = mymail($email, $subject, $message, $settings); + $rundate = date('d/M/y G:i', is_numeric($now) ? (int)$now : strtotime($now)); + + if (!$mail) + $db->logger( "Email FAILED - Server Created " . $home_id); + + + //WEBHOOK Discord + discordmsg(array('content' => "A new server, ". $home_name ." ID #". $home_id . ", has just been created."), $settings['discord_webhook_main'] ?? ''); + //end WEBHOOK Discord + } + // END EMAIL + + + } + // Set expiration date in panel database + // Status values: Active (provisioned & current), Invoiced (renewal invoice open), + // Expired (past due and awaiting deletion) + // end_date / next_invoice_date: when the next renewal invoice should be generated + if ($alreadyProvisioned) + { + $existing_end = strtotime((string)($order['end_date'] ?? '')); + if ($existing_end === false || $existing_end <= 0) { + $existing_end = time(); + } + $end_date_str = date('Y-m-d H:i:s', $existing_end); + } + elseif ($order['invoice_duration'] == "day") + { + + if(empty($order['end_date']) || $order['end_date'] === NULL){ + $end_date = strtotime('+'.$order['qty'].' 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('+'.$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); + } + + // Set order status to 'Active' (server provisioned and current) + $db->query("UPDATE `{$db_prefix}billing_orders` + SET status='Active' + WHERE order_id=".$db->realEscapeSingle($order_id)); + + // Set the order expiration / next renewal date + $db->query("UPDATE `{$db_prefix}billing_orders` + SET end_date='" . $db->realEscapeSingle($end_date_str) . "', + remote_control_password='" . $db->realEscapeSingle($remote_control_password) . "', + ftp_password='" . $db->realEscapeSingle($ftp_password) . "' + WHERE order_id=".$db->realEscapeSingle($order_id)); + + // Save home_id created by this order + $db->query("UPDATE `{$db_prefix}billing_orders` + SET home_id='" . $db->realEscapeSingle($home_id) . "' WHERE order_id=".$db->realEscapeSingle($order_id)); + + $db->query("UPDATE `{$db_prefix}billing_invoices` + SET home_id=" . $db->realEscapeSingle($home_id) . ", + billing_status='Active' + WHERE order_id=" . $db->realEscapeSingle($order_id)); + + $db->query("UPDATE `{$db_prefix}billing_transactions` + SET home_id=" . $db->realEscapeSingle($home_id) . " + WHERE invoice_id IN (SELECT invoice_id FROM `{$db_prefix}billing_invoices` WHERE order_id=" . $db->realEscapeSingle($order_id) . ")"); + + // Set billing_status and next_invoice_date on server_homes + $db->query("UPDATE `{$db_prefix}server_homes` + SET billing_status = 'Active', + next_invoice_date = '" . $db->realEscapeSingle($end_date_str) . "', + billing_enabled = 1 + WHERE home_id = " . $db->realEscapeSingle($home_id)); + + $provisioned_count++; + + } + + $db->query( "UPDATE `{$db_prefix}game_mods` SET max_players= ".$order['max_players']." WHERE home_id=".$db->realEscapeSingle($home_id)); + + // Show results and redirect + if ($provisioned_count > 0) { + echo "
"; + echo "

Server Provisioning Complete

"; + echo "

Successfully provisioned $provisioned_count server(s). Your server(s) are now active.

"; + echo "
"; + echo "

View My Servers

"; + // Auto-redirect after 3 seconds + echo ""; + } else { + echo "
"; + echo "

No servers to provision. All orders have already been processed.

"; + echo "
"; + echo "

View My Orders

"; + } + + } else { + echo "
"; + echo "

No paid orders found to provision.

"; + echo "
"; + echo "

View My Orders

"; + $provisioned_count = 0; + $failed_count = 0; + } + $GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array( + 'provisioned_count' => isset($provisioned_count) ? $provisioned_count : 0, + 'failed_count' => isset($failed_count) ? $failed_count : 0, + 'orders' => $processed_orders, + ); +} +?> + function exec_ogp_module() { global $db,$view,$settings,$table_prefix; diff --git a/modules/billing/module.php b/modules/billing/module.php index 307f8654..fce6f552 100644 --- a/modules/billing/module.php +++ b/modules/billing/module.php @@ -25,7 +25,7 @@ // Module general information $module_title = "billing"; $module_version = "3.6"; -$db_version = 7; +$db_version = 8; $module_required = FALSE; // Module description $module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management."; @@ -422,4 +422,39 @@ $install_queries[7] = array( }, ); +// ----------------------------------------------------------------------- +// db_version 8 — Provisioning error logging and default mod/build support. +// (a) billing_provisioning_errors: records every failed auto-provision +// attempt so admins can diagnose port/mod issues without digging +// through PHP logs. +// (b) config_mods.is_default_for_billing: admins can mark exactly one +// mod/build per game as the automatic billing install default. +// Both changes are safe to re-run (IF NOT EXISTS / INFORMATION_SCHEMA). +// ----------------------------------------------------------------------- +$install_queries[8] = array( + // (a) Create billing_provisioning_errors table + "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."billing_provisioning_errors` ( + `error_id` INT(11) NOT NULL AUTO_INCREMENT, + `billing_order_id` INT(11) NOT NULL DEFAULT 0, + `home_id` INT(11) NOT NULL DEFAULT 0, + `user_id` INT(11) NOT NULL DEFAULT 0, + `remote_server_id` INT(11) NOT NULL DEFAULT 0, + `ip_id` INT(11) NOT NULL DEFAULT 0, + `attempted_port` INT(11) NOT NULL DEFAULT 0, + `mod_cfg_id` INT(11) NOT NULL DEFAULT 0, + `failure_message` TEXT NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`error_id`), + KEY `billing_order_id` (`billing_order_id`), + KEY `created_at` (`created_at`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;", + + // (b) Add is_default_for_billing to config_mods if missing + function($db) { + $r = $db->resultQuery("SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'OGP_DB_PREFIXconfig_mods' AND COLUMN_NAME = 'is_default_for_billing'"); + if ($r && isset($r[0]['cnt']) && (int)$r[0]['cnt'] > 0) return true; + return (bool)$db->query("ALTER TABLE `OGP_DB_PREFIXconfig_mods` ADD `is_default_for_billing` TINYINT(1) NOT NULL DEFAULT 0"); + }, +); + ?> \ No newline at end of file diff --git a/modules/billing/navigation.xml b/modules/billing/navigation.xml index 82cb8dd8..08333f80 100644 --- a/modules/billing/navigation.xml +++ b/modules/billing/navigation.xml @@ -13,6 +13,10 @@ These pages are accessible within the panel for server provisioning and manageme My Orders + + Game Mod Defaults + + Manage All Orders