fix: add_to_cart SQL mismatch, Browse Servers routing, canonical game dedup, OS-aware locations, XML editor improvements

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/aecffd5d-b644-4e4d-b13e-b392e78d4606

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-06 23:45:27 +00:00 committed by GitHub
parent 7ff7adca9d
commit acbb850e21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 495 additions and 352 deletions

View file

@ -204,7 +204,7 @@ $sql = "INSERT INTO {$table_prefix}billing_invoices (
billing_status, invoice_date, due_date, description, invoice_duration, rate_type, rate_per_player,
players, period_start, period_end, subtotal, total_due, payment_status, qty, coupon_id
) VALUES (
0, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, 0.00, 'USD', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0
0, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, 0.00, 'USD', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0
)";
$stmt = $db->prepare($sql);

View file

@ -541,7 +541,7 @@ $siteBase = $protocol . $host;
<div class="cart-empty">
<h2>Your cart is empty</h2>
<p>Browse our game servers and add them to your cart to get started!</p>
<a href="/order.php" class="btn">Browse Servers</a>
<a href="/serverlist.php" class="btn">Browse Servers</a>
</div>
<?php else: ?>
<table class="cart-table">
@ -640,7 +640,7 @@ $siteBase = $protocol . $host;
</button>
</form>
<div class="action-buttons" style="margin-top:15px;">
<a href="/order.php" class="btn btn-secondary">Continue Shopping</a>
<a href="/serverlist.php" class="btn btn-secondary">Continue Shopping</a>
<a href="/my_account.php" class="btn btn-secondary">My Account</a>
</div>
</div>
@ -672,7 +672,7 @@ $siteBase = $protocol . $host;
<div id="status-message" class="status-message"></div>
<?php endif; ?>
<div class="action-buttons">
<a href="/order.php" class="btn btn-secondary">Continue Shopping</a>
<a href="/serverlist.php" class="btn btn-secondary">Continue Shopping</a>
<a href="/my_account.php" class="btn btn-secondary">My Account</a>
</div>
</div>

View file

@ -24,8 +24,8 @@
// Module general information
$module_title = "billing";
$module_version = "3.4";
$db_version = 5;
$module_version = "3.5";
$db_version = 6;
$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.";
@ -386,4 +386,17 @@ $install_queries[5] = array(
}
);
?>
// -----------------------------------------------------------------------
// db_version 6 — Add server_os column to remote_servers for OS-aware
// game/service selection in the billing storefront.
// Default 'linux' preserves existing behaviour for all current installs.
// -----------------------------------------------------------------------
$install_queries[6] = array(
function($db) {
$r = $db->resultQuery("SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'OGP_DB_PREFIXremote_servers' AND COLUMN_NAME = 'server_os'");
if ($r && isset($r[0]['cnt']) && (int)$r[0]['cnt'] > 0) return true;
return (bool)$db->query("ALTER TABLE `OGP_DB_PREFIXremote_servers` ADD `server_os` ENUM('linux','windows','any') NOT NULL DEFAULT 'linux' AFTER `display_public_ip`");
}
);
?>

View file

@ -9,15 +9,13 @@
<?php
/*
This is the actual "order gameserver" page. There is a page that displays all the possible game servers we can rent. This page displays the options
for a single specific game server and has the "Add to Cart" button.
The gameserver selected is passed from the gameserverss page by a Post of the ServiceID
When the user clicks the "Add to Cart" button, the next page to load is "add_to_cart.php" which creates all the DB entries.
All the configuration info is passed to the add_to_cart.php in hidden fields
In our website, we are setting "post" pages with a "Tag". The first tag in our post should be the service ID from the services table
There are other methods that might be better to get the info. But all we need is the "service_ID" in the "{$table_prefix}billing_services" table
This method means we can use one code block in every game page and fill in the data dynamically.
This is the "order gameserver" page. It displays the options for a single specific game server and
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.
*/
// Require login for ordering
@ -43,278 +41,363 @@ if (!$db) {
include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php');
if (isset($_POST['save']) AND !empty($_POST['description']))
{
$new_description = str_replace("\\r\\n", "<br>", $_POST['description']);
$service = $_POST['service_id'];
$change_description = "UPDATE {$table_prefix}billing_services
SET description ='".$new_description."'
WHERE service_id=".$service;
$save = $db->query($change_description);
}
?>
if (isset($_POST['save']) && !empty($_POST['description'])) {
$new_description = str_replace("\\r\\n", "<br>", $_POST['description']);
$service = intval($_POST['service_id']);
$stmt = $db->prepare("UPDATE {$table_prefix}billing_services SET description = ? WHERE service_id = ?");
if ($stmt) {
$stmt->bind_param("si", $new_description, $service);
$stmt->execute();
$stmt->close();
}
}
/**
* 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';
}
<!-- ------------------------------------------------------------------------------
THIS IS WHAT WE DISPLAY ON THE SHOP PAGE AT THE TOP
-->
<?php
// Shop Form
$req_service_id = intval($_REQUEST['service_id'] ?? 0);
if ($req_service_id !== 0) {
$where_service_id = " WHERE enabled = 1 AND service_id=" . $req_service_id;
} else {
$where_service_id = " WHERE enabled = 1";
}
$qry_services = "SELECT * FROM {$table_prefix}billing_services " . $where_service_id . " ORDER BY service_name";
$services_result = $db->query($qry_services);
if ($services_result === false) {
echo "<p class='error'>Unable to load service information. Please try again or contact support.</p>";
error_log("billing order.php: query failed - " . $db->error . " | SQL: " . $qry_services);
billing_maybe_close_db($db);
include(__DIR__ . '/includes/footer.php');
echo '</body></html>';
exit;
}
// Fetch all rows into an array so foreach works correctly
$serviceRows = [];
while ($row = $services_result->fetch_assoc()) {
$serviceRows[] = $row;
}
$services_result->free();
if ($req_service_id !== 0 && empty($serviceRows)) {
error_log("billing order.php: service_id={$req_service_id} not found or not enabled");
echo "<p class='error'>The requested service could not be found or is no longer available.</p>";
echo "<p><a href='serverlist.php'>Back to server list</a></p>";
billing_maybe_close_db($db);
include(__DIR__ . '/includes/footer.php');
echo '</body></html>';
exit;
}
?>
<div class="clearfix">
<?php
foreach ($serviceRows as $row)
{
if(!isset($_REQUEST['service_id']))
{
?>
<div class="float-left p-30-20">
<img src="<?php echo htmlspecialchars(billing_image_url((string)($row['img_url'] ?? '')), ENT_QUOTES, 'UTF-8');?>" width="460" height="225" >
<br>
<?php echo htmlspecialchars((string)$row['service_name'], ENT_QUOTES, 'UTF-8');?>
<br>
<?php
if ($row['price_monthly'] == 0.0) {
echo "FREE";
// --- Fetch the requested service with config_homes join for canonical game info ---
$req_service_id = intval($_REQUEST['service_id'] ?? 0);
if ($req_service_id !== 0) {
$where_service_id = " WHERE bs.enabled = 1 AND bs.service_id=" . $req_service_id;
} else {
echo "$" . number_format(floatval($row['price_monthly']),2). " Monthly";
$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
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}
ORDER BY bs.service_name";
$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";
$services_result = $db->query($qry_services);
}
if ($services_result === false) {
echo "<p class='error'>Unable to load service information. Please try again or contact support.</p>";
error_log("billing order.php: query failed - " . $db->error);
billing_maybe_close_db($db);
include(__DIR__ . '/includes/footer.php');
echo '</body></html>';
exit;
}
$serviceRows = [];
while ($row = $services_result->fetch_assoc()) {
$serviceRows[] = $row;
}
$services_result->free();
if ($req_service_id !== 0 && empty($serviceRows)) {
error_log("billing order.php: service_id={$req_service_id} not found or not enabled");
echo "<p class='error'>The requested service could not be found or is no longer available.</p>";
echo "<p><a href='serverlist.php'>Back to server list</a></p>";
billing_maybe_close_db($db);
include(__DIR__ . '/includes/footer.php');
echo '</body></html>';
exit;
}
// Check whether remote_servers has a server_os column (added by db_version 6 migration).
// We gracefully degrade: if the column is absent, all servers are treated as compatible.
$hasServerOsColumn = false;
$osColCheck = $db->query("SHOW COLUMNS FROM {$table_prefix}remote_servers LIKE 'server_os'");
if ($osColCheck && $osColCheck->num_rows > 0) {
$hasServerOsColumn = true;
$osColCheck->free();
}
?>
<div class="clearfix">
<?php
foreach ($serviceRows as $row)
{
if (!isset($_REQUEST['service_id']))
{
?>
<div class="float-left p-30-20">
<?php
$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'); ?>" width="460" height="225"
onerror="this.src='/images/games/default_server.png'; this.onerror=null;">
<br>
<?php echo htmlspecialchars((string)($row['cfg_game_name'] ?? $row['service_name']), ENT_QUOTES, 'UTF-8'); ?>
<br>
<?php
if (floatval($row['price_monthly']) == 0.0) {
echo "FREE";
} else {
echo "$" . number_format(floatval($row['price_monthly']), 2) . " Monthly";
}
?>
<br>
<form action="<?php echo $row['description'];?>" method="POST">
<input name="service_id" type="hidden" value="<?php echo $row['service_id'];?>" />
<input name="order_server" type="submit" value="Server Info">
</form>
<form action="" method="POST">
<input name="service_id" type="hidden" value="<?php echo $row['service_id'];?>" />
<input name="order_server" type="submit" value="Order Server">
</form>
</div>
<a href="order.php?service_id=<?php echo intval($row['service_id']); ?>" class="gsw-btn">Order Now</a>
</div>
</>
<?php
} else
//THIS IS THE SERVER WE WANT TO ORDER
{
?>
<div class="float-left decorative-bottom">
<img src="<?php echo htmlspecialchars(billing_image_url((string)($row['img_url'] ?? '')), ENT_QUOTES, 'UTF-8');?>" width="230" height="112">
<center><b> <?php echo htmlspecialchars((string)$row['service_name'], ENT_QUOTES, 'UTF-8');?></b></center>
<?php
//$isAdmin = if( current_user_can('administrator')){
//$isAdmin = true;
//$isAdmin = $db->isAdmin($_SESSION['user_id'] );
$isAdmin = false;
if($isAdmin)
{
if(!isset($_POST['edit']))
{
echo "<p style='color:gray;width:230px;' >$row[description]<p>";
echo "<form action='' method='post'>".
"<input type='hidden' name='service_id' value='$row[service_id]' />".
"<input type='submit' name='edit' value='Edit' />".
"</form>";
}
else
{
echo "<form action='' method='post'>".
"<textarea style='resize:none;width:230px;height:132px;' name='description' >".str_replace("<br>", "\r\n", $row['description'])."</textarea><br>".
"<input type='hidden' name='service_id' value='$row[service_id]' />".
"<input type='submit' name='save' value='Save' />".
"</form>";
}
}
else
echo "<p style='color:gray;width:280px;' >$row[description]<p>";
?>
</div>
<table class="float-left">
<form method="post" action="add_to_cart.php">
<input type="hidden" name="service_id" size="15" value="<?php echo intval($_REQUEST['service_id'] ?? $row['service_id'] ?? 0); ?>">
<input type="hidden" name="remote_control_password" size="15" value="">
<input type="hidden" name="ftp_password" size="15" value="">
<tr>
<td align="right"><b>Game Server Name</b> </td>
<td align="left">
<input type="text" name="home_name" size="40" value="<?php echo $row['service_name'];?>">
</td>
<tr>
<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").
$available_server = false;
$remoteIdsCsv = (string)($row['remote_server_id'] ?? '');
$allowedIds = [];
foreach (explode(',', $remoteIdsCsv) as $part) {
$part = trim($part);
if ($part !== '' && ctype_digit($part)) {
$allowedIds[] = (int)$part;
}
}
if (!empty($allowedIds)) {
$inList = implode(',', $allowedIds);
$rsQuery = "SELECT remote_server_id, remote_server_name
FROM {$table_prefix}remote_servers
WHERE remote_server_id IN ({$inList})
ORDER BY remote_server_name";
$rsResult = $db->query($rsQuery);
if ($rsResult) {
$firstServer = true;
while ($rs = $rsResult->fetch_assoc()) {
$rsID = (int)$rs['remote_server_id'];
$rsNAME = htmlspecialchars((string)$rs['remote_server_name'], ENT_QUOTES, 'UTF-8');
$checked = $firstServer ? ' checked' : '';
$available_server = true;
$firstServer = false;
echo "<div>\n"
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' required{$checked}>\n"
. " <label for='rs_{$rsID}'>{$rsNAME}</label>\n"
. "</div>\n";
}
}
}
?>
<?php
}else
// THIS IS THE SERVER WE WANT TO ORDER
{
// Determine canonical game name and OS for this service
$svcGameKey = (string)($row['cfg_game_key'] ?? '');
$svcGameOs = order_game_key_os($svcGameKey);
$canonicalGameName = (string)($row['cfg_game_name'] ?? $row['service_name']);
// 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)) {
$escapedName = $db->real_escape_string($canonicalGameName);
$siblingQuery = "SELECT bs.service_id, ch.game_key AS cfg_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.enabled = 1 AND ch.game_name = '{$escapedName}'";
$siblingResult = $db->query($siblingQuery);
if ($siblingResult) {
while ($sib = $siblingResult->fetch_assoc()) {
$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="float-left decorative-bottom">
<?php
$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'); ?>" width="230" height="112"
onerror="this.src='/images/games/default_server.png'; this.onerror=null;">
<center><b><?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?></b></center>
<?php
$isAdmin = false;
if ($isAdmin) {
if (!isset($_POST['edit'])) {
echo "<p style='color:gray;width:230px;'>" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "</p>";
echo "<form action='' method='post'>"
. "<input type='hidden' name='service_id' value='" . intval($row['service_id']) . "' />"
. "<input type='submit' name='edit' value='Edit' />"
. "</form>";
} else {
$descEditable = htmlspecialchars(str_replace("<br>", "\r\n", (string)($row['description'] ?? '')), ENT_QUOTES, 'UTF-8');
echo "<form action='' method='post'>"
. "<textarea style='resize:none;width:230px;height:132px;' name='description'>{$descEditable}</textarea><br>"
. "<input type='hidden' name='service_id' value='" . intval($row['service_id']) . "' />"
. "<input type='submit' name='save' value='Save' />"
. "</form>";
}
} else {
echo "<p style='color:gray;width:280px;'>" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "</p>";
}
?>
</div>
<table class="float-left">
<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="remote_control_password" value="">
<input type="hidden" name="ftp_password" value="">
<tr>
<td align="right"><b>Game Server Name</b> </td>
<td align="left">
<input type="text" name="home_name" size="40" value="<?php echo htmlspecialchars((string)($row['service_name'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>">
</td>
<tr>
<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.
$available_server = false;
$remoteIdsCsv = (string)($row['remote_server_id'] ?? '');
</td>
</tr>
<tr>
<td align="right"><b>Configure</b></td>
<td align="left">
<div class="slidecontainer">
<center><b>Player Slots</b> </center>
<input type="range" name="max_players" min="<?php echo $row['slot_min_qty']?>" max="<?php echo $row['slot_max_qty']?>" value="<?php echo $row['slot_min_qty']?>" class="slider" id="playerRange">
<center><b>Months</b></center>
<input type="range" name="qty" min="1" max="24" value="1" class="slider" id="invoiceRange">
// Also gather allowed IDs from sibling OS-variant services
$allAllowedIds = [];
foreach (explode(',', $remoteIdsCsv) as $part) {
$part = trim($part);
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);
<p>Player Slots: <span id="playerSlots"></span><br>
<span>Price: $<?php echo number_format(floatval($row['price_monthly']),2 );?> USD</span><br>
<span id="invoiceDuration"></span><br>
<span id="totalPrice"></span></p>
</div>
<script>
var slider = document.getElementById("playerRange");
var invoiceslider = document.getElementById("invoiceRange");
if (!empty($allAllowedIds)) {
$inList = implode(',', $allAllowedIds);
// Select server_os if the column exists (added by db_version 6 migration)
$osSel = $hasServerOsColumn ? ', server_os' : ", 'any' AS server_os";
$rsQuery = "SELECT remote_server_id, remote_server_name{$osSel}
FROM {$table_prefix}remote_servers
WHERE remote_server_id IN ({$inList})
ORDER BY remote_server_name";
$rsResult = $db->query($rsQuery);
if ($rsResult) {
$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');
$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
}
$available_server = true;
$firstServer = false;
$safeOs = htmlspecialchars($rsOs, ENT_QUOTES, 'UTF-8');
echo "<div>\n"
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' data-os='{$safeOs}' required{$checked} onchange='gspUpdateServiceId(this)'>\n"
. " <label for='rs_{$rsID}'>{$rsNAME}</label>\n"
. "</div>\n";
}
$rsResult->free();
}
}
?>
</td>
</tr>
<tr>
<td align="right"><b>Configure</b></td>
<td align="left">
<div class="slidecontainer">
<center><b>Player Slots</b> </center>
<input type="range" name="max_players" min="<?php echo intval($row['slot_min_qty']); ?>" max="<?php echo intval($row['slot_max_qty']); ?>" value="<?php echo intval($row['slot_min_qty']); ?>" class="slider" id="playerRange">
<center><b>Months</b></center>
<input type="range" name="qty" min="1" max="24" value="1" class="slider" id="invoiceRange">
var output = document.getElementById("playerSlots");
var price = document.getElementById("totalPrice");
var invoiceDuration = document.getElementById("invoiceDuration");
var totalvalue = 0;
<p>Player Slots: <span id="playerSlots"></span><br>
<span>Price: $<?php echo number_format(floatval($row['price_monthly']), 2); ?> USD</span><br>
<span id="invoiceDuration"></span><br>
<span id="totalPrice"></span></p>
</div>
output.innerHTML = slider.value;
invoiceDuration.innerHTML = "Duration: "+invoiceslider.value+" months";
totalvalue = slider.value * invoiceslider.value * <?php echo number_format($row['price_monthly'],2);?>;
price.innerHTML = "Total Price: $"+totalvalue.toFixed(2) ;
<script>
(function() {
var slider = document.getElementById("playerRange");
var invoiceslider = document.getElementById("invoiceRange");
var output = document.getElementById("playerSlots");
var price = document.getElementById("totalPrice");
var invoiceDuration = document.getElementById("invoiceDuration");
var pricePerSlot = <?php echo number_format(floatval($row['price_monthly']), 2, '.', ''); ?>;
slider.oninput = function() {
output.innerHTML = this.value;
invoiceDuration.innerHTML = "Duration: "+invoiceslider.value+" months";
totalvalue = slider.value * invoiceslider.value * <?php echo number_format($row['price_monthly'],2);?>;
price.innerHTML = "Total Price: $"+totalvalue.toFixed(2) ;
}
invoiceslider.oninput = function() {
invoiceDuration.innerHTML = "Duration: "+invoiceslider.value+" months";
totalvalue = slider.value * invoiceslider.value * <?php echo number_format($row['price_monthly'],2);?>;
price.innerHTML = "Total Price: $"+totalvalue.toFixed(2) ;
}
</script>
<input type="hidden" name="invoice_duration" value="month" />
</td>
</tr>
<tr>
<td align="left" colspan="2">
<input name="service_id" type="hidden" value="<?php echo $row['service_id'];?>"/>
<?php
// Only show Add to Cart when logged in
$is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) || (isset($_SESSION['website_username']) && !empty($_SESSION['website_username']));
?>
<?php if ($available_server && $is_logged_in): ?>
<button type="submit" name="add_to_cart" class="gsw-btn">Add to Cart</button>
<?php else: ?>
<div class="login-placeholder">Please <a href="login.php">login</a> to order</div>
<?php endif; ?>
</form>
</td>
</tr>
<tr>
<td align="left" colspan="2">
<form action ="serverlist.php" method="POST">
<button class="gsw-btn-secondary">Back to List</button>
</form>
</td>
</tr>
</table>
<?php
}
}
?>
</div>
// 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);
output.innerHTML = slots;
invoiceDuration.innerHTML = "Duration: " + months + " month" + (months !== 1 ? "s" : "");
price.innerHTML = "Total Price: $" + (slots * months * pricePerSlot).toFixed(2);
}
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>
<input type="hidden" name="invoice_duration" value="month" />
</td>
</tr>
<tr>
<td align="left" colspan="2">
<?php
// Only show Add to Cart when logged in
$is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) || (isset($_SESSION['website_username']) && !empty($_SESSION['website_username']));
?>
<?php if ($available_server && $is_logged_in): ?>
<button type="submit" name="add_to_cart" class="gsw-btn">Add to Cart</button>
<?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>
<?php endif; ?>
</form>
</td>
</tr>
<tr>
<td align="left" colspan="2">
<form action="serverlist.php" method="GET">
<button class="gsw-btn-secondary">Back to List</button>
</form>
</td>
</tr>
</table>
<?php
}
}
?>
</div>
<?php
// Close database connection
billing_maybe_close_db($db);
billing_maybe_close_db($db);
?>
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>

View file

@ -7,10 +7,6 @@
</head>
<body>
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Include database configuration
require_once(__DIR__ . '/bootstrap.php');
@ -37,23 +33,59 @@ if (isset($_POST['save']) && !empty($_POST['description'])) {
$stmt->close();
}
// Fetch services
// 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.
$service_id = isset($_REQUEST['service_id']) ? intval($_REQUEST['service_id']) : 0;
$where_service_id = $service_id !== 0
? "WHERE enabled = 1 AND service_id = $service_id AND remote_server_id != '' AND remote_server_id IS NOT NULL"
: "WHERE enabled = 1 AND remote_server_id != '' AND remote_server_id IS NOT NULL";
$qry_services = "SELECT * FROM {$table_prefix}billing_services $where_service_id ORDER BY service_name";
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
FROM {$table_prefix}billing_services bs
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);
if (!$result_services) {
// config_homes join may not exist on all installs; fall back to services-only query
$where_clause_fallback = str_replace('bs.', '', $where_clause);
$qry_services_fallback = "SELECT *, NULL AS cfg_game_name, NULL AS cfg_game_key
FROM {$table_prefix}billing_services
{$where_clause_fallback}
ORDER BY service_name";
$result_services = $db->query($qry_services_fallback);
}
if (!$result_services) {
echo "<meta http-equiv='refresh' content='1'>";
billing_maybe_close_db($db);
return;
}
// Fetch all service rows into an array so the template foreach works correctly
// 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.
$canonicalName = !empty($row['cfg_game_name'])
? $row['cfg_game_name']
: $row['service_name'];
if (isset($seenCanonical[$canonicalName])) {
// Already have this game — skip the duplicate OS variant
continue;
}
$seenCanonical[$canonicalName] = true;
$serviceRows[] = $row;
}
$result_services->free();
@ -69,13 +101,18 @@ include(__DIR__ . '/includes/menu.php');
<?php if (!isset($_REQUEST['service_id'])): ?>
<!-- Service listing (all) -->
<div class="float-left p-30-20">
<?php $imgSrc = billing_image_url((string)($row['img_url'] ?? '')); ?>
<?php if ($imgSrc !== ''): ?>
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" width="460" height="225"><br>
<?php endif; ?>
<strong><?php echo htmlspecialchars((string)$row['service_name'], ENT_QUOTES, 'UTF-8'); ?></strong><br>
<?php
echo ($row['price_monthly'] == 0.0) ? "FREE" : "$" . number_format(floatval($row['price_monthly']), 2) . " Monthly";
$imgSrc = billing_image_url((string)($row['img_url'] ?? ''));
// Use a generic fallback image when the service has no image configured
if ($imgSrc === '') {
$imgSrc = '/images/games/default_server.png';
}
?>
<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
echo (floatval($row['price_monthly']) == 0.0) ? "FREE" : "$" . number_format(floatval($row['price_monthly']), 2) . " Monthly";
?>
<br>
@ -84,44 +121,48 @@ include(__DIR__ . '/includes/menu.php');
<?php else: ?>
<!-- Single service detail view -->
<div class="float-left decorative-bottom">
<?php $imgSrc = billing_image_url((string)($row['img_url'] ?? '')); ?>
<?php if ($imgSrc !== ''): ?>
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" width="230" height="112"><br>
<?php endif; ?>
<center><b><?php echo htmlspecialchars((string)$row['service_name'], ENT_QUOTES, 'UTF-8'); ?></b></center>
<?php
$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'); ?>" 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
$isAdmin = false; // change to actual check, e.g. current_user_can('administrator')
$isAdmin = false;
if ($isAdmin) {
if (!isset($_POST['edit'])) {
echo "<p style='color:gray;width:230px;'>{$row['description']}</p>";
echo "<p style='color:gray;width:230px;'>" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "</p>";
echo "<form method='post'>
<input type='hidden' name='service_id' value='{$row['servioce_id']}'>
<input type='hidden' name='service_id' value='" . intval($row['service_id']) . "'>
<input type='submit' name='edit' value='Edit'>
</form>";
} else {
$desc = str_replace("<br>", "\r\n", $row['description']);
$desc = htmlspecialchars(str_replace("<br>", "\r\n", (string)($row['description'] ?? '')), ENT_QUOTES, 'UTF-8');
echo "<form method='post'>
<textarea style='resize:none;width:230px;height:132px;' name='description'>$desc</textarea><br>
<input type='hidden' name='service_id' value='{$row['service_id']}'>
<textarea style='resize:none;width:230px;height:132px;' name='description'>{$desc}</textarea><br>
<input type='hidden' name='service_id' value='" . intval($row['service_id']) . "'>
<input type='submit' name='save' value='Save'>
</form>";
}
} else {
echo "<p style='color:gray;width:280px;'>{$row['description']}</p>";
echo "<p style='color:gray;width:280px;'>" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "</p>";
}
?>
</div>
<!-- Order Form -->
<form method="post" action="order_server.php">
<input type="hidden" name="service_id" value="<?php echo $row['service_id']; ?>">
<input type="hidden" name="service_id" value="<?php echo intval($row['service_id']); ?>">
<input type="hidden" name="remote_control_password" value="">
<input type="hidden" name="ftp_password" value="">
<table class="float-left">
<tr>
<td align="right"><b>Game Server Name</b></td>
<td><input type="text" name="home_name" size="40" value="<?php echo $row['service_name']; ?>"></td>
<td><input type="text" name="home_name" size="40" value="<?php echo htmlspecialchars((string)($row['service_name'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"></td>
</tr>
<!-- Add other form fields as needed -->
<tr>