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:
parent
7ff7adca9d
commit
acbb850e21
6 changed files with 495 additions and 352 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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`");
|
||||
}
|
||||
);
|
||||
|
||||
?>
|
||||
|
|
@ -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'); ?>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -233,61 +233,61 @@ function config_games_print_editor_css()
|
|||
$printed = true;
|
||||
echo <<<CSS
|
||||
<style>
|
||||
.xml-editor-wrapper{margin:20px 0;padding:12px;background:#111;border:1px solid #222;border-radius:8px}
|
||||
.xml-node{border:1px solid #333;border-radius:6px;padding:12px;margin-bottom:10px;background:#181818}
|
||||
.xml-editor-wrapper{margin:20px 0;padding:14px;background:#111;border:1px solid #222;border-radius:8px;font-size:1rem}
|
||||
.xml-node{border:1px solid #333;border-radius:6px;padding:14px;margin-bottom:12px;background:#181818}
|
||||
.xml-node--required{border-left:3px solid #1c6dd0}
|
||||
.xml-node__header{display:flex;justify-content:space-between;align-items:center;gap:12px;border-bottom:1px solid #2a2a2a;padding-bottom:6px;margin-bottom:8px}
|
||||
.xml-node__title{font-weight:600;color:#f5f5f5}
|
||||
.xml-node__title--required::after{content:" *";color:#e06c75;font-size:0.8rem}
|
||||
.xml-node__path{font-size:0.85rem;color:#989898}
|
||||
.xml-node__badge{font-size:0.72rem;padding:2px 6px;border-radius:3px;text-transform:uppercase;letter-spacing:0.05em;margin-left:6px}
|
||||
.xml-node__header{display:flex;justify-content:space-between;align-items:center;gap:12px;border-bottom:1px solid #2a2a2a;padding-bottom:8px;margin-bottom:10px}
|
||||
.xml-node__title{font-weight:600;color:#f5f5f5;font-size:1rem}
|
||||
.xml-node__title--required::after{content:" *";color:#e06c75;font-size:0.85rem}
|
||||
.xml-node__path{font-size:0.88rem;color:#b0b0b0}
|
||||
.xml-node__badge{font-size:0.75rem;padding:2px 7px;border-radius:3px;text-transform:uppercase;letter-spacing:0.05em;margin-left:6px}
|
||||
.xml-node__badge--required{background:#1c3a6d;color:#7eb3f0}
|
||||
.xml-node__badge--optional{background:#2a2a2a;color:#888}
|
||||
.xml-node__body label{font-size:0.85rem;color:#bbb;display:block;margin-bottom:4px}
|
||||
.xml-node__body input[type="text"], .xml-node__body textarea, .xml-node__body select{width:100%;padding:8px;border:1px solid #3a3a3a;border-radius:4px;background:#101010;color:#fff;font-family:monospace}
|
||||
.xml-node__body textarea{min-height:120px}
|
||||
.xml-node__badge--optional{background:#2a2a2a;color:#aaa}
|
||||
.xml-node__body label{font-size:0.9rem;color:#d0d0d0;display:block;margin-bottom:5px}
|
||||
.xml-node__body input[type="text"], .xml-node__body textarea, .xml-node__body select{width:100%;padding:8px 10px;border:1px solid #3a3a3a;border-radius:4px;background:#101010;color:#f0f0f0;font-family:monospace;font-size:0.93rem}
|
||||
.xml-node__body textarea{min-height:130px;line-height:1.4}
|
||||
.xml-node__attributes{margin-top:8px}
|
||||
.xml-node__attributes .attr-row{display:flex;gap:8px;align-items:center;margin-bottom:6px}
|
||||
.xml-node__attributes .attr-row input[type="text"]{flex:1}
|
||||
.xml-children{margin-top:10px;border-left:2px solid #2a2a2a;padding-left:12px}
|
||||
.xml-children{margin-top:10px;border-left:2px solid #2a2a2a;padding-left:14px}
|
||||
.xml-actions{display:flex;justify-content:flex-end;margin-top:16px;padding:8px 18px 0}
|
||||
.xml-node__actions{display:flex;gap:8px;align-items:center}
|
||||
.xml-node__apply{background:#1c6dd0;border:1px solid #114b99;color:#fff;padding:6px 12px;border-radius:4px;cursor:pointer}
|
||||
.xml-node__apply{background:#1c6dd0;border:1px solid #114b99;color:#fff;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:0.93rem}
|
||||
.xml-node__apply:hover{background:#1f7aec}
|
||||
.xml-global-save{background:#1c6dd0;border:1px solid #114b99;color:#fff;padding:10px 28px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:0.03em;cursor:pointer;transition:background 0.2s ease,transform 0.2s ease;box-shadow:0 2px 6px rgba(0,0,0,0.35)}
|
||||
.xml-global-save{background:#1c6dd0;border:1px solid #114b99;color:#fff;padding:10px 28px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:0.03em;cursor:pointer;transition:background 0.2s ease,transform 0.2s ease;box-shadow:0 2px 6px rgba(0,0,0,0.35);font-size:0.95rem}
|
||||
.xml-global-save:hover{background:#1f7aec;transform:translateY(-1px)}
|
||||
.xml-global-save--top{float:right;margin:0 18px 12px 0}
|
||||
.xml-hint{font-size:0.85rem;color:#999;margin-top:4px}
|
||||
.xml-validation-errors{background:#2d0f0f;border:1px solid #8b1c1c;border-radius:6px;padding:12px 16px;margin-bottom:14px;color:#f88}
|
||||
.xml-hint{font-size:0.88rem;color:#b0b0b0;margin-top:5px}
|
||||
.xml-validation-errors{background:#2d0f0f;border:1px solid #8b1c1c;border-radius:6px;padding:12px 16px;margin-bottom:14px;color:#ffaaaa;font-size:0.93rem}
|
||||
.xml-validation-errors ul{margin:6px 0 0 16px;padding:0}
|
||||
.xml-raw-toggle{margin:8px 0 4px;color:#7eb3f0;cursor:pointer;font-size:0.9rem;text-decoration:underline;background:none;border:none;padding:0}
|
||||
.xml-raw-toggle{margin:8px 0 4px;color:#7eb3f0;cursor:pointer;font-size:0.95rem;text-decoration:underline;background:none;border:none;padding:0}
|
||||
.xml-raw-section{margin-top:10px;display:none}
|
||||
.xml-raw-section textarea{width:100%;min-height:300px;font-family:monospace;font-size:0.85rem;background:#0c0c0c;color:#eee;border:1px solid #3a3a3a;border-radius:4px;padding:8px}
|
||||
.xml-raw-warning{background:#2d2200;border:1px solid #7a5a00;border-radius:4px;padding:8px 12px;color:#f0c050;font-size:0.85rem;margin-bottom:6px}
|
||||
.xml-section-header{margin:20px 0 4px;font-size:0.8rem;color:#888;text-transform:uppercase;letter-spacing:0.1em;border-bottom:1px solid #2a2a2a;padding-bottom:4px}
|
||||
.xml-node__desc{font-size:0.82rem;color:#aaa;background:#0e0e0e;border-left:3px solid #2a4a7a;padding:6px 10px;margin:6px 0 8px;border-radius:0 4px 4px 0}
|
||||
.xml-node__options{margin:4px 0 4px 12px;padding:0;list-style:disc inside}
|
||||
.xml-node__options li{margin-bottom:2px}
|
||||
.xml-node__options code{color:#7eb3f0;background:rgba(30,100,200,0.12);padding:1px 4px;border-radius:3px}
|
||||
.xml-node__example{display:block;margin-top:4px;color:#888}
|
||||
.xml-node__example code{color:#a0d0a0;background:rgba(30,150,50,0.1);padding:1px 4px;border-radius:3px}
|
||||
.xml-jump-link{display:inline-block;margin-bottom:12px;padding:6px 14px;background:#1c6dd0;color:#fff;border-radius:4px;text-decoration:none;font-size:0.9rem}
|
||||
.xml-raw-section textarea{width:100%;min-height:320px;font-family:monospace;font-size:0.9rem;background:#0c0c0c;color:#f0f0f0;border:1px solid #3a3a3a;border-radius:4px;padding:10px;line-height:1.4}
|
||||
.xml-raw-warning{background:#2d2200;border:1px solid #7a5a00;border-radius:4px;padding:10px 14px;color:#f0c050;font-size:0.9rem;margin-bottom:8px}
|
||||
.xml-section-header{margin:22px 0 6px;font-size:0.85rem;color:#aaa;text-transform:uppercase;letter-spacing:0.1em;border-bottom:1px solid #2a2a2a;padding-bottom:5px}
|
||||
.xml-node__desc{font-size:0.88rem;color:#c8c8c8;background:#0e0e0e;border-left:3px solid #2a4a7a;padding:8px 12px;margin:8px 0 10px;border-radius:0 4px 4px 0;line-height:1.5}
|
||||
.xml-node__options{margin:6px 0 4px 14px;padding:0;list-style:disc inside}
|
||||
.xml-node__options li{margin-bottom:3px;font-size:0.9rem}
|
||||
.xml-node__options code{color:#7eb3f0;background:rgba(30,100,200,0.12);padding:1px 5px;border-radius:3px}
|
||||
.xml-node__example{display:block;margin-top:6px;color:#aaa;font-size:0.88rem}
|
||||
.xml-node__example code{color:#a0d0a0;background:rgba(30,150,50,0.1);padding:1px 5px;border-radius:3px}
|
||||
.xml-jump-link{display:inline-block;margin-bottom:12px;padding:7px 16px;background:#1c6dd0;color:#fff;border-radius:4px;text-decoration:none;font-size:0.93rem}
|
||||
.xml-jump-link:hover{background:#1f7aec;text-decoration:none}
|
||||
.xml-section-grid{display:flex;flex-direction:column;gap:14px;margin-bottom:18px}
|
||||
.xml-section-block{border:1px solid #303030;border-radius:6px;background:#141414;padding:12px}
|
||||
.xml-section-block__head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px}
|
||||
.xml-section-block__title{font-size:1.02rem;color:#f0f0f0;font-weight:600}
|
||||
.xml-section-block__meta{font-size:0.8rem;color:#9f9f9f}
|
||||
.xml-section-block__desc{font-size:0.86rem;color:#b0b0b0;margin:0 0 10px}
|
||||
.xml-section-block textarea{width:100%;min-height:170px;background:#0f0f0f;border:1px solid #3c3c3c;border-radius:4px;color:#f7f7f7;padding:8px;font-family:monospace;font-size:0.84rem}
|
||||
.xml-section-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
|
||||
.xml-btn{border:1px solid #3f3f3f;background:#222;color:#fff;padding:6px 10px;border-radius:4px;cursor:pointer}
|
||||
.xml-section-grid{display:flex;flex-direction:column;gap:16px;margin-bottom:20px}
|
||||
.xml-section-block{border:1px solid #303030;border-radius:6px;background:#141414;padding:14px}
|
||||
.xml-section-block__head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:10px}
|
||||
.xml-section-block__title{font-size:1.05rem;color:#f0f0f0;font-weight:600}
|
||||
.xml-section-block__meta{font-size:0.83rem;color:#b0b0b0}
|
||||
.xml-section-block__desc{font-size:0.9rem;color:#c8c8c8;margin:0 0 12px;line-height:1.5}
|
||||
.xml-section-block textarea{width:100%;min-height:180px;background:#0f0f0f;border:1px solid #3c3c3c;border-radius:4px;color:#f7f7f7;padding:10px;font-family:monospace;font-size:0.9rem;line-height:1.4}
|
||||
.xml-section-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
|
||||
.xml-btn{border:1px solid #3f3f3f;background:#222;color:#e8e8e8;padding:7px 12px;border-radius:4px;cursor:pointer;font-size:0.9rem}
|
||||
.xml-btn:hover{background:#2a2a2a}
|
||||
.xml-btn--primary{background:#1c6dd0;border-color:#114b99}
|
||||
.xml-btn--primary{background:#1c6dd0;border-color:#114b99;color:#fff}
|
||||
.xml-btn--primary:hover{background:#1f7aec}
|
||||
.xml-btn--danger{background:#6b1f1f;border-color:#8d2d2d}
|
||||
.xml-btn--danger{background:#6b1f1f;border-color:#8d2d2d;color:#ffcccc}
|
||||
.xml-btn--danger:hover{background:#8d2d2d}
|
||||
.xml-add-section{border:1px dashed #3a3a3a;border-radius:6px;padding:10px;margin-bottom:16px}
|
||||
.xml-add-section{border:1px dashed #3a3a3a;border-radius:6px;padding:12px;margin-bottom:18px}
|
||||
.xml-add-section select{min-width:260px}
|
||||
</style>
|
||||
CSS;
|
||||
|
|
@ -730,6 +730,23 @@ function config_games_render_top_level_editor($home_cfg_id, $configFile)
|
|||
echo "</div>";
|
||||
echo "</form>";
|
||||
}
|
||||
|
||||
// Render missing optional schema sections as informational cards with an Add button.
|
||||
$descriptions = config_games_tag_descriptions();
|
||||
foreach ($optionalMissing as $missingName) {
|
||||
$safeMissing = htmlspecialchars($missingName, ENT_QUOTES, 'UTF-8');
|
||||
$missingDesc = htmlspecialchars($descriptions[$missingName]['desc'] ?? 'Optional configuration section.', ENT_QUOTES, 'UTF-8');
|
||||
echo "<div class='xml-section-block' style='border-color:#2a3a2a;background:#101810;opacity:0.85'>";
|
||||
echo "<div class='xml-section-block__head'><div><div class='xml-section-block__title' style='color:#a0c0a0'>{$safeMissing}</div><div class='xml-section-block__meta'>Optional — not in this file</div></div></div>";
|
||||
echo "<p class='xml-section-block__desc' style='font-style:italic;color:#9aba9a'>This section is not currently in this XML file. You can add it if needed.</p>";
|
||||
echo "<p class='xml-section-block__desc'>{$missingDesc}</p>";
|
||||
echo "<form action='?m=config_games&home_cfg_id=" . (int)$home_cfg_id . "' method='post'>";
|
||||
echo "<input type='hidden' name='home_cfg_id' value='" . (int)$home_cfg_id . "'>";
|
||||
echo "<input type='hidden' name='section_name' value='{$safeMissing}'>";
|
||||
echo "<div class='xml-section-actions'><button class='xml-btn xml-btn--primary' type='submit' name='add_optional_section' value='1'>Add Section</button></div>";
|
||||
echo "</form>";
|
||||
echo "</div>";
|
||||
}
|
||||
echo "</div>";
|
||||
}
|
||||
|
||||
|
|
@ -1134,22 +1151,11 @@ function exec_ogp_module() {
|
|||
echo "<div id='xml-editor-section'>";
|
||||
config_games_render_top_level_editor($home_cfg_id, $config_file);
|
||||
|
||||
echo "<details style='margin:18px 0'><summary style='cursor:pointer;color:#9dc7ff'>Open legacy detailed node editor (previous default editor)</summary>";
|
||||
echo "<form action='?m=config_games&home_cfg_id=".$home_cfg_id."' method='post'>";
|
||||
echo "<input type='hidden' name='home_cfg_id' value='".(int)$home_cfg_id."'>";
|
||||
echo "<button type='submit' name='save_xml' value='1' class='xml-global-save xml-global-save--top'>".get_lang('save')."</button>";
|
||||
echo "<div style='clear:both'></div>";
|
||||
echo config_games_render_editor($xml);
|
||||
echo "<div class='xml-actions'><button type='submit' name='save_xml' value='1' class='xml-global-save'>".get_lang('save')."</button></div>";
|
||||
echo "<p class='note'>★ = required field. Use the action dropdown to remove entire sections. Attribute values left blank will be removed. Script sections such as post_install are fully editable. Changes are validated against the schema before saving.</p>";
|
||||
echo "</form>";
|
||||
echo "</details>";
|
||||
|
||||
// Raw XML editor
|
||||
echo "<hr style='margin:24px 0;border-color:#333'>";
|
||||
echo "<h3 style='margin-bottom:8px'>Full Raw XML Editor</h3>";
|
||||
echo "<div class='xml-raw-warning'>⚠ <strong>Warning:</strong> Saving raw XML bypasses the guided editor. The file will be validated against the schema before saving. Invalid XML will be rejected.</div>";
|
||||
echo "<button type='button' class='xml-raw-toggle' onclick=\"var s=document.getElementById('raw_xml_section');s.style.display=s.style.display==='none'?'block':'none'\">Toggle Raw XML Editor</button>";
|
||||
echo "<button type='button' id='raw_xml_toggle_btn' class='xml-raw-toggle' onclick=\"(function(){var s=document.getElementById('raw_xml_section');var b=document.getElementById('raw_xml_toggle_btn');if(s.style.display!=='block'){s.style.display='block';b.textContent='Hide Raw XML Editor';}else{s.style.display='none';b.textContent='Show Raw XML Editor';}})();\">Show Raw XML Editor</button>";
|
||||
echo "<div id='raw_xml_section' class='xml-raw-section'>";
|
||||
echo "<form action='?m=config_games&home_cfg_id=".$home_cfg_id."' method='post'>";
|
||||
echo "<input type='hidden' name='home_cfg_id' value='".(int)$home_cfg_id."'>";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue