Rollback OS auto-switching and enforce exact service provisioning
Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/3df8435d-6b14-494a-b81e-ca5ca02f3332 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
parent
8537c4f0f7
commit
87678609ab
9 changed files with 317 additions and 165 deletions
|
|
@ -1,6 +1,7 @@
|
|||
# Changelog
|
||||
|
||||
## 2026-05-09
|
||||
- **Billing OS-variant rollback + exact-service provisioning:** Removed storefront canonical Linux/Windows deduping and order-time OS service auto-switching so each enabled `billing_services` row is sold as its own variant. Checkout/provision now preserve the exact selected `service_id`/`home_cfg_id`/XML, enforce location OS compatibility without silent swapping, log selected XML context in provisioning traces, and show “Server installation is in progress.” instead of immediate executable-missing wording while update installs are active. Also replaced deprecated `utf8_encode()` usage in `modules/gamemanager/view_server_log.php` for PHP 8.3 compatibility.
|
||||
- **Mandatory provisioning trace log + existing-home install retry:** Added visible fail-closed trace logging to `modules/billing/logs/provisioning_trace.log`, threaded detailed per-order provisioning results into checkout success flows, and changed billing provisioning so `Active` orders with an existing `home_id` only skip when the install is already complete. Incomplete existing homes now keep allocating missing IP/mod data and re-use `gamemanager_trigger_update_install()` with traced inputs/outputs instead of silently requiring a manual Game Monitor update.
|
||||
- **Billing provisioning auto-run + idempotency hardening:** Updated paid capture, free checkout, PayPal webhook renewals, and admin add-home registration to always hand activated orders to `billing_invoke_provision()` in trusted internal context. `modules/billing/create_servers.php` now enforces 6-character alphanumeric default passwords, defaults new homes to `ftp_status=1`, adds stricter service/node/port/mod validation logging, retries existing `home_id` installs only when incomplete, and skips already-installed homes to prevent duplicate provisioning on refresh/retry paths.
|
||||
- **Game Monitor IP:PORT fallback reliability:** `modules/gamemanager/server_monitor.php` now resolves missing display/connect endpoints from `home_ip_ports` + `remote_server_ips` for each home so newly provisioned servers consistently show IP:PORT even when initial joined row data is incomplete.
|
||||
|
|
|
|||
|
|
@ -13,3 +13,4 @@
|
|||
- Add an admin billing orders "provisioning details" drawer that reads `modules/billing/logs/provisioning.log` and shows the latest mechanism/result/error per order without leaving the panel.
|
||||
- Add an automated end-to-end check that verifies `create_servers.php` skips already-installed homes while still retrying existing-home orders with missing executable/IP-port/mod prerequisites.
|
||||
- Add a repeatable QA fixture that exercises `modules/billing/logs/provisioning_trace.log` writability failures and verifies payment success pages surface the traced provision result for paid and free orders.
|
||||
- Add an admin/serverlist UI badge that shows detected service OS variant (Windows/Linux/Any) from XML metadata next to each purchasable service row.
|
||||
|
|
|
|||
|
|
@ -71,10 +71,41 @@ function billing_rate_from_service(mysqli $db, string $table_prefix, int $servic
|
|||
return $rate;
|
||||
}
|
||||
|
||||
function billing_fail_add_to_cart(string $message, array $context = []): void
|
||||
function billing_detect_service_os(string $cfgFile, string $gameKey): string
|
||||
{
|
||||
$haystack = strtolower(trim($cfgFile !== '' ? $cfgFile : $gameKey));
|
||||
if ($haystack === '') {
|
||||
return 'any';
|
||||
}
|
||||
if (preg_match('/(?:^|[_\\-])(win|windows)(?:[_\\-]|$)/i', $haystack)) {
|
||||
return 'windows';
|
||||
}
|
||||
if (preg_match('/(?:^|[_\\-])linux(?:[_\\-]|$)/i', $haystack)) {
|
||||
return 'linux';
|
||||
}
|
||||
return 'any';
|
||||
}
|
||||
|
||||
function billing_normalize_node_os(string $serverOs): string
|
||||
{
|
||||
$value = strtolower(trim($serverOs));
|
||||
if ($value === '' || $value === 'any') {
|
||||
return 'any';
|
||||
}
|
||||
if (str_starts_with($value, 'win')) {
|
||||
return 'windows';
|
||||
}
|
||||
if (str_starts_with($value, 'lin')) {
|
||||
return 'linux';
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
function billing_fail_add_to_cart(string $message, array $context = [], ?string $redirect = null): void
|
||||
{
|
||||
site_log_error('add_to_cart_failed', array_merge(['message' => $message], $context));
|
||||
header('Location: /cart.php?error=add_to_cart');
|
||||
$target = $redirect ?? '/cart.php?error=add_to_cart';
|
||||
header('Location: ' . $target);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
@ -165,13 +196,21 @@ $service_name = '';
|
|||
$base_rate = 0.0;
|
||||
$slot_min_qty = 1;
|
||||
$slot_max_qty = 1;
|
||||
$service_home_cfg_id = 0;
|
||||
$service_remote_server_csv = '';
|
||||
$service_cfg_file = '';
|
||||
$service_game_key = '';
|
||||
$durationInfo = billing_normalize_duration($invoice_duration);
|
||||
if ($service_id > 0) {
|
||||
$stmt = $db->prepare("SELECT service_name, price_monthly, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
|
||||
$stmt = $db->prepare("SELECT bs.service_name, bs.price_monthly, bs.slot_min_qty, bs.slot_max_qty, bs.home_cfg_id, bs.remote_server_id, ch.home_cfg_file, ch.game_key
|
||||
FROM {$table_prefix}billing_services bs
|
||||
LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id
|
||||
WHERE bs.service_id = ? AND bs.enabled = 1
|
||||
LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('i', $service_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($service_name, $price_monthly, $slot_min_qty, $slot_max_qty);
|
||||
$stmt->bind_result($service_name, $price_monthly, $slot_min_qty, $slot_max_qty, $service_home_cfg_id, $service_remote_server_csv, $service_cfg_file, $service_game_key);
|
||||
if ($stmt->fetch()) {
|
||||
$base_rate = floatval($price_monthly);
|
||||
// constrain slots
|
||||
|
|
@ -182,6 +221,61 @@ if ($service_id > 0) {
|
|||
}
|
||||
}
|
||||
|
||||
if ($service_id <= 0 || $base_rate < 0) {
|
||||
billing_fail_add_to_cart('Invalid service selection', ['service_id' => $service_id]);
|
||||
}
|
||||
|
||||
if ($service_name === '') {
|
||||
billing_fail_add_to_cart('Selected service is not available', ['service_id' => $service_id], '/serverlist.php');
|
||||
}
|
||||
|
||||
if ($ip_id <= 0) {
|
||||
billing_fail_add_to_cart('No location selected', ['service_id' => $service_id], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode('Please select a server location.'));
|
||||
}
|
||||
|
||||
$allowedServerIds = [];
|
||||
foreach (explode(',', (string)$service_remote_server_csv) as $part) {
|
||||
$part = trim($part);
|
||||
if ($part !== '' && ctype_digit($part)) {
|
||||
$allowedServerIds[(int)$part] = true;
|
||||
}
|
||||
}
|
||||
if (!isset($allowedServerIds[$ip_id])) {
|
||||
billing_fail_add_to_cart('Selected location is not allowed for this service', [
|
||||
'service_id' => $service_id,
|
||||
'ip_id' => $ip_id,
|
||||
'remote_server_csv' => $service_remote_server_csv,
|
||||
], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode('Selected location is not available for this service.'));
|
||||
}
|
||||
|
||||
$hasServerOsColumn = false;
|
||||
$osColCheck = mysqli_query($db, "SHOW COLUMNS FROM {$table_prefix}remote_servers LIKE 'server_os'");
|
||||
if ($osColCheck && mysqli_num_rows($osColCheck) > 0) {
|
||||
$hasServerOsColumn = true;
|
||||
}
|
||||
|
||||
if ($hasServerOsColumn) {
|
||||
$rsQuery = mysqli_query($db, "SELECT remote_server_id, server_os FROM {$table_prefix}remote_servers WHERE remote_server_id = " . intval($ip_id) . " LIMIT 1");
|
||||
if ($rsQuery && mysqli_num_rows($rsQuery) === 1) {
|
||||
$rsRow = mysqli_fetch_assoc($rsQuery);
|
||||
$serviceOs = billing_detect_service_os((string)$service_cfg_file, (string)$service_game_key);
|
||||
$nodeOs = billing_normalize_node_os((string)($rsRow['server_os'] ?? 'any'));
|
||||
if ($serviceOs !== 'any' && $nodeOs !== 'any' && $serviceOs !== $nodeOs) {
|
||||
$message = $serviceOs === 'windows'
|
||||
? 'This service requires a Windows server location.'
|
||||
: 'This service requires a Linux server location.';
|
||||
billing_fail_add_to_cart('Service and node OS mismatch', [
|
||||
'service_id' => $service_id,
|
||||
'home_cfg_id' => $service_home_cfg_id,
|
||||
'cfg_file' => $service_cfg_file,
|
||||
'node_os' => $nodeOs,
|
||||
], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode($message));
|
||||
}
|
||||
} else {
|
||||
billing_fail_add_to_cart('Selected remote server not found', ['service_id' => $service_id, 'ip_id' => $ip_id], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode('Selected server location no longer exists.'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($base_rate <= 0 && $display_service_id > 0) {
|
||||
$fallback_rate = billing_rate_from_service($db, $table_prefix, $display_service_id, $durationInfo['rate_type']);
|
||||
if ($fallback_rate > 0) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
.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; min-width: 240px; max-width: 280px; }
|
||||
.servers-cell { text-align: left; min-width: 160px; max-width: 220px; width: 220px; }
|
||||
.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 {
|
||||
|
|
|
|||
|
|
@ -229,9 +229,13 @@ if (!function_exists('billing_detect_install_state')) {
|
|||
$state['exec_path'] = $execPath;
|
||||
$state['exec_exists'] = ($remote->rfile_exists($execPath) === 1);
|
||||
$state['complete'] = $state['exec_exists'];
|
||||
$state['reason'] = $state['exec_exists']
|
||||
? 'Expected executable already exists on the remote server.'
|
||||
: 'Expected executable is missing on the remote server.';
|
||||
if ($state['exec_exists']) {
|
||||
$state['reason'] = 'Expected executable already exists on the remote server.';
|
||||
} elseif (!empty($state['update_active'])) {
|
||||
$state['reason'] = 'Server installation is in progress.';
|
||||
} else {
|
||||
$state['reason'] = 'Expected executable is missing on the remote server.';
|
||||
}
|
||||
return $state;
|
||||
}
|
||||
}
|
||||
|
|
@ -288,6 +292,53 @@ if (!function_exists('billing_agent_offline_reason')) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!function_exists('billing_detect_service_os')) {
|
||||
function billing_detect_service_os(string $cfg_file, string $game_key): string
|
||||
{
|
||||
$haystack = strtolower(trim($cfg_file !== '' ? $cfg_file : $game_key));
|
||||
if ($haystack === '') {
|
||||
return 'any';
|
||||
}
|
||||
if (preg_match('/(?:^|[_\\-])(win|windows)(?:[_\\-]|$)/i', $haystack)) {
|
||||
return 'windows';
|
||||
}
|
||||
if (preg_match('/(?:^|[_\\-])linux(?:[_\\-]|$)/i', $haystack)) {
|
||||
return 'linux';
|
||||
}
|
||||
return 'any';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('billing_normalize_node_os')) {
|
||||
function billing_normalize_node_os(string $server_os): string
|
||||
{
|
||||
$value = strtolower(trim($server_os));
|
||||
if ($value === '' || $value === 'any') {
|
||||
return 'any';
|
||||
}
|
||||
if (str_starts_with($value, 'win')) {
|
||||
return 'windows';
|
||||
}
|
||||
if (str_starts_with($value, 'lin')) {
|
||||
return 'linux';
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('billing_remote_servers_has_os_column')) {
|
||||
function billing_remote_servers_has_os_column($db, string $db_prefix): bool
|
||||
{
|
||||
static $cache = array();
|
||||
if (isset($cache[$db_prefix])) {
|
||||
return $cache[$db_prefix];
|
||||
}
|
||||
$rows = $db->resultQuery("SHOW COLUMNS FROM `{$db_prefix}remote_servers` LIKE 'server_os'");
|
||||
$cache[$db_prefix] = !empty($rows);
|
||||
return $cache[$db_prefix];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('billing_invoke_provision')) {
|
||||
function billing_invoke_provision(array $options = array())
|
||||
{
|
||||
|
|
@ -681,6 +732,11 @@ function exec_ogp_module()
|
|||
$selected_port = 0;
|
||||
$selected_mod_id = 0;
|
||||
$resolved_mod_cfg_id = 0;
|
||||
$home_cfg_id = 0;
|
||||
$mod_cfg_id = 0;
|
||||
$selected_config_xml = '';
|
||||
$selected_game_key = '';
|
||||
$selected_service_os = 'any';
|
||||
$install_mechanism = BILLING_INSTALL_MECHANISM;
|
||||
$install_result = 'pending';
|
||||
$install_message = '';
|
||||
|
|
@ -704,9 +760,12 @@ function exec_ogp_module()
|
|||
}
|
||||
billing_provision_trace('Resolved latest invoice row for order.', array('invoice_row' => $invoiceRow));
|
||||
//Query service info
|
||||
$service = $db->resultQuery( "SELECT *
|
||||
FROM `{$db_prefix}billing_services`
|
||||
WHERE service_id=".$db->realEscapeSingle($service_id) );
|
||||
$service = $db->resultQuery(
|
||||
"SELECT bs.*, ch.home_cfg_file, ch.game_key
|
||||
FROM `{$db_prefix}billing_services` bs
|
||||
LEFT JOIN `{$db_prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id
|
||||
WHERE bs.service_id=" . $db->realEscapeSingle($service_id)
|
||||
);
|
||||
billing_provision_trace('Loaded billing service row.', array(
|
||||
'service_id' => intval($service_id),
|
||||
'service_row_found' => !empty($service[0]),
|
||||
|
|
@ -717,6 +776,9 @@ function exec_ogp_module()
|
|||
{
|
||||
$home_cfg_id = $service[0]['home_cfg_id'];
|
||||
$mod_cfg_id = $service[0]['mod_cfg_id'];
|
||||
$selected_config_xml = (string)($service[0]['home_cfg_file'] ?? '');
|
||||
$selected_game_key = (string)($service[0]['game_key'] ?? '');
|
||||
$selected_service_os = billing_detect_service_os($selected_config_xml, $selected_game_key);
|
||||
//remote_server_id has been stored in IP_ID
|
||||
//$remote_server_id = $service[0]['remote_server_id'];
|
||||
$remote_server_id = $order['ip'];
|
||||
|
|
@ -730,6 +792,8 @@ function exec_ogp_module()
|
|||
'order_status' => $order['status'] ?? '',
|
||||
'order_home_id_before_provisioning' => intval($order['home_id'] ?? 0),
|
||||
'selected_home_cfg_id' => intval($home_cfg_id),
|
||||
'selected_config_xml' => $selected_config_xml,
|
||||
'selected_service_os' => $selected_service_os,
|
||||
'selected_remote_server_id' => intval($remote_server_id),
|
||||
));
|
||||
if (intval($home_cfg_id) <= 0) {
|
||||
|
|
@ -740,6 +804,44 @@ function exec_ogp_module()
|
|||
$order_failed = true;
|
||||
$order_failure_reason = "Invalid remote server selection '{$remote_server_id}' on order #{$order_id} for service_id {$service_id}.";
|
||||
}
|
||||
if (!$order_failed) {
|
||||
$allowedRemote = array();
|
||||
foreach (explode(',', (string)($service[0]['remote_server_id'] ?? '')) as $part) {
|
||||
$part = trim($part);
|
||||
if ($part !== '' && ctype_digit($part)) {
|
||||
$allowedRemote[(int)$part] = true;
|
||||
}
|
||||
}
|
||||
if (!empty($allowedRemote) && !isset($allowedRemote[intval($remote_server_id)])) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Selected remote server #{$remote_server_id} is not enabled for service_id {$service_id}.";
|
||||
}
|
||||
}
|
||||
if (!$order_failed && billing_remote_servers_has_os_column($db, $db_prefix)) {
|
||||
$remoteRow = $db->resultQuery(
|
||||
"SELECT remote_server_id, remote_server_name, server_os
|
||||
FROM `{$db_prefix}remote_servers`
|
||||
WHERE remote_server_id=" . $db->realEscapeSingle($remote_server_id) . "
|
||||
LIMIT 1"
|
||||
);
|
||||
if (empty($remoteRow[0])) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Remote server #{$remote_server_id} not found for order #{$order_id} (service_id {$service_id}).";
|
||||
} else {
|
||||
$node_os = billing_normalize_node_os((string)($remoteRow[0]['server_os'] ?? 'any'));
|
||||
billing_provision_trace('Resolved remote server OS for compatibility check.', array(
|
||||
'selected_remote_server_id' => intval($remote_server_id),
|
||||
'selected_node_os' => $node_os,
|
||||
'selected_service_os' => $selected_service_os,
|
||||
));
|
||||
if ($selected_service_os !== 'any' && $node_os !== 'any' && $selected_service_os !== $node_os) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = $selected_service_os === 'windows'
|
||||
? 'This service requires a Windows server location.'
|
||||
: 'This service requires a Linux server location.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1291,8 +1393,10 @@ function exec_ogp_module()
|
|||
'order_id' => intval($order_id),
|
||||
'invoice_id' => intval($provision_invoice_id),
|
||||
'user_id' => intval($user_id),
|
||||
'service_id' => intval($service_id),
|
||||
'home_id' => intval($home_id),
|
||||
'home_cfg_id' => intval($home_cfg_id ?? 0),
|
||||
'config_xml' => (string)$selected_config_xml,
|
||||
'mod_id' => intval($selected_mod_id),
|
||||
'ip_id' => intval($selected_ip_id),
|
||||
'port' => intval($selected_port),
|
||||
|
|
@ -1307,8 +1411,10 @@ function exec_ogp_module()
|
|||
'BILLING PROVISION RESULT order_id=' . intval($order_id)
|
||||
. ' invoice_id=' . intval($provision_invoice_id)
|
||||
. ' user_id=' . intval($user_id)
|
||||
. ' service_id=' . intval($service_id)
|
||||
. ' home_id=' . intval($home_id)
|
||||
. ' home_cfg_id=' . intval($home_cfg_id ?? 0)
|
||||
. ' config_xml=' . (string)$selected_config_xml
|
||||
. ' mod_id=' . intval($selected_mod_id)
|
||||
. ' ip_id=' . intval($selected_ip_id)
|
||||
. ' port=' . intval($selected_port)
|
||||
|
|
|
|||
|
|
@ -64,9 +64,8 @@ This is the "order gameserver" page. It displays the options for a single specif
|
|||
has the "Add to Cart" button. The gameserver selected is passed from the serverlist page by a GET
|
||||
of the service_id. When the user clicks "Add to Cart", the next page is add_to_cart.php.
|
||||
|
||||
OS-aware selection: if both a Linux and a Windows variant of the same game exist as separate
|
||||
billing_services entries, the system automatically detects the selected location's OS (from
|
||||
remote_servers.server_os) and routes the cart add to the correct service variant.
|
||||
Each enabled billing service row is listed and purchased as its own exact variant.
|
||||
The selected service_id remains the source of truth for checkout and provisioning.
|
||||
*/
|
||||
|
||||
// Require login for ordering
|
||||
|
|
@ -103,35 +102,35 @@ if (isset($_POST['save']) && !empty($_POST['description'])) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive OS ('linux'|'windows'|'any') from a game_key string.
|
||||
* Checks for _win / _windows substrings; then _linux; else 'any'.
|
||||
*/
|
||||
function order_game_key_os(string $gameKey): string
|
||||
{
|
||||
$lk = strtolower($gameKey);
|
||||
if (str_contains($lk, '_win')) {
|
||||
return 'windows';
|
||||
}
|
||||
if (str_contains($lk, '_linux')) {
|
||||
return 'linux';
|
||||
}
|
||||
return 'any';
|
||||
}
|
||||
|
||||
function order_price_is_free($value): bool
|
||||
{
|
||||
return ((int) round(((float)$value) * 100)) === 0;
|
||||
}
|
||||
|
||||
function order_canonical_game_key(string $gameKey): string
|
||||
function order_detect_service_os(string $cfgFile, string $gameKey): string
|
||||
{
|
||||
$gameKey = strtolower(trim($gameKey));
|
||||
if ($gameKey === '') {
|
||||
return '';
|
||||
$haystack = strtolower(trim($cfgFile !== '' ? $cfgFile : $gameKey));
|
||||
if ($haystack === '') {
|
||||
return 'any';
|
||||
}
|
||||
$canonical = preg_replace('/_(linux|linux32|linux64|win|win32|win64|windows|windows32|windows64)$/i', '', $gameKey);
|
||||
return $canonical !== '' ? $canonical : $gameKey;
|
||||
if (preg_match('/(?:^|[_\\-])(win|windows)(?:[_\\-]|$)/i', $haystack)) {
|
||||
return 'windows';
|
||||
}
|
||||
if (preg_match('/(?:^|[_\\-])linux(?:[_\\-]|$)/i', $haystack)) {
|
||||
return 'linux';
|
||||
}
|
||||
return 'any';
|
||||
}
|
||||
|
||||
function order_variant_label(string $serviceOs): string
|
||||
{
|
||||
if ($serviceOs === 'windows') {
|
||||
return 'Windows';
|
||||
}
|
||||
if ($serviceOs === 'linux') {
|
||||
return 'Linux';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// --- Fetch the requested service with config_homes join for canonical game info ---
|
||||
|
|
@ -142,7 +141,7 @@ if ($req_service_id !== 0) {
|
|||
$where_service_id = " WHERE bs.enabled = 1";
|
||||
}
|
||||
|
||||
$qry_services = "SELECT bs.*, ch.game_name AS cfg_game_name, ch.game_key AS cfg_game_key
|
||||
$qry_services = "SELECT bs.*, ch.game_name AS cfg_game_name, ch.game_key AS cfg_game_key, ch.home_cfg_file AS cfg_file
|
||||
FROM {$table_prefix}billing_services bs
|
||||
LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id
|
||||
{$where_service_id}
|
||||
|
|
@ -152,10 +151,10 @@ $services_result = $db->query($qry_services);
|
|||
if ($services_result === false) {
|
||||
// Fallback: query without join if config_homes doesn't exist in this context
|
||||
$where_service_id_simple = str_replace('bs.', '', $where_service_id);
|
||||
$qry_services = "SELECT *, NULL AS cfg_game_name, NULL AS cfg_game_key
|
||||
FROM {$table_prefix}billing_services
|
||||
{$where_service_id_simple}
|
||||
ORDER BY service_name";
|
||||
$qry_services = "SELECT *, NULL AS cfg_game_name, NULL AS cfg_game_key, NULL AS cfg_file
|
||||
FROM {$table_prefix}billing_services
|
||||
{$where_service_id_simple}
|
||||
ORDER BY service_name";
|
||||
$services_result = $db->query($qry_services);
|
||||
}
|
||||
|
||||
|
|
@ -193,6 +192,8 @@ if ($osColCheck && $osColCheck->num_rows > 0) {
|
|||
$osColCheck->free();
|
||||
}
|
||||
|
||||
$order_error_message = isset($_GET['error_message']) ? trim((string)$_GET['error_message']) : '';
|
||||
|
||||
?>
|
||||
<div class="order-shell">
|
||||
<div class="clearfix">
|
||||
|
|
@ -227,45 +228,16 @@ echo "$" . number_format(floatval($row['price_monthly']), 2) . " Monthly";
|
|||
}else
|
||||
// THIS IS THE SERVER WE WANT TO ORDER
|
||||
{
|
||||
// Determine canonical game name and OS for this service
|
||||
// Determine exact selected service display and OS label from config metadata.
|
||||
$svcGameKey = (string)($row['cfg_game_key'] ?? '');
|
||||
$svcGameOs = order_game_key_os($svcGameKey);
|
||||
$cfgFile = (string)($row['cfg_file'] ?? '');
|
||||
$svcGameOs = order_detect_service_os($cfgFile, $svcGameKey);
|
||||
$canonicalGameName = (string)($row['cfg_game_name'] ?? $row['service_name']);
|
||||
$canonicalGameKey = order_canonical_game_key($svcGameKey);
|
||||
|
||||
// Build map of OS variant service IDs for JS-based automatic selection.
|
||||
// Look for sibling services that share the same cfg_game_name (canonical) but differ in OS.
|
||||
// e.g. if current service is arma3_linux64, find the arma3_win64 service too.
|
||||
$osServiceMap = []; // ['linux' => service_id, 'windows' => service_id]
|
||||
if ($svcGameOs !== 'any' && (!empty($canonicalGameName) || !empty($canonicalGameKey))) {
|
||||
$siblingQuery = "SELECT bs.service_id, ch.game_key AS cfg_game_key, ch.game_name AS cfg_game_name
|
||||
FROM {$table_prefix}billing_services bs
|
||||
LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id
|
||||
WHERE bs.enabled = 1";
|
||||
$siblingResult = $db->query($siblingQuery);
|
||||
if ($siblingResult) {
|
||||
while ($sib = $siblingResult->fetch_assoc()) {
|
||||
$sibGameKey = (string)($sib['cfg_game_key'] ?? '');
|
||||
$sibCanonical = order_canonical_game_key($sibGameKey);
|
||||
$sibName = (string)($sib['cfg_game_name'] ?? '');
|
||||
if ($canonicalGameKey !== '') {
|
||||
if ($sibCanonical !== $canonicalGameKey) {
|
||||
continue;
|
||||
$variantLabel = order_variant_label($svcGameOs);
|
||||
$displayName = $canonicalGameName;
|
||||
if ($variantLabel !== '' && stripos($displayName, $variantLabel) === false) {
|
||||
$displayName .= ' - ' . $variantLabel;
|
||||
}
|
||||
} elseif ($canonicalGameName !== '' && strcasecmp($sibName, $canonicalGameName) !== 0) {
|
||||
continue;
|
||||
}
|
||||
$sibOs = order_game_key_os((string)($sib['cfg_game_key'] ?? ''));
|
||||
$osServiceMap[$sibOs] = (int)$sib['service_id'];
|
||||
}
|
||||
$siblingResult->free();
|
||||
}
|
||||
}
|
||||
// Always include the current service as a fallback
|
||||
if (!isset($osServiceMap[$svcGameOs]) || $svcGameOs === 'any') {
|
||||
$osServiceMap[$svcGameOs] = (int)$row['service_id'];
|
||||
}
|
||||
$osServiceMapJson = json_encode($osServiceMap, JSON_THROW_ON_ERROR);
|
||||
|
||||
?>
|
||||
<div class="order-layout">
|
||||
|
|
@ -274,9 +246,9 @@ $osServiceMapJson = json_encode($osServiceMap, JSON_THROW_ON_ERROR);
|
|||
$imgSrc = billing_image_url((string)($row['img_url'] ?? ''));
|
||||
if ($imgSrc === '') { $imgSrc = '/images/games/default_server.png'; }
|
||||
?>
|
||||
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" alt="<?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" alt="<?php echo htmlspecialchars($displayName, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
onerror="this.src='/images/games/default_server.png'; this.onerror=null;">
|
||||
<center class="order-media-title"><b><?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?></b></center>
|
||||
<center class="order-media-title"><b><?php echo htmlspecialchars($displayName, ENT_QUOTES, 'UTF-8'); ?></b></center>
|
||||
<?php
|
||||
$isAdmin = false;
|
||||
if ($isAdmin) {
|
||||
|
|
@ -300,9 +272,11 @@ echo "<p class='order-media-desc'>" . htmlspecialchars((string)($row['descriptio
|
|||
?>
|
||||
</div>
|
||||
<div class="order-form-card">
|
||||
<?php if ($order_error_message !== ''): ?>
|
||||
<p class="error"><?php echo htmlspecialchars($order_error_message, ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
<?php endif; ?>
|
||||
<table class="order-form-table">
|
||||
<form method="post" action="add_to_cart.php">
|
||||
<!-- service_id is updated by JS when the location OS changes -->
|
||||
<input type="hidden" id="order_service_id" name="service_id" value="<?php echo intval($row['service_id']); ?>">
|
||||
<input type="hidden" name="display_service_id" value="<?php echo intval($row['service_id']); ?>">
|
||||
<input type="hidden" name="remote_control_password" value="">
|
||||
|
|
@ -318,13 +292,9 @@ echo "<p class='order-media-desc'>" . htmlspecialchars((string)($row['descriptio
|
|||
<td align="right"><b>Location</b></td>
|
||||
<td align="left">
|
||||
<?php
|
||||
// Fetch servers available for this game from billing_services.remote_server_id
|
||||
// (a comma-separated list of numeric remote server IDs, e.g. "1,3,7").
|
||||
// When OS-aware: also collect sibling service's allowed IDs to show all compatible locations.
|
||||
// Fetch servers available for this exact selected service from billing_services.remote_server_id.
|
||||
$available_server = false;
|
||||
$remoteIdsCsv = (string)($row['remote_server_id'] ?? '');
|
||||
|
||||
// Also gather allowed IDs from sibling OS-variant services
|
||||
$allAllowedIds = [];
|
||||
foreach (explode(',', $remoteIdsCsv) as $part) {
|
||||
$part = trim($part);
|
||||
|
|
@ -332,23 +302,6 @@ if ($part !== '' && ctype_digit($part)) {
|
|||
$allAllowedIds[] = (int)$part;
|
||||
}
|
||||
}
|
||||
// Add IDs from sibling service variants so locations appear regardless of which
|
||||
// service variant is the "primary" one shown to the user.
|
||||
if (count($osServiceMap) > 1) {
|
||||
foreach ($osServiceMap as $_os => $sibSvcId) {
|
||||
if ($sibSvcId === (int)$row['service_id']) continue;
|
||||
$sibRow = $db->query("SELECT remote_server_id FROM {$table_prefix}billing_services WHERE service_id = " . intval($sibSvcId) . " LIMIT 1");
|
||||
if ($sibRow && ($sibData = $sibRow->fetch_assoc())) {
|
||||
foreach (explode(',', (string)($sibData['remote_server_id'] ?? '')) as $part) {
|
||||
$part = trim($part);
|
||||
if ($part !== '' && ctype_digit($part)) {
|
||||
$allAllowedIds[] = (int)$part;
|
||||
}
|
||||
}
|
||||
$sibRow->free();
|
||||
}
|
||||
}
|
||||
}
|
||||
$allAllowedIds = array_unique($allAllowedIds);
|
||||
|
||||
if (!empty($allAllowedIds)) {
|
||||
|
|
@ -365,18 +318,17 @@ $firstServer = true;
|
|||
while ($rs = $rsResult->fetch_assoc()) {
|
||||
$rsID = (int)$rs['remote_server_id'];
|
||||
$rsNAME = htmlspecialchars((string)$rs['remote_server_name'], ENT_QUOTES, 'UTF-8');
|
||||
$rsOs = (string)($rs['server_os'] ?? 'any');
|
||||
$rsOsRaw = strtolower((string)($rs['server_os'] ?? 'any'));
|
||||
$rsOs = str_starts_with($rsOsRaw, 'win') ? 'windows' : (str_starts_with($rsOsRaw, 'lin') ? 'linux' : ($rsOsRaw === '' ? 'any' : $rsOsRaw));
|
||||
$checked = $firstServer ? ' checked' : '';
|
||||
// Skip this location if we know the service is OS-specific and the
|
||||
// node OS is incompatible AND no sibling service covers this OS.
|
||||
if ($svcGameOs !== 'any' && $rsOs !== 'any' && $rsOs !== $svcGameOs && !isset($osServiceMap[$rsOs])) {
|
||||
continue; // Incompatible OS variant with no fallback service
|
||||
if ($svcGameOs !== 'any' && $rsOs !== 'any' && $rsOs !== $svcGameOs) {
|
||||
continue;
|
||||
}
|
||||
$available_server = true;
|
||||
$firstServer = false;
|
||||
$safeOs = htmlspecialchars($rsOs, ENT_QUOTES, 'UTF-8');
|
||||
echo "<div class='location-option'>\n"
|
||||
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' data-os='{$safeOs}' required{$checked} onchange='gspUpdateServiceId(this)'>\n"
|
||||
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' data-os='{$safeOs}' required{$checked}>\n"
|
||||
. " <label for='rs_{$rsID}'>{$rsNAME}</label>\n"
|
||||
. "</div>\n";
|
||||
}
|
||||
|
|
@ -410,9 +362,6 @@ var price = document.getElementById("totalPrice");
|
|||
var invoiceDuration = document.getElementById("invoiceDuration");
|
||||
var pricePerSlot = <?php echo number_format(floatval($row['price_monthly']), 2, '.', ''); ?>;
|
||||
|
||||
// OS-aware service variant map: {os: service_id}
|
||||
var osServiceMap = <?php echo $osServiceMapJson; ?>;
|
||||
|
||||
function recalc() {
|
||||
var slots = parseInt(slider.value, 10);
|
||||
var months = parseInt(invoiceslider.value, 10);
|
||||
|
|
@ -428,24 +377,6 @@ totalInput.value = total;
|
|||
recalc();
|
||||
slider.oninput = recalc;
|
||||
invoiceslider.oninput = recalc;
|
||||
|
||||
// Update the hidden service_id based on the selected location's OS.
|
||||
window.gspUpdateServiceId = function(radio) {
|
||||
var os = radio.getAttribute('data-os') || 'any';
|
||||
var svcInput = document.getElementById('order_service_id');
|
||||
if (!svcInput) return;
|
||||
// Pick the service for this OS, fall back to 'any', then first available
|
||||
if (osServiceMap[os] !== undefined) {
|
||||
svcInput.value = osServiceMap[os];
|
||||
} else if (osServiceMap['any'] !== undefined) {
|
||||
svcInput.value = osServiceMap['any'];
|
||||
}
|
||||
// else keep the current value
|
||||
};
|
||||
|
||||
// Trigger on page load for the pre-checked radio
|
||||
var checked = document.querySelector('input[name="ip_id"]:checked');
|
||||
if (checked) { window.gspUpdateServiceId(checked); }
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
|
@ -466,7 +397,17 @@ $is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['websit
|
|||
<?php elseif (!$is_logged_in): ?>
|
||||
<div class="login-placeholder">Please <a href="login.php">login</a> to order</div>
|
||||
<?php else: ?>
|
||||
<p class="error">No available server locations for this game.</p>
|
||||
<p class="error">
|
||||
<?php
|
||||
if ($svcGameOs === 'windows') {
|
||||
echo 'This service requires a Windows server location.';
|
||||
} elseif ($svcGameOs === 'linux') {
|
||||
echo 'This service requires a Linux server location.';
|
||||
} else {
|
||||
echo 'No available server locations for this service.';
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -28,15 +28,19 @@ function billing_service_price_is_free($value): bool
|
|||
return ((int) round(((float)$value) * 100)) === 0;
|
||||
}
|
||||
|
||||
function billing_canonical_game_identity(array $row): string
|
||||
function billing_detect_variant_label(array $row): string
|
||||
{
|
||||
$gameKey = strtolower(trim((string)($row['cfg_game_key'] ?? '')));
|
||||
if ($gameKey !== '') {
|
||||
$canonicalKey = preg_replace('/_(linux|linux32|linux64|win|win32|win64|windows|windows32|windows64)$/i', '', $gameKey);
|
||||
return 'key:' . ($canonicalKey !== '' ? $canonicalKey : $gameKey);
|
||||
$haystack = strtolower(trim((string)($row['cfg_file'] ?? $row['cfg_game_key'] ?? '')));
|
||||
if ($haystack === '') {
|
||||
return '';
|
||||
}
|
||||
$gameName = strtolower(trim((string)($row['cfg_game_name'] ?? $row['service_name'] ?? '')));
|
||||
return 'name:' . $gameName;
|
||||
if (preg_match('/(?:^|[_\-])(win|windows)(?:[_\-]|$)/i', $haystack)) {
|
||||
return 'Windows';
|
||||
}
|
||||
if (preg_match('/(?:^|[_\-])linux(?:[_\-]|$)/i', $haystack)) {
|
||||
return 'Linux';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Save new description if admin
|
||||
|
|
@ -49,17 +53,16 @@ if (isset($_POST['save']) && !empty($_POST['description'])) {
|
|||
$stmt->close();
|
||||
}
|
||||
|
||||
// Fetch services, joining config_homes to get canonical game_name and game_key for OS detection.
|
||||
// LEFT JOIN so services without a linked config_homes entry still appear.
|
||||
// Fetch enabled services, keeping one row per billing service.
|
||||
$service_id = isset($_REQUEST['service_id']) ? intval($_REQUEST['service_id']) : 0;
|
||||
if ($service_id !== 0) {
|
||||
$where_clause = "WHERE bs.enabled = 1 AND bs.service_id = {$service_id} AND bs.remote_server_id != '' AND bs.remote_server_id IS NOT NULL";
|
||||
} else {
|
||||
$where_clause = "WHERE bs.enabled = 1 AND bs.remote_server_id != '' AND bs.remote_server_id IS NOT NULL";
|
||||
}
|
||||
$qry_services = "SELECT bs.*, ch.game_name AS cfg_game_name, ch.game_key AS cfg_game_key
|
||||
$qry_services = "SELECT bs.*, ch.game_name AS cfg_game_name, ch.game_key AS cfg_game_key, ch.home_cfg_file AS cfg_file
|
||||
FROM {$table_prefix}billing_services bs
|
||||
LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id
|
||||
LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id
|
||||
{$where_clause}
|
||||
ORDER BY bs.service_name";
|
||||
$result_services = $db->query($qry_services);
|
||||
|
|
@ -70,10 +73,10 @@ if (!$result_services) {
|
|||
$qry_services_fallback = "SELECT service_id, home_cfg_id, enabled, service_name, description,
|
||||
img_url, price_monthly, slot_min_qty, slot_max_qty,
|
||||
remote_server_id,
|
||||
NULL AS cfg_game_name, NULL AS cfg_game_key
|
||||
NULL AS cfg_game_name, NULL AS cfg_game_key, NULL AS cfg_file
|
||||
FROM {$table_prefix}billing_services
|
||||
{$where_clause_fallback}
|
||||
ORDER BY service_name";
|
||||
{$where_clause_fallback}
|
||||
ORDER BY service_name";
|
||||
$result_services = $db->query($qry_services_fallback);
|
||||
}
|
||||
|
||||
|
|
@ -83,25 +86,8 @@ if (!$result_services) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Fetch all service rows and deduplicate by canonical game name so that
|
||||
// arma3_linux64 and arma3_win64 (both named "Arma 3") appear only once.
|
||||
// When a specific service_id is requested we skip deduplication.
|
||||
$serviceRows = [];
|
||||
$seenCanonical = [];
|
||||
while ($row = $result_services->fetch_assoc()) {
|
||||
if ($service_id !== 0) {
|
||||
// Single-service detail view: always include without deduplication
|
||||
$serviceRows[] = $row;
|
||||
continue;
|
||||
}
|
||||
// Derive canonical display name: prefer config_homes game_name (consistent across OS
|
||||
// variants), fall back to service_name.
|
||||
$canonicalIdentity = billing_canonical_game_identity($row);
|
||||
if (isset($seenCanonical[$canonicalIdentity])) {
|
||||
// Already have this game — skip the duplicate OS variant
|
||||
continue;
|
||||
}
|
||||
$seenCanonical[$canonicalIdentity] = true;
|
||||
$serviceRows[] = $row;
|
||||
}
|
||||
$result_services->free();
|
||||
|
|
@ -126,7 +112,14 @@ include(__DIR__ . '/includes/menu.php');
|
|||
?>
|
||||
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" width="460" height="225"
|
||||
onerror="this.src='/images/games/default_server.png'; this.onerror=null;"><br>
|
||||
<strong><?php echo htmlspecialchars((string)($row['cfg_game_name'] ?? $row['service_name']), ENT_QUOTES, 'UTF-8'); ?></strong><br>
|
||||
<?php
|
||||
$serviceDisplayName = (string)($row['cfg_game_name'] ?? $row['service_name']);
|
||||
$variantLabel = billing_detect_variant_label($row);
|
||||
if ($variantLabel !== '' && stripos($serviceDisplayName, $variantLabel) === false) {
|
||||
$serviceDisplayName .= ' - ' . $variantLabel;
|
||||
}
|
||||
?>
|
||||
<strong><?php echo htmlspecialchars($serviceDisplayName, ENT_QUOTES, 'UTF-8'); ?></strong><br>
|
||||
<?php
|
||||
echo billing_service_price_is_free($row['price_monthly'] ?? 0) ? "FREE" : "$" . number_format((float)$row['price_monthly'], 2) . " Monthly";
|
||||
?>
|
||||
|
|
@ -145,7 +138,14 @@ include(__DIR__ . '/includes/menu.php');
|
|||
?>
|
||||
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" width="230" height="112"
|
||||
onerror="this.src='/images/games/default_server.png'; this.onerror=null;"><br>
|
||||
<center><b><?php echo htmlspecialchars((string)($row['cfg_game_name'] ?? $row['service_name']), ENT_QUOTES, 'UTF-8'); ?></b></center>
|
||||
<?php
|
||||
$detailDisplayName = (string)($row['cfg_game_name'] ?? $row['service_name']);
|
||||
$detailVariantLabel = billing_detect_variant_label($row);
|
||||
if ($detailVariantLabel !== '' && stripos($detailDisplayName, $detailVariantLabel) === false) {
|
||||
$detailDisplayName .= ' - ' . $detailVariantLabel;
|
||||
}
|
||||
?>
|
||||
<center><b><?php echo htmlspecialchars($detailDisplayName, ENT_QUOTES, 'UTF-8'); ?></b></center>
|
||||
|
||||
<?php
|
||||
$isAdmin = false;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Last Updated at 2:43pm on 2026-05-09
|
||||
Last Updated at 4:03pm on 2026-05-09
|
||||
|
|
|
|||
|
|
@ -93,7 +93,16 @@ function exec_ogp_module()
|
|||
{
|
||||
// Force log file contents to be UTF-8 (fixes http://www.opengamepanel.org/forum/viewthread.php?thread_id=5379)
|
||||
if(hasValue($home_log)){
|
||||
$home_log = utf8_encode($home_log);
|
||||
if (function_exists('mb_check_encoding') && function_exists('mb_convert_encoding')) {
|
||||
if (!mb_check_encoding($home_log, 'UTF-8')) {
|
||||
$home_log = mb_convert_encoding($home_log, 'UTF-8', 'ISO-8859-1');
|
||||
}
|
||||
} elseif (function_exists('iconv')) {
|
||||
$converted = @iconv('ISO-8859-1', 'UTF-8//IGNORE', $home_log);
|
||||
if ($converted !== false) {
|
||||
$home_log = $converted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Using the refreshed class
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue