site changes by codex

This commit is contained in:
Frank Harris 2025-11-20 08:10:31 -05:00
parent f0b7f96987
commit dc24d43921
34 changed files with 1736 additions and 247 deletions

View file

@ -205,7 +205,7 @@ The billing module is designed to be standalone and relocatable:
- Does NOT include panel files (like includes/functions.php)
- Connects directly to MySQL using mysqli_connect()
- Can be deployed on same machine as panel OR external web host
- Sessions are separate: "gameservers_website" namespace
- Sessions are separate: "opengamepanel_web" namespace
---
@ -245,3 +245,4 @@ The billing module is now functional with:
4. All files validated for syntax correctness
The changes are minimal, surgical, and follow the repository guidelines for standalone billing module architecture.

View file

@ -151,7 +151,7 @@ When game documentation is finished:
## Technical Notes
### Session Management
- **CRITICAL:** Always use `session_name("gameservers_website")` before `session_start()`
- **CRITICAL:** Always use `session_name("opengamepanel_web")` before `session_start()`
- Sessions are separate from panel sessions
- User authentication stored in `$_SESSION['website_user_id']`
@ -175,3 +175,4 @@ When game documentation is finished:
---
**Last Updated:** December 19, 2024
**Version:** 2.0 (with Visual TODO System)

View file

@ -65,9 +65,9 @@ Implemented comprehensive system to visually identify incomplete game documentat
### 1. PayPal Payment Capture Session Issue (FIXED)
**Problem:** Payment capture was failing with `NO_USER_SESSION` error even though user was logged in.
**Root Cause:** The `api/capture_order.php` file was calling `session_start()` without setting the session name first, so it couldn't access the `gameservers_website` session where the user_id is stored.
**Root Cause:** The `api/capture_order.php` file was calling `session_start()` without setting the session name first, so it couldn't access the `opengamepanel_web` session where the user_id is stored.
**Solution:** Added `session_name("gameservers_website")` before `session_start()` in `capture_order.php`.
**Solution:** Added `session_name("opengamepanel_web")` before `session_start()` in `capture_order.php`.
**File Modified:** `modules/billing/api/capture_order.php` (line ~148)
@ -323,3 +323,4 @@ Each game's `index.php` should follow this structure:
---
**End of Summary**

View file

@ -9,7 +9,7 @@ Successfully implemented login functionality for the website (_website/) that au
Full-featured login page with:
- Modern, responsive UI design
- Authentication against panel DB using MD5 (panel-compatible)
- Separate website session: `gameservers_website`
- Separate website session: `opengamepanel_web`
- Input validation and sanitization
- Error and success message display
- Automatic redirect after successful login
@ -71,7 +71,7 @@ Database testing utility that checks:
## Technical Details
### Session Management
- **Website Session Name:** `gameservers_website`
- **Website Session Name:** `opengamepanel_web`
- **Panel Session Name:** `opengamepanel_web` (unchanged)
- **Complete separation:** Users can be logged into one without the other
@ -178,3 +178,4 @@ All requirements from the problem statement have been met:
✅ Authenticate against panel DB
✅ Create separate login session
✅ Maintain panel compatibility

View file

@ -8,7 +8,7 @@ This implementation adds login functionality to the website that authenticates u
### 1. `_website/login.php` (NEW)
- Full-featured login page with modern UI
- Authenticates against panel DB using MD5 password hashing (panel-compatible)
- Creates separate website session using `gameservers_website` session name
- Creates separate website session using `opengamepanel_web` session name
- Logs all login attempts via logger() function
- Session variables set:
- `$_SESSION['website_user_id']` - User ID from ogp_users
@ -32,7 +32,7 @@ This implementation adds login functionality to the website that authenticates u
## Session Management
### Separate Sessions
- **Website Session**: `gameservers_website` (this implementation)
- **Website Session**: `opengamepanel_web` (this implementation)
- **Panel Session**: `opengamepanel_web` (existing panel)
These sessions are completely separate - users can be logged into one without being logged into the other.
@ -62,7 +62,7 @@ Requires connection to panel database with access to:
### For Developers:
Check if user is logged in:
```php
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) {
@ -107,3 +107,4 @@ This implementation follows the no-code planning guidelines from `.github/copilo
- Login credentials are the same as panel login (same user table)
- Website session does not grant access to panel - separate login required
- Logger function from db.php creates logfile.txt for audit trail

View file

@ -26,6 +26,7 @@ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
<a class="gsw-btn" href="./invoices.php">Invoice History</a>
<a class="gsw-btn" href="admin_coupons.php">Manage Coupons</a>
<a class="gsw-btn" href="admin_config.php">Edit Site Config</a>
<a class="gsw-btn" href="docs/xml_notes.php">XML Config Guide</a>
</div>
<hr>

View file

@ -13,6 +13,7 @@
// Include billing bootstrap (loads config and DB helper)
require_once(__DIR__ . '/bootstrap.php');
$siteBaseUrl = isset($SITE_BASE_URL) ? trim((string)$SITE_BASE_URL) : '';
// Protect this page: require admin
require_once(__DIR__ . '/includes/admin_auth.php');
@ -27,6 +28,10 @@ if (!$db) {
include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php');
echo "<div class='panel mb-12'><strong>Need the XML field reference?</strong> ";
echo "<a href=\"/modules/billing/docs/xml_notes.php\" target=\"_blank\" rel=\"noopener\">Open XML Notes</a>";
echo "</div>";
/* show errors during setup */
@ini_set('display_errors','1');
error_reporting(E_ALL);
@ -79,7 +84,12 @@ if (isset($_POST['update_remote_servers'])) {
/* helper: update one service row from posted array */
function update_service_row(mysqli $db, string $locationCol, int $sid, array $svc){
$name = esc_mysqli($db, trim($svc['service_name'] ?? ''));
$price = esc_mysqli($db, trim($svc['price_monthly'] ?? '0.00'));
$priceMonthly = number_format((float)($svc['price_monthly'] ?? 0), 2, '.', '');
$priceYearly = number_format((float)($svc['price_year'] ?? 0), 2, '.', '');
$priceDaily = number_format((float)($svc['price_daily'] ?? 0), 2, '.', '');
$priceMonthEsc = esc_mysqli($db, $priceMonthly);
$priceYearEsc = esc_mysqli($db, $priceYearly);
$priceDailyEsc = esc_mysqli($db, $priceDaily);
$img = esc_mysqli($db, trim($svc['img_url'] ?? ''));
$en = !empty($svc['enabled']) ? 1 : 0;
@ -104,7 +114,9 @@ function update_service_row(mysqli $db, string $locationCol, int $sid, array $sv
`{$locationCol}`='{$locListEsc}',
slot_min_qty={$minSlots},
slot_max_qty={$maxSlots},
price_monthly='{$price}',
price_daily='{$priceDailyEsc}',
price_monthly='{$priceMonthEsc}',
price_year='{$priceYearEsc}',
img_url='{$img}',
enabled={$en}
WHERE service_id={$sid}";
@ -178,7 +190,9 @@ $services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locat
<th>Service Name <small class="muted">(ID below)</small></th>
<th>Min Slots</th>
<th>Max Slots</th>
<th>Price (Daily)</th>
<th>Price (Monthly)</th>
<th>Price (Year)</th>
<th>Thumbnail URL</th>
<th>Preview</th>
<th>Update Row</th>
@ -196,8 +210,8 @@ $services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locat
if ($imgUrl !== '') {
if (is_abs_url($imgUrl)) {
$displayUrl = $imgUrl;
} elseif ($SITE_BASE_URL !== '') {
$displayUrl = join_base($SITE_BASE_URL, $imgUrl);
} elseif ($siteBaseUrl !== '') {
$displayUrl = join_base($siteBaseUrl, $imgUrl);
} else {
// Use relative path (local folder)
$displayUrl = $imgUrl;
@ -227,10 +241,18 @@ $services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locat
<input type="number" name="service[<?php echo $sid; ?>][slot_max_qty]" value="<?php echo (int)$row['slot_max_qty']; ?>" min="1" step="1" class="w-90">
</td>
<td>
<input type="text" name="service[<?php echo $sid; ?>][price_daily]" value="<?php echo h(number_format((float)$row['price_daily'], 2, '.', '')); ?>" size="8">
</td>
<td>
<input type="text" name="service[<?php echo $sid; ?>][price_monthly]" value="<?php echo h($row['price_monthly']); ?>" size="8">
</td>
<td>
<input type="text" name="service[<?php echo $sid; ?>][price_year]" value="<?php echo h(number_format((float)$row['price_year'], 2, '.', '')); ?>" size="8">
</td>
<!-- Thumbnail URL input -->
<td>
<input type="text" name="service[<?php echo $sid; ?>][img_url]" value="<?php echo h($row['img_url']); ?>" class="min-w-240">
@ -305,6 +327,17 @@ $services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locat
</form>
<?php endif; ?>
<div class="panel" style="margin-top:20px;">
<h3>Environment</h3>
<table class="cart-table">
<tr><th>Site Base URL</th><td><?php echo $siteBaseUrl !== '' ? h($siteBaseUrl) : '(empty — using relative paths)'; ?></td></tr>
<tr><th>Data directory</th><td><?php echo isset($SITE_DATA_DIR) ? h($SITE_DATA_DIR) : '(unset)'; ?></td></tr>
<tr><th>PHP SAPI</th><td><?php echo h(PHP_SAPI); ?></td></tr>
<tr><th>Writable?</th><td><?php echo (isset($SITE_DATA_DIR) && is_writable($SITE_DATA_DIR)) ? 'yes' : 'no'; ?></td></tr>
<tr><th>XML Reference</th><td><a href="/modules/billing/docs/xml_notes.php" target="_blank" rel="noopener">Open XML Notes</a></td></tr>
</table>
</div>
<!-- JS: Per-row: enable/disable Primary radios based on whether that location is checked -->
<script>
document.querySelectorAll('.locs-box').forEach(function(box){

View file

@ -138,7 +138,7 @@ log_payment('PAYMENT_CAPTURED', [
// Start session to get user_id (use billing website session name)
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
}
$user_id = isset($_SESSION['website_user_id']) ? intval($_SESSION['website_user_id']) :
@ -152,8 +152,8 @@ if ($user_id <= 0) {
}
// Connect to database
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
if (!$db) {
$mysqli = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
if (!$mysqli) {
log_payment('DB_CONNECTION_FAILED', mysqli_connect_error());
ob_clean();
echo json_encode(['error' => 'db_connection_failed', 'request_id' => $requestId]);
@ -161,8 +161,8 @@ if (!$db) {
}
$now = date('Y-m-d H:i:s');
$esc_txid = mysqli_real_escape_string($db, $txid);
$esc_paypal_json = mysqli_real_escape_string($db, $paypal_json);
$esc_txid = mysqli_real_escape_string($mysqli, $txid);
$esc_paypal_json = mysqli_real_escape_string($mysqli, $paypal_json);
// Apply coupon from session to invoices before marking paid
session_start();
@ -171,12 +171,12 @@ if ($coupon_id > 0) {
// Get unpaid invoices for this user to apply coupon
$invoices_query = "SELECT invoice_id, amount FROM {$table_prefix}billing_invoices
WHERE user_id=$user_id AND status='due'";
$invoices_result = mysqli_query($db, $invoices_query);
$invoices_result = mysqli_query($mysqli, $invoices_query);
// Get coupon details
$coupon_query = "SELECT discount_percent FROM {$table_prefix}billing_coupons
WHERE coupon_id=$coupon_id AND is_active=1 LIMIT 1";
$coupon_result = mysqli_query($db, $coupon_query);
$coupon_result = mysqli_query($mysqli, $coupon_query);
if ($coupon_result && mysqli_num_rows($coupon_result) === 1) {
$coupon_row = mysqli_fetch_assoc($coupon_result);
@ -194,7 +194,7 @@ if ($coupon_id > 0) {
discount_amount=" . number_format($discount_amt, 2, '.', '') . ",
amount=" . number_format($new_amount, 2, '.', '') . "
WHERE invoice_id=$inv_id";
mysqli_query($db, $update_coupon_sql);
mysqli_query($mysqli, $update_coupon_sql);
log_payment('COUPON_APPLIED', ['invoice_id' => $inv_id, 'discount' => $discount_amt]);
}
@ -202,7 +202,7 @@ if ($coupon_id > 0) {
$update_usage_sql = "UPDATE {$table_prefix}billing_coupons
SET current_uses = current_uses + 1
WHERE coupon_id=$coupon_id";
mysqli_query($db, $update_usage_sql);
mysqli_query($mysqli, $update_usage_sql);
// Clear coupon from session
unset($_SESSION['cart_coupon_code']);
@ -216,45 +216,84 @@ $updateInvoicesSql = "UPDATE {$table_prefix}billing_invoices
WHERE user_id=$user_id AND status='due'";
log_payment('UPDATE_INVOICES_SQL', $updateInvoicesSql);
$updateResult = mysqli_query($db, $updateInvoicesSql);
$updateResult = mysqli_query($mysqli, $updateInvoicesSql);
if (!$updateResult) {
log_payment('UPDATE_INVOICES_FAILED', mysqli_error($db));
mysqli_close($db);
log_payment('UPDATE_INVOICES_FAILED', mysqli_error($mysqli));
mysqli_close($mysqli);
ob_clean();
echo json_encode(['error' => 'update_invoices_failed', 'request_id' => $requestId]);
exit;
}
$affectedInvoices = mysqli_affected_rows($db);
$affectedInvoices = mysqli_affected_rows($mysqli);
log_payment('INVOICES_MARKED_PAID', ['count' => $affectedInvoices]);
// Get all invoices we just marked paid
$getInvoicesSql = "SELECT * FROM {$table_prefix}billing_invoices
WHERE user_id=$user_id AND payment_txid='$esc_txid'";
$invoicesResult = mysqli_query($db, $getInvoicesSql);
$invoicesResult = mysqli_query($mysqli, $getInvoicesSql);
$ordersCreated = 0;
$renewedOrders = 0;
$newOrderIds = [];
while ($inv = mysqli_fetch_assoc($invoicesResult)) {
$invoice_id = intval($inv['invoice_id']);
$existing_order_id = intval($inv['order_id'] ?? 0);
// Skip if invoice already linked to an order (renewal)
// Handle renewals by extending the existing order
if ($existing_order_id > 0) {
log_payment('RENEWAL_INVOICE', ['invoice_id' => $invoice_id, 'order_id' => $existing_order_id]);
$durationUnit = strtolower(trim($inv['invoice_duration'] ?? 'month'));
$allowedDurations = ['day','month','year'];
if (!in_array($durationUnit, $allowedDurations, true)) {
$durationUnit = 'month';
}
$qty = max(1, intval($inv['qty'] ?? 1));
$orderInfoSql = "SELECT end_date FROM {$table_prefix}billing_orders WHERE order_id=$existing_order_id LIMIT 1";
$orderInfoRes = mysqli_query($mysqli, $orderInfoSql);
$currentEnd = null;
if ($orderInfoRes && mysqli_num_rows($orderInfoRes) === 1) {
$infoRow = mysqli_fetch_assoc($orderInfoRes);
$currentEnd = $infoRow['end_date'] ?? null;
}
$baseTs = time();
if (!empty($currentEnd)) {
$parsed = strtotime($currentEnd);
if ($parsed !== false && $parsed > time()) {
$baseTs = $parsed;
}
}
$newEndDate = date('Y-m-d H:i:s', strtotime("+$qty $durationUnit", $baseTs));
$renewSql = "UPDATE {$table_prefix}billing_orders
SET status='installed', end_date='$newEndDate', paid_ts='$now', payment_txid='$esc_txid'
WHERE order_id=$existing_order_id LIMIT 1";
if (mysqli_query($mysqli, $renewSql)) {
$renewedOrders++;
log_payment('ORDER_RENEWED', [
'order_id' => $existing_order_id,
'invoice_id' => $invoice_id,
'new_end_date' => $newEndDate
]);
} else {
log_payment('ORDER_RENEWAL_FAILED', [
'order_id' => $existing_order_id,
'invoice_id' => $invoice_id,
'error' => mysqli_error($mysqli)
]);
}
continue;
}
// Create new order
$service_id = intval($inv['service_id']);
$home_name = mysqli_real_escape_string($db, $inv['home_name']);
$home_name = mysqli_real_escape_string($mysqli, $inv['home_name']);
$ip = intval($inv['ip']);
$max_players = intval($inv['max_players']);
$qty = intval($inv['qty']);
$duration = mysqli_real_escape_string($db, $inv['invoice_duration']);
$duration = mysqli_real_escape_string($mysqli, $inv['invoice_duration']);
$amount = floatval($inv['amount']);
$rcon_pw = mysqli_real_escape_string($db, $inv['remote_control_password']);
$ftp_pw = mysqli_real_escape_string($db, $inv['ftp_password']);
$rcon_pw = mysqli_real_escape_string($mysqli, $inv['remote_control_password']);
$ftp_pw = mysqli_real_escape_string($mysqli, $inv['ftp_password']);
// Calculate end_date
$end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration"));
@ -272,25 +311,46 @@ while ($inv = mysqli_fetch_assoc($invoicesResult)) {
log_payment('INSERT_ORDER_SQL', substr($insertOrderSql, 0, 300));
if (mysqli_query($db, $insertOrderSql)) {
$new_order_id = mysqli_insert_id($db);
if (mysqli_query($mysqli, $insertOrderSql)) {
$new_order_id = mysqli_insert_id($mysqli);
log_payment('ORDER_CREATED', ['order_id' => $new_order_id, 'invoice_id' => $invoice_id]);
$newOrderIds[] = $new_order_id;
// Link invoice to order
$linkSql = "UPDATE {$table_prefix}billing_invoices SET order_id=$new_order_id WHERE invoice_id=$invoice_id";
mysqli_query($db, $linkSql);
mysqli_query($mysqli, $linkSql);
$ordersCreated++;
} else {
log_payment('INSERT_ORDER_FAILED', mysqli_error($db));
log_payment('INSERT_ORDER_FAILED', mysqli_error($mysqli));
}
}
mysqli_close($db);
mysqli_close($mysqli);
$autoProvisionResult = ['provisioned_count' => 0, 'failed_count' => 0, 'orders' => []];
if (!empty($newOrderIds)) {
require_once __DIR__ . '/../includes/panel_bridge.php';
$panelCtx = billing_panel_bootstrap();
if ($panelCtx && isset($panelCtx['db']) && $panelCtx['db'] instanceof OGPDatabase) {
$GLOBALS['db'] = $panelCtx['db'];
$GLOBALS['settings'] = $panelCtx['settings'];
require_once __DIR__ . '/../create_servers.php';
$autoProvisionResult = billing_invoke_provision([
'order_ids' => $newOrderIds,
'user_id' => $user_id,
'is_admin' => true
]);
log_payment('AUTO_PROVISION_COMPLETE', $autoProvisionResult);
} else {
log_payment('AUTO_PROVISION_SKIPPED', 'panel bootstrap failed');
}
}
log_payment('PROCESSING_COMPLETE', [
'invoices_paid' => $affectedInvoices,
'orders_created' => $ordersCreated,
'orders_renewed' => $renewedOrders,
'txid' => $txid
]);
@ -301,5 +361,9 @@ echo json_encode([
'order_id' => $paypal_order_id,
'txid' => $txid,
'invoices_paid' => $affectedInvoices,
'orders_created' => $ordersCreated
'orders_created' => $ordersCreated,
'orders_renewed' => $renewedOrders,
'provisioned' => $autoProvisionResult['provisioned_count'] ?? 0
]);

View file

@ -3,6 +3,9 @@
// Central bootstrap for billing website pages. Loads config, provides safe DB helper
// and ensures $table_prefix is available.
// Ensure session sync with panel happens first
require_once __DIR__ . '/includes/session_bridge.php';
// Load configuration (includes/config.inc.php) if present
$config_path = __DIR__ . '/includes/config.inc.php';
if (file_exists($config_path)) {

View file

@ -7,7 +7,7 @@
// Start session with website session name
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
}
@ -760,3 +760,4 @@ $siteBase = $protocol . $host;
<?php include(__DIR__ . '/includes/footer.php'); ?>
</body>
</html>

View file

@ -1,15 +1,48 @@
<?php
require_once("includes/lib_remote.php");
require_once("modules/config_games/server_config_parser.php");
require_once __DIR__ . '/../../includes/lib_remote.php';
require_once __DIR__ . '/../config_games/server_config_parser.php';
if (!function_exists('billing_invoke_provision')) {
function billing_invoke_provision(array $options = array())
{
$GLOBALS['BILLING_PROVISION_OVERRIDE'] = $options;
ob_start();
exec_ogp_module();
$output = ob_get_clean();
$result = isset($GLOBALS['BILLING_PROVISION_LAST_RESULT']) ? $GLOBALS['BILLING_PROVISION_LAST_RESULT'] : array();
$result['output'] = $output;
unset($GLOBALS['BILLING_PROVISION_OVERRIDE'], $GLOBALS['BILLING_PROVISION_LAST_RESULT']);
return $result;
}
}
function exec_ogp_module()
{
global $db,$view,$settings;
$user_id = $_SESSION['user_id'];
$isAdmin = $db->isAdmin( $_SESSION['user_id'] );
$override = isset($GLOBALS['BILLING_PROVISION_OVERRIDE']) ? $GLOBALS['BILLING_PROVISION_OVERRIDE'] : null;
$user_id = isset($override['user_id']) ? intval($override['user_id']) : (isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0);
$isAdmin = isset($override['is_admin']) ? (bool)$override['is_admin'] : $db->isAdmin($user_id);
$provision_all = $override ? !empty($override['provision_all']) : isset($_POST['provision_all']);
$orderIds = array();
if ($override && !empty($override['order_ids'])) {
$orderIds = array_map('intval', (array)$override['order_ids']);
}
if (empty($orderIds)) {
$order_id = null;
if (isset($_POST['order_id'])) {
$order_id = $_POST['order_id'];
}
if(isset($_GET['order_id'])){
$order_id = $_GET['order_id'];
}
if (!empty($order_id)) {
$orderIds = array(intval($order_id));
}
}
// Handle provision_all request - provision all paid orders for this user
if (isset($_POST['provision_all'])) {
if ($provision_all) {
if ( $isAdmin ){
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE status='paid' ORDER BY order_id" );
} else {
@ -18,25 +51,19 @@ function exec_ogp_module()
}
// Handle provision_single or order_id parameter - provision specific order
else {
$order_id = null;
if (isset($_POST['order_id'])) {
$order_id = $_POST['order_id'];
}
if(isset($_GET['order_id'])){
$order_id = $_GET['order_id'];
}
if (!$order_id) {
if (empty($orderIds)) {
echo "<div class='failure'>No order ID specified.</div>";
$GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array('provisioned_count'=>0,'failed_count'=>0,'orders'=>array());
return;
}
$idList = implode(',', array_map('intval', $orderIds));
if ( $isAdmin ){
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id=".$db->realEscapeSingle($order_id)." AND status='paid'" );
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND status='paid'" );
} else {
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id=".$db->realEscapeSingle($order_id)." AND user_id=".$db->realEscapeSingle($user_id)." AND status='paid'" );
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND user_id=".$db->realEscapeSingle($user_id)." AND status='paid'" );
}
}
$processed_orders = array();
if( !empty($orders) )
{
$provisioned_count = 0;
@ -45,6 +72,7 @@ function exec_ogp_module()
foreach($orders as $order)
{
$order_id = $order['order_id'];
$processed_orders[] = intval($order_id);
$service_id = $order['service_id'];
$home_name = $order['home_name'];
$remote_control_password = $order['remote_control_password'];
@ -408,7 +436,14 @@ function exec_ogp_module()
echo "<p>No paid orders found to provision.</p>";
echo "</div>";
echo "<p><a href='home.php?m=billing&p=my_orders' class='btn'>View My Orders</a></p>";
$provisioned_count = 0;
$failed_count = 0;
}
$GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array(
'provisioned_count' => isset($provisioned_count) ? $provisioned_count : 0,
'failed_count' => isset($failed_count) ? $failed_count : 0,
'orders' => $processed_orders,
);
}
?>

View file

@ -15,7 +15,7 @@ echo "Session cookie params: " . json_encode(session_get_cookie_params()) . "\n"
echo "Session status (before start): " . session_status() . "\n";
// Try to start a named session used by _website
session_name('gameservers_website');
session_name('opengamepanel_web');
@session_start();
echo "Session status (after start): " . session_status() . "\n";
echo "Session id: " . session_id() . "\n";
@ -70,3 +70,4 @@ echo " - Ensure the site path is served under the expected /_website/ path and t
echo " - If sessions aren't persistent across requests, check webserver user permissions and session.save_path.\n";
?>

View file

@ -6,7 +6,7 @@
// Start session using the website session name to match the rest of the site
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
}
@ -428,3 +428,4 @@ uksort($grouped, function($a, $b) use ($categoryOrder) {
</div>
</body>
</html>

View file

@ -8,12 +8,12 @@ This document summarizes the comprehensive enhancements made to the billing modu
### 1. Documentation Page Login Button Issue ✅
**Problem:** Documentation page showed "Login" button even when user was logged in.
**Root Cause:** docs.php used basic `session_start()` instead of the website's session name.
**Solution:** Changed to use `gameservers_website` session name to match rest of website.
**Solution:** Changed to use `opengamepanel_web` session name to match rest of website.
### 2. Cart Page Display Issue ✅
**Problem:** Cart page didn't display when clicking menu link.
**Root Cause:** cart.php also used basic `session_start()` causing session inconsistency.
**Solution:** Changed to use `gameservers_website` session name for consistency.
**Solution:** Changed to use `opengamepanel_web` session name for consistency.
### 3. Documentation Content Enhancement ✅
**Problem:** Documentation was basic, system-specific, and not comprehensive enough for SEO.
@ -33,7 +33,7 @@ session_start();
// NEW
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
}
```
@ -179,7 +179,7 @@ The same comprehensive template can be applied to all remaining games:
## Implementation Notes
### Session Name Consistency
The entire billing module now uses `gameservers_website` session name:
The entire billing module now uses `opengamepanel_web` session name:
- login.php ✅
- register.php ✅
- logout.php ✅
@ -247,3 +247,4 @@ The documentation system is now:
*Created: November 8, 2024*
*Last Updated: November 8, 2024*

View file

@ -0,0 +1,622 @@
## OGP XML Notes / still W.I.P.
_The order of each XML element matters, and this guide presents them in their order of appearance!_
___
### Linux and Windows:
#### Game Config:
This is the first element. There can only be one `<game_config>` element.
```
<game_config>
the whole XML file content here
</game_config>
```
All the following elements should be contained within `<game_config>` element.
___
#### Game Key:
Comes after `<game_config>` element (actually within `<game_config>` element as said above). There can only be one `<game_key>` element. Example:
```
<game_key>space_engineers_win64</game_key>
```
This is a unique key used to identify this specific game server in OGP. You should not use spaces, nor any special character in it, only alpha-numeric value and underscores. It should contain a suffix related to the compatible OS. Available suffixes are `_win32`, `_win64`, `_linux32`, `_linux64`, using one of these suffixes in the game_key will let OGP know which OS it is available on, making it visible or not when you install a new game server, depending on your OS architecture. `_win` and `_linux` work too, but we highly recommend to now use the previously listed suffixes.
___
#### Query Protocol:
Comes after `<game_key>` element. There can only be one `<protocol>` element. Example:
```
<protocol>lgsl</protocol>
```
It defines the query protocol used by OGP. Available protocols are `lgsl`, `gameq`, `rcon` (`rcon2`? `lcon`?)
___
#### LGSL Query Name:
Comes after `<protocol>` element. There can only be one `<lgsl_query_name>` element. Example:
```
<lgsl_query_name>killingfloor2</lgsl_query_name>
```
This is the unique key referencing this specific game server in the LGSL protocol file, used to query the game server.
___
#### GameQ Query Name:
Comes after `<protocol>` element. There can only be one `<gameq_query_name>` element. Example:
```
<gameq_query_name>redorchestra2</gameq_query_name>
```
This is the unique key referencing this specific game server in the GameQ protocol files, used to query the game server.
___
#### Installer:
Comes after `<protocol>` element (or comes after `<lgsl_query_name>` or `<gameq_query_name>` element when used). There can only be one `<installer>` element. Example:
```
<installer>steamcmd</installer>
```
Defines the use of SteamCMD tool to install the game server.
___
#### Game Name:
Comes after `<installer>` element. There can only be one `<game_name>` element. Example:
```
<game_name>Killing Floor 2</game_name>
```
This is the real game server name appearing in the list when installing a new game server.
___
#### Server Executable Name:
Comes after `<game_name>` element. There can only be one `<server_exec_name>` element. Example:
```
<server_exec_name>SpaceEngineersDedicated.exe</server_exec_name>
```
This is the server executable name used in the start command line.
___
#### Query Port:
Comes after `<server_exec_name>` element. There can only be one `<query_port>` element. Example:
```
<query_port type='add'>13</query_port>
```
Difference between the server port (`%PORT%`) and the query port (`%QUERY_PORT%`). In this example the variable %QUERY_PORT% will be 13 added to the port value.
___
#### CLI Template:
Comes after `<server_exec_name>` element. There can only be one `<cli_template>` element. Example:
```
<cli_template>-console %BASE_PATH% -ignorelastsession</cli_template>
```
```
<cli_template>%MAP%%GAMEMODE%%DIFFICULTY%%GAMELENGTH%%PLAYERS%%MUTATOR% %PORT% %IP% %WEB_ADMIN_PORT% %QUERY_PORT%</cli_template>
```
This is the template that will generate the start command line placed after the server executable name.
You can use these variables which are known to OGP:
```
GAME_TYPE
HOSTNAME
IP
MAP
PID_FILE
PLAYERS
PORT
QUERY_PORT
BASE_PATH
HOME_PATH
SAVE_PATH
OUTPUT_PATH
CONTROL_PASSWORD
```
These variable should be between `%` characters, the Panel will then replace them with the proper value when generating the start command line.
You can also use custom variables that you will define later in the XML.
___
#### CLI Parameters:
Comes after `<cli_template>` element. There can only be one `<cli_params>` element. Example:
```
<cli_params>
<cli_param id="MAP" cli_string="" />
<cli_param id="IP" cli_string="-MultiHome=" options="q"/>
<cli_param id="PORT" cli_string="-Port=" options="sq"/>
<cli_param id="PID_FILE" cli_string="" />
<cli_param id="GAME_TYPE" cli_string="" />
<cli_param id="HOME_PATH" cli_string="" />
</cli_params>
```
It defines the known variables used in `<cli_template>`. In this example we can imagine that for **%MAP%** it will generate the map name without space or quotes around it, because there is no `options`, for **%IP%** it will generate `-MultiHome="123.123.123.123"` using the `cli_string` and adding only quotes around the game server IP value because of `options="q"`, and **%PORT%** will generate `-Port= "27015"` using the `cli_string` and adding space and quotes around the game server port because of `options="sq"`.
___
#### Reserve Ports:
Comes after `<cli_template>` or `<cli_params>` element. There can only be one `<reserve_ports>` element. Example:
```
<reserve_ports>
<port type="subtract" id="WEB_ADMIN_PORT" cli_string="-WebAdminPort=">5</port>
<port type="add" id="STEAM_PORT" cli_string="-SteamPort=">19238</port>
<port type="add" id="MY_CUSTOM_PORT">666</port>
</reserve_ports>
```
You can add reserved ports here to use in the generated start command line. These ports will also be managed by OGP if OGP is used to manage the Agent machine firewall. Type can be `add` or `subtract` which is self explanatory. In this example when using the %WEB_ADMIN_PORT% variable in the `<cli_template>` it will generate `-WebAdminPort=XXX`, XXX being 5 subtracted to the Port set for this game server, when using the %STEAM_PORT% variable in the `<cli_template>` it will generate `-SteamPort=XXX` where XXX will be 19238 added to the Port set for this game server. As you can see, the variable %MY_CUSTOM_PORT% have no `cli_string`, this can be used this way to simply open this port (which here would be 666 added to the game server port) in the Agent machine firewall, when OGP is set to control the machine firewall (note: the firewall management may actually not open anything else than the game server port, to be verified).
___
#### CLI Allowed Characters:
Comes after `<reserve_ports>` element. There can only be one `<cli_allow_chars>` element. Example:
```
<cli_allow_chars>;</cli_allow_chars>
```
Used to allow some special characters in the command line. Escaped by default: ```\ " ' | & ; > < ` $ ( ) [ ]```
___
#### Maps Location:
Comes after `<cli_template>` element. There can only be one `<maps_location>` element. Example:
```
<maps_location>folder/maps</maps_location>
```
It sets the path of the map folder for this game server, which will be used to generate a selectable map list available before starting the game server. If folder contains map files it will use their name without the extension, if it contains sub folders with each map inside each own sub folder, it will generate the map list based on the folders names contained in the defined path. The selected map in the list will be used to replace the %MAP% variable in the `<cli_template>`.
___
#### Map List:
Comes after `<cli_template>` element. There can only be one `<map_list>` element. Example:
```
<map_list>maplist.txt</map_list>
```
The map list file path used to generate the selectable map list available before starting the game server. In this example it will look for a file called maplist.txt inside the root of the game server. Map list should have one map per line. The selected map in the list will be used to replace the %MAP% variable in the `<cli_template>`.
___
#### Console Log:
Comes after `<cli_template>` element. There can only be one `<console_log>` element. Example:
```
<console_log>KFGame/Logs/Launch.log</console_log>
```
It defines the path of the log file that will be shown in the LOG page of the game server. Most game servers, especially on Linux, will not need that, when in general, Windows game server will need it to properly show the output log.
___
#### Executable Location:
Comes after `<console_log>` element. There can only be one `<exe_location>` element. Example:
```
<exe_location>Binaries/Win64</exe_location>
```
It defines the path of the game server executable when not in the root folder.
___
#### Max User Amount:
Comes after `<exe_location>` element. There can only be one `<max_user_amount>` element. Example:
```
<max_user_amount>64</max_user_amount>
```
It defines the maximum player number you will be able to set when creating the game server.
___
#### Control Protocol:
Comes after `<max_user_amount>` element. There can only be one `<control_protocol>` element. Example:
```
<control_protocol>rcon2</control_protocol>
```
Can be `rcon`, `rcon2`, or `lcon` (legacy). Note that `rcon` can also have type option to define, which can be `old` or `new`. Example:
```
<control_protocol>rcon</control_protocol>
<control_protocol_type>old</control_protocol_type>
```
___
#### Mods:
Comes after `<control_protocol>` element. There can only be one `<mods>` element. Example:
```
<mods>
<mod key="insurgency">
<name>none</name>
<installer_name>237410</installer_name>
</mod>
</mods>
```
Used to define different mods for the game server, in this example there is only one mod available which will be the default installed one (actually the game server itself here). The `<installer_name>` here is the Steam appID that will be used to install and update the game server. (note: case RSync to explain? Case multi mods to explain?)
___
#### Replace Texts
`<replace_texts>` Comes after `<mods>`.
Contains multiple `<text>` entries like so:
```
<replace_texts>
<text key="control_password">
<default>ServerAdminPassword=.*</default>
<var>ServerAdminPassword=</var>
<filepath>ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini</filepath>
<options>sq</options>
</text>
<text key="home_name">
<default>SessionName=.*</default>
<var>SessionName=</var>
<filepath>ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini</filepath>
<options>sq</options>
</text>
</replace_texts>
```
`<default>` within `<text>` is what the line to replace starts with.
`<var>` within `<text>` is the key for what should be kept when the line is replaced with the value entered by the user when replacing text occurs.
`<filepath>` within `<text>` specifies the text file to make the replacement in.
`<options>` within `<text>` specifies how to enter the user's value after the `<var>` key. Possible options are:
```
nothing / no value = placed as is
s = space / separated
q = quoted
sq = space and quotes
sc = space and ends with a comma
sqc = space, quoted, and ends with a comma
```
These replace text will be applied on server start and modify the specified config files with the values generated by the Panel.
___
#### Server Params:
`<server_params>` Comes after `<replace_texts>`.
Contains multiple `<param>` entries like so:
```
<server_params>
<param id="SP" key="?ServerPassword=" type="text">
<option>ns</option>
<caption>Server Password</caption>
<desc>Players must know this password to connect.</desc>
</param>
<param id="DIFFICULTY" key="?Difficulty=" type="select">
<option value=""></option>
<option value="0">Normal</option>
<option value="1">Hard</option>
<options>ns</options>
<caption>Difficulty</caption>
<desc>This sets the server difficulty. Leave empty to configure this parameter in the config files or webadmin</desc>
</param>
<param key="-EnableCheats" type="checkbox_key_value">
<caption>Cheats</caption>
<desc>Enable the cheats to be used from the ingame Admin menu</desc>
</param>
</server_params>
```
`id` attribute on the `<param>` specifies which variable to replace in the `<cli_template>`
`key` attribute specifies what will replace the variable defined by id attribute
type attribute will define what kind of parameter it is, possible values are `text` `select` `checkbox_key_value`:
- `text` will allow to write text value to be added during the replacement of the variables in startup command line. For example, `%SP%` in `<cli_template>` would be replaced with `?ServerPassword=XXX` where XXX would be the value entered by the user in the text box, if nothing is entered the variable is not replaced but removed from `<cli_template>`. The value entered can be modified to fit your needs by using the `<option>` element within the `<param>` element.
- `select` will create a selectable list to choose the parameter value. The `%DIFFICULTY%` variable in `<cli_template>` would be replaced with `?Difficulty=1` if `Hard` would have been selected before starting the server.
- `checkbox_key_value` will add a simple checkbox that, if ticked before starting server, would add the parameter `-EnableCheats` at the end of the startup command line.
Valid options are for the `<options>` element within the `<param>` element within the `<server_params>` element are:
```
ns = no space between key and value
q = quotes wrapped around value after key (no space added)
s = space added after key before value (no quotes added)
anything else = space after key and quotes around the value
```
___
#### Custom Fields:
Comes after `<server_params>` element. There can only be one `<custom_fields>` element. Example:
```
<custom_fields>
<field key="sv_maxrate" type="text">
<default>sv_maxrate.*</default>
<default_value>0</default_value>
<var>sv_maxrate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>Max bandwidth rate allowed on server ( bytes per second ), 0 == unlimited.</desc>
</field>
<field key="sv_minrate" type="text">
<default>sv_minrate.*</default>
<default_value>5000</default_value>
<var>sv_minrate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>Min bandwidth rate allowed on server ( bytes per second ), 0 == unlimited.</desc>
</field>
<field key="sv_maxcmdrate" type="text">
<default>sv_maxcmdrate.*</default>
<default_value>66</default_value>
<var>sv_maxcmdrate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>If sv_mincmdrate is > 0, this sets the maximum value for cl_cmdrate.</desc>
</field>
<field key="sv_mincmdrate" type="text">
<default>sv_mincmdrate.*</default>
<default_value>67</default_value>
<var>sv_mincmdrate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>This sets the minimum value for cl_cmdrate. 0 == unlimited.</desc>
</field>
<field key="sv_maxupdaterate" type="text">
<default>sv_maxupdaterate.*</default>
<default_value>66</default_value>
<var>sv_maxupdaterate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>Maximum updates per second that the server will allow.</desc>
</field>
<field key="sv_minupdaterate" type="text">
<default>sv_minupdaterate.*</default>
<default_value>67</default_value>
<var>sv_minupdaterate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>Minimum updates per second that the server will allow.</desc>
</field>
</custom_fields>
```
Custom Fields available when you go to Custom Fields button from the game server page. These custom fields will be applied on every server start and will replace the specific values in the specified config files.
___
#### List Players Command:
Comes after `<custom_fields>` element. There can only be one `<list_players_command>` element. Example:
```
<list_players_command>status</list_players_command>
```
This is the command to list the players on the server when using `control_protocol`.
___
#### Player Info Regex:
Comes after `<list_players_command>` element. There can only be one `<player_info_regex>` element. Example:
```
<player_info_regex>#\#\s*(\d*)\s*\"(.*)\".*#</player_info_regex>
```
Regex used for player info when using `control_protocol`.
___
#### Player Info:
Comes after `<player_info_regex>` element. There can only be one `<player_info>` element. Example:
```
<player_info>
<index key="1">userid</index>
<index key="2">Name</index>
</player_info>
```
Defines the different keys for player infos when using `control_protocol`.
___
#### Player Commands:
Comes after `<player_info>` element. There can only be one `<player_commands>` element. Example:
```
<player_commands>
<command key="Kick" type="hidden">
<string>kick "%Name%"</string>
</command>
<command key="Ban" type="select">
<option value="0">Permanent</option>
<option value="15">15m</option>
<option value="30">30m</option>
<option value="60">1h</option>
<option value="1440">1D</option>
<option value="10080">1W</option>
<option value="43200">30D</option>
<string>banid %input% %userid% kick</string>
</command>
<command key="Change Nick" type="text">
<default>new nick</default>
<string>sm_rename #%userid% "%input%"</string>
</command>
</player_commands>
```
Defines the commands available when using `control_protocol`.
___
#### Pre Install:
Comes after `<player_commands>` element. There can only be one `<pre_install>` element. It can contain multiple entries one per line. Example:
```
<pre_install>
</pre_install>
```
Script executed before installing the game server.
___
#### Post Install:
Comes after `<pre_install>` element. There can only be one `<post_install>` element. It can contain multiple entries one per line. Example:
```
<post_install>
cp linux64/steamclient.so ./steamclient.so
echo &apos;#!/bin/bash
./bin/AvorionServer $@&apos; &gt; avorion_ogpstarter.sh
chmod +x avorion_ogpstarter.sh
</post_install>
```
Script executed after installing or updating the game server, similar to `pre_install` (which `pre_install is executed before installation/update obviously)`. In this example, after installation or update of the game server, it will copy the file linux64/steamclient.so to ./steamclient.so and generate a bash script in the file avorion_ogpstarter.sh and then make this bash script executable. Note that the bash script must be written to be properly interpreted by the XML (see the `"` character replaced by `&apos;`, `>` replaced by `&gt;`, and so on.. if you write plain bash script it will break the XML in Panel, so keep that in mind and replace special characters with their XML readable counterpart.
___
### Windows:
#### Pre-Start Commands:
Comes after the `<server_params>` element. There can only be one `<pre_start>` element. It can run multiple lines of script that will be executed by the cmd batch environment.
This can be filled with lines of script that will be run in a batch script just before the game server is started. The script will always change directory into the home directory before your commands will run, so you can reference things locally.
```
<pre_start></pre_start>
```
___
#### Environment Variables:
Comes after `<pre_start>` element. There can only be one `<environment_variables>` element. It can contain multiple entries one per line.
```
<environment_variables>
</environment_variables>
```
Used for setting environment variables which may be needed by some servers. This is run in the batch environment, so please make sure you're using Windows commands for your environment SETS.
Special entries:
`{OGP_HOME_DIR}` will be replaced by the home directory for the game server.
___
### Linux:
#### Pre-Start Commands:
Comes after the `<server_params>` element. There can only be one `<pre_start>` element. It can run multiple lines of script that will be executed by the bash shell.
This can be filled with lines of script that will be run in a bash script just before the game server is started. You do NOT need to provide the shebang "#!/bin/bash" in your commands. The script will always change directory into the home directory before your commands will run, so you can reference things locally.
```
<pre_start></pre_start>
```
Example (writes hiya to a file named testingPreStart in the home directory of the server):
```
<pre_start>
echo "hiya" >> testingPreStart
</pre_start>
```
___
#### Environment Variables:
Comes after `<pre_start>` element. There can only be one `<environment_variables>` element. It can contain multiple entries one per line.
```
<environment_variables>
</environment_variables>
```
Used for setting environment variables which may be needed by some servers such as Rust.
Example:
```
<environment_variables>
export LD_LIBRARY_PATH="{OGP_HOME_DIR}/RustDedicated_Data/Plugins/x86_64"
</environment_variables>
```
Special entries:
`{OGP_HOME_DIR}` will be replaced by the home directory for the game server.
___
### Linux and Windows:
#### Locking / Protecting Additional Files:
Comes after `<environment_variables>` element. There can only be one `<lock_files>` element. It can contain multiple entries one per line.
```
<lock_files>
</lock_files>
```
Used for protecting additional game server files by using the system program `chattr`. This is a Linux only element. You can use relative (paths are relative to the home directory for the game server) or absolute paths.
Example:
```
<lock_files>
bin/AvorionServer
</lock_files>
```
The above example adds chattr +i to the bin/AvorionServer executable. This prevents the user from changing it. When SteamCMD or Rsync is run to update the files, everything is unlocked, but the files specified here will be locked post update / install. These files are also locked again (just to make sure) before the server is restarted and started. Entries here will not BREAK or AFFECT the steamcmd or Rsync update process.
Special entries:
`{OGP_HOME_DIR}` will be replaced by the home directory for the game server. However, if you're using this variable, you should just use a local path.
___
#### Configuration Files:
Comes after `<lock_files>` element. There can only be one `<configuration_files>` element.
There can be multiple `<file>` entries within `<configuration_files>` element, the description in the `<file>` element is not mandatory. Example:
```
<configuration_files>
<file description="The Main config file">Path/To/Config/File.ini</file>
<file description="Another Config file">FolderX/GameEngine.ini</file>
<file>FolderZ/GameConfig.ini</file>
</configuration_files>
```
It defines the files directly editable from Panel even without access to the server files directly. This requires the EditConfigFiles extra module.

View file

@ -0,0 +1,147 @@
<?php
require_once(__DIR__ . '/../includes/admin_auth.php');
require_once(__DIR__ . '/../includes/config.inc.php');
include(__DIR__ . '/../includes/top.php');
include(__DIR__ . '/../includes/menu.php');
$sourceFile = __DIR__ . '/XML-Notes.md';
$markdown = file_exists($sourceFile) ? file_get_contents($sourceFile) : 'Source file missing.';
function billing_render_markdown($markdown)
{
$markdown = str_replace("\r\n", "\n", (string)$markdown);
$lines = explode("\n", $markdown);
$html = '';
$inCode = false;
$inList = false;
foreach ($lines as $line) {
$trim = trim($line);
if ($trim === '```') {
if ($inCode) {
$html .= "</code></pre>\n";
$inCode = false;
} else {
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$html .= "<pre class=\"code-block\"><code>";
$inCode = true;
}
continue;
}
if ($inCode) {
$html .= htmlspecialchars($line, ENT_QUOTES, 'UTF-8') . "\n";
continue;
}
if ($trim === '___') {
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$html .= "<hr>\n";
continue;
}
if (preg_match('/^(#{2,6})\s+(.*)$/', $line, $m)) {
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$level = strlen($m[1]);
$text = htmlspecialchars($m[2], ENT_QUOTES, 'UTF-8');
$html .= "<h{$level}>{$text}</h{$level}>\n";
continue;
}
if (strpos($trim, '- ') === 0) {
if (!$inList) {
$html .= "<ul>\n";
$inList = true;
}
$item = htmlspecialchars(substr($trim, 2), ENT_QUOTES, 'UTF-8');
$item = preg_replace('/`([^`]+)`/', '<code>$1</code>', $item);
$html .= "<li>{$item}</li>\n";
continue;
}
if ($trim === '') {
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$html .= "<p></p>\n";
continue;
}
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$lineHtml = htmlspecialchars($line, ENT_QUOTES, 'UTF-8');
$lineHtml = preg_replace('/`([^`]+)`/', '<code>$1</code>', $lineHtml);
$html .= "<p>{$lineHtml}</p>\n";
}
if ($inList) {
$html .= "</ul>\n";
}
if ($inCode) {
$html .= "</code></pre>\n";
}
return $html;
}
$docHtml = billing_render_markdown($markdown);
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>XML Configuration Notes</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../css/header.css">
<style>
.doc-wrapper {
max-width: 960px;
margin: 0 auto;
padding: 30px;
background: rgba(0,0,0,0.6);
border-radius: 8px;
line-height: 1.5;
}
.doc-wrapper h2, .doc-wrapper h3, .doc-wrapper h4 {
margin-top: 24px;
color: #fff;
}
.doc-wrapper p {
color: #e3e3e3;
}
.doc-wrapper code {
background: rgba(255,255,255,0.08);
padding: 2px 4px;
border-radius: 4px;
}
pre.code-block {
background: rgba(0,0,0,0.85);
color: #8ef0ff;
padding: 12px;
overflow-x: auto;
border-radius: 6px;
}
ul {
margin-left: 20px;
color: #e3e3e3;
}
</style>
</head>
<body>
<div class="container-wide panel">
<h1>Game Config XML Reference</h1>
<p>
This page mirrors the <a href="https://github.com/OpenGamePanel/OGP-Website/wiki/XML-Notes" target="_blank" rel="noopener noreferrer">
OGP XML Notes</a> so we can edit and review configuration expectations directly inside the repo.
Update <code>modules/billing/docs/XML-Notes.md</code> whenever the upstream wiki changes.
</p>
<div class="doc-wrapper">
<?php echo $docHtml; ?>
</div>
</div>
<?php include(__DIR__ . '/../includes/footer.php'); ?>
</body>
</html>

View file

@ -1,6 +1,6 @@
<?php
// Start a separate session for the website
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
// Include database configuration
@ -291,3 +291,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['request_reset'])) {
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>

View file

@ -1,9 +1,6 @@
<?php
// Admin authorization include — include early (before output) on admin pages
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_start();
}
require_once(__DIR__ . '/session_bridge.php');
// If not logged in, redirect to login
if (empty($_SESSION['website_user_id'])) {
@ -66,3 +63,5 @@ if (strtolower($role) !== 'admin') {
// If we reach here, user is an admin
?>

View file

@ -1,11 +1,9 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_start();
}
require_once(__DIR__ . '/session_bridge.php');
// Debugging mode: do not enforce login redirects. Pages can load without authentication.
// If you later want to re-enable, restore the original redirect behavior.
// (This file intentionally left as a no-op during debugging.)
return;
?>

View file

@ -4,11 +4,7 @@
* This file provides a consistent navigation menu across all website pages
*/
// Start the website session to check if user is logged in (if not already started)
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_start();
}
require_once(__DIR__ . '/session_bridge.php');
// Check login status
// Primary check uses website_user_id, but some remote deployments may only set website_username.
@ -122,3 +118,4 @@ if ($is_logged_in) {
</nav>
</div>
</div>

View file

@ -0,0 +1,97 @@
<?php
/**
* Panel bridge helpers for the storefront.
* Provides access to the native OGPDatabase layer, settings, and XML parsers
* without duplicating the panel bootstrap logic in each script.
*/
if (!function_exists('billing_panel_bootstrap')) {
/**
* Initialize the panel runtime and return shared context.
*
* @return array{db:OGPDatabase|null, settings:array, table_prefix:string}|null
*/
function billing_panel_bootstrap()
{
static $context = null;
if ($context !== null) {
return $context;
}
$root = realpath(__DIR__ . '/../../');
if ($root === false) {
error_log('billing_panel_bootstrap: unable to resolve project root');
return null;
}
// Define panel constants if they are not already defined (panel runtime does this for us).
if (!defined('INCLUDES')) {
define('INCLUDES', 'includes/');
}
if (!defined('MODULES')) {
define('MODULES', 'modules/');
}
// Load panel helpers that provisioning logic depends on.
require_once $root . '/includes/functions.php';
require_once $root . '/includes/helpers.php';
require_once $root . '/includes/lib_remote.php';
require_once $root . '/modules/config_games/server_config_parser.php';
// Load panel configuration (db credentials, prefix, etc.)
$configFile = $root . '/includes/config.inc.php';
if (!file_exists($configFile)) {
error_log('billing_panel_bootstrap: missing config file ' . $configFile);
return null;
}
require $configFile;
// Ensure required variables exist before attempting to connect.
if (!isset($db_type, $db_host, $db_user, $db_pass, $db_name, $table_prefix)) {
error_log('billing_panel_bootstrap: config variables not initialized');
return null;
}
$panelDb = createDatabaseConnection($db_type, $db_host, $db_user, $db_pass, $db_name, $table_prefix);
if (!($panelDb instanceof OGPDatabase)) {
error_log('billing_panel_bootstrap: failed to connect to panel database');
return null;
}
$settings = $panelDb->getSettings();
$context = [
'db' => $panelDb,
'settings' => is_array($settings) ? $settings : [],
'table_prefix' => $table_prefix,
];
return $context;
}
}
if (!function_exists('billing_get_panel_db')) {
/**
* Convenience wrapper to fetch the shared OGPDatabase handle.
*
* @return OGPDatabase|null
*/
function billing_get_panel_db()
{
$ctx = billing_panel_bootstrap();
return $ctx['db'] ?? null;
}
}
if (!function_exists('billing_get_panel_settings')) {
/**
* Convenience wrapper to fetch panel settings (time zone, steam creds, etc.).
*
* @return array
*/
function billing_get_panel_settings()
{
$ctx = billing_panel_bootstrap();
return $ctx['settings'] ?? [];
}
}

View file

@ -0,0 +1,32 @@
<?php
/**
* Session bridge to keep panel + storefront logins in sync.
* Always call this before rendering billing pages.
*/
if (session_status() === PHP_SESSION_NONE) {
session_name('opengamepanel_web');
session_start();
}
// If the panel session is populated, mirror into website-specific keys.
if (!empty($_SESSION['user_id']) && empty($_SESSION['website_user_id'])) {
$_SESSION['website_user_id'] = (int)$_SESSION['user_id'];
if (!empty($_SESSION['users_login'])) {
$_SESSION['website_username'] = $_SESSION['users_login'];
}
if (!empty($_SESSION['users_group'])) {
$_SESSION['website_user_role'] = $_SESSION['users_group'];
}
}
// If the website session is populated but the panel keys are missing, mirror back.
if (!empty($_SESSION['website_user_id']) && empty($_SESSION['user_id'])) {
$_SESSION['user_id'] = (int)$_SESSION['website_user_id'];
if (!empty($_SESSION['website_username'])) {
$_SESSION['users_login'] = $_SESSION['website_username'];
}
if (!empty($_SESSION['website_user_role'])) {
$_SESSION['users_group'] = $_SESSION['website_user_role'];
}
}

View file

@ -1,6 +1,6 @@
<?php
// Start a separate session for the website (not the panel session)
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
// Enable error display for debugging the white screen issue. Remove or gate in production.
ini_set('display_errors', 1);
@ -73,34 +73,62 @@ $success_message = '';
// Process login form submission: simplified for debugging
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
$username = trim($_POST['ulogin'] ?? '');
if ($username === '') {
$error_message = 'Please enter a username.';
site_log_warn('login_failed_empty_username', ['ip'=>$_SERVER['REMOTE_ADDR'] ?? '', 'script'=>$_SERVER['SCRIPT_NAME'] ?? '']);
$password = $_POST['upassword'] ?? '';
if ($username === '' || $password === '') {
$error_message = 'Please enter both a username and password.';
site_log_warn('login_failed_missing_fields', ['ip'=>$_SERVER['REMOTE_ADDR'] ?? '', 'script'=>$_SERVER['SCRIPT_NAME'] ?? '']);
} else {
// Normal operation: create website session (should be set after proper auth)
// In final mode, preserve username but do not fabricate IDs. The site should set website_user_id after proper registration/login.
$_SESSION['website_username'] = $username;
$_SESSION['website_login_time'] = time();
// Try to resolve an existing panel user_id by username so the menu and admin checks work.
$resolved_uid = null;
if ($db) {
$safe = mysqli_real_escape_string($db, $username);
$res = @mysqli_query($db, "SELECT user_id FROM {$table_prefix}users WHERE users_login = '$safe' LIMIT 1");
if ($res && mysqli_num_rows($res) === 1) {
$r = mysqli_fetch_assoc($res);
$resolved_uid = intval($r['user_id'] ?? 0);
$safe = mysqli_real_escape_string($db, $username);
$sql = "SELECT user_id, users_login, users_passwd, users_pass_hash, users_role, users_lang, users_theme FROM {$table_prefix}users WHERE users_login = '$safe' LIMIT 1";
$res = mysqli_query($db, $sql);
if ($res && mysqli_num_rows($res) === 1) {
$row = mysqli_fetch_assoc($res);
$userId = intval($row['user_id']);
$legacyHash = $row['users_passwd'] ?? '';
$modernHash = $row['users_pass_hash'] ?? '';
$authOk = false;
if (!empty($modernHash) && function_exists('password_verify')) {
$authOk = password_verify($password, $modernHash);
}
if (!$authOk && !empty($legacyHash)) {
$authOk = (md5($password) === $legacyHash);
if ($authOk && function_exists('password_hash')) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$escapedHash = mysqli_real_escape_string($db, $newHash);
mysqli_query($db, "UPDATE {$table_prefix}users SET users_pass_hash = '$escapedHash' WHERE user_id = $userId LIMIT 1");
}
}
if ($authOk) {
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
$_SESSION['users_login'] = $row['users_login'] ?? $username;
$_SESSION['users_passwd'] = $legacyHash;
$_SESSION['users_group'] = $row['users_role'] ?? 'user';
$_SESSION['users_lang'] = $row['users_lang'] ?? '';
$_SESSION['users_theme'] = $row['users_theme'] ?? '';
$_SESSION['website_user_id'] = $userId;
$_SESSION['website_username'] = $row['users_login'] ?? $username;
$_SESSION['website_user_role'] = $row['users_role'] ?? '';
$_SESSION['website_login_time'] = time();
require_once(__DIR__ . '/includes/panel_bridge.php');
$panelCtx = billing_panel_bootstrap();
if ($panelCtx && isset($panelCtx['db']) && $panelCtx['db'] instanceof OGPDatabase) {
$_SESSION['users_api_key'] = $panelCtx['db']->getApiToken($userId);
} else {
$_SESSION['users_api_key'] = $_SESSION['users_api_key'] ?? '';
}
site_log_info('login_success', ['username'=>$username, 'ip'=>$_SERVER['REMOTE_ADDR'] ?? '']);
$returnToParam = $_POST['return_to'] ?? '';
$destination = $sanitize_return_path($returnToParam);
if ($destination === '') {
$destination = $SITE_ROOT_PATH . '/index.php';
}
header('Location: ' . $destination);
exit();
}
}
if (!empty($resolved_uid)) {
$_SESSION['website_user_id'] = $resolved_uid;
} else {
// Fallback: assign a numeric session id so the menu treats the user as logged in during debugging
$_SESSION['website_user_id'] = time();
}
site_log_info('login_success', ['username'=>$username, 'ip'=>$_SERVER['REMOTE_ADDR'] ?? '']);
// Always redirect to index.php under site root
header('Location: ' . $SITE_ROOT_PATH . '/index.php');
exit();
$error_message = 'Invalid username or password.';
site_log_warn('login_failed_invalid_credentials', ['username'=>$username, 'ip'=>$_SERVER['REMOTE_ADDR'] ?? '']);
}
}
@ -310,3 +338,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>

View file

@ -1,6 +1,6 @@
<?php
// Start the website session
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
// Logger function
@ -30,3 +30,4 @@ $siteRoot = $pos !== false ? substr($script, 0, $pos + strlen('/_website')) : rt
header('Location: ' . $siteRoot . '/index.php');
exit();
?>

View file

@ -27,8 +27,15 @@ $module_title = "billing";
$module_version = "3.0";
$db_version = 1;
$module_required = FALSE;
// Navigation disabled - this is now a purely external module
$module_menus = array();
// Module description
$module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management.";
// Register module menus so panel can show links (user and admin views)
$module_menus = array(
array('subpage' => 'my_orders', 'name' => 'My Orders', 'group' => 'user'),
array('subpage' => 'provision_servers', 'name' => 'Provision Servers', 'group' => 'user'),
array('subpage' => 'admin_orders', 'name' => 'Manage Orders', 'group' => 'admin')
);
$install_queries = array();

View file

@ -9,7 +9,7 @@
<?php
// Start session to check login status
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
}
@ -392,3 +392,4 @@ $status_config = [
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>

View file

@ -1,5 +1,5 @@
<?php
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
require_once(__DIR__ . '/bootstrap.php');
@ -70,3 +70,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['username']) && !empt
</form>
</body>
</html>

View file

@ -4,7 +4,7 @@
// Require login and configuration
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
}
require_once(__DIR__ . '/bootstrap.php');
@ -277,3 +277,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_renewal'])) {
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>

View file

@ -1,6 +1,6 @@
<?php
// Start a separate session for the website
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
// Include database configuration
@ -301,3 +301,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['reset_password']) &&
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>

View file

@ -134,7 +134,7 @@ echo "</div>";
// Test 7: Test session functionality
echo "<div class='section'>";
echo "<h2>Test 7: Session Test</h2>";
session_name("gameservers_website");
session_name("opengamepanel_web");
session_start();
$_SESSION['test_key'] = 'test_value';
if (isset($_SESSION['test_key']) && $_SESSION['test_key'] === 'test_value') {
@ -155,3 +155,4 @@ echo "</div>";
echo "</body></html>";
?>

View file

@ -24,6 +24,205 @@
require_once("server_config_parser.php");
function config_games_normalize_path($path)
{
$clean = preg_replace('/[^A-Za-z0-9_\\[\\]\\/\\-]/', '', (string)$path);
return ltrim($clean, '/');
}
function config_games_print_editor_css()
{
static $printed = false;
if ($printed) {
return;
}
$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-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__path{font-size:0.85rem;color:#989898}
.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__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-actions{text-align:right;margin-top:16px}
.xml-node__actions{display:flex;gap:8px;align-items:center}
.xml-hint{font-size:0.85rem;color:#999;margin-top:4px}
</style>
CSS;
}
function config_games_render_node(SimpleXMLElement $node, array $ancestors, array &$counters, int $depth = 0)
{
$name = $node->getName();
$pathKey = implode('/', $ancestors) === '' ? $name : implode('/', $ancestors) . '/' . $name;
$counters[$pathKey] = ($counters[$pathKey] ?? 0) + 1;
$index = $counters[$pathKey];
$pathParts = array_merge($ancestors, ["{$name}[{$index}]"]);
$rawPath = implode('/', $pathParts);
$path = config_games_normalize_path($rawPath);
$hasChildren = count($node->children()) > 0;
$value = (string)$node;
$safeLabel = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
$safePath = htmlspecialchars($path, ENT_QUOTES, 'UTF-8');
$displayPath = htmlspecialchars(str_replace('[', '[', $rawPath), ENT_QUOTES, 'UTF-8');
$isScript = in_array(strtolower($name), ['pre_install','post_install','precmd','postcmd','cli_template']);
$html = "<div class='xml-node depth-{$depth}'>";
$html .= "<div class='xml-node__header'><div><div class='xml-node__title'>{$safeLabel}</div><div class='xml-node__path'>{$displayPath}</div></div>";
$html .= "<div class='xml-node__actions'><label>Action</label><select name=\"nodes[{$safePath}][action]\"><option value='keep'>Keep</option><option value='remove'>Remove</option></select></div></div>";
$html .= "<div class='xml-node__body'>";
$html .= "<input type='hidden' name=\"nodes[{$safePath}][path]\" value=\"{$safePath}\">";
$html .= "<input type='hidden' name=\"nodes[{$safePath}][has_children]\" value=\"" . ($hasChildren ? '1' : '0') . "\">";
if (!$hasChildren || $isScript) {
$safeValue = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
if ($isScript || strlen($value) > 120) {
$html .= "<label>Value</label><textarea name=\"nodes[{$safePath}][value]\">{$safeValue}</textarea>";
} else {
$html .= "<label>Value</label><input type='text' name=\"nodes[{$safePath}][value]\" value=\"{$safeValue}\">";
}
} elseif (trim($value) !== '') {
$safeValue = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
$html .= "<label>Inner Text</label><textarea name=\"nodes[{$safePath}][value]\">{$safeValue}</textarea>";
$html .= "<p class='xml-hint'>This element contains nested tags; clearing the text does not remove children.</p>";
}
$attributes = $node->attributes();
if ($attributes && count($attributes) > 0) {
$html .= "<div class='xml-node__attributes'><strong>Attributes</strong>";
foreach ($attributes as $attrName => $attrValue) {
$attrSafe = htmlspecialchars($attrName, ENT_QUOTES, 'UTF-8');
$valSafe = htmlspecialchars((string)$attrValue, ENT_QUOTES, 'UTF-8');
$html .= "<div class='attr-row'><span>{$attrSafe}</span><input type='text' name=\"nodes[{$safePath}][attributes][{$attrSafe}]\" value=\"{$valSafe}\" placeholder='Leave blank to remove'></div>";
}
$html .= "<div class='attr-row'><input type='text' name=\"nodes[{$safePath}][new_attribute][name]\" placeholder='New attribute name'><input type='text' name=\"nodes[{$safePath}][new_attribute][value]\" placeholder='New attribute value'></div>";
$html .= "</div>";
} else {
$html .= "<div class='xml-node__attributes'><div class='attr-row'><input type='text' name=\"nodes[{$safePath}][new_attribute][name]\" placeholder='Attribute name'><input type='text' name=\"nodes[{$safePath}][new_attribute][value]\" placeholder='Attribute value'></div></div>";
}
if ($hasChildren) {
$html .= "<div class='xml-children'>";
foreach ($node->children() as $child) {
$html .= config_games_render_node($child, array_merge($ancestors, ["{$name}[{$index}]"]), $counters, $depth + 1);
}
$html .= "</div>";
}
$html .= "</div></div>";
return $html;
}
function config_games_render_editor(SimpleXMLElement $xml)
{
config_games_print_editor_css();
$rootName = $xml->getName();
$html = "<div class='xml-editor-wrapper'>";
$counters = [];
foreach ($xml->children() as $child) {
$html .= config_games_render_node($child, [$rootName], $counters);
}
$html .= "</div>";
return $html;
}
function config_games_save_xml($db, $home_cfg_id, array $nodesPayload)
{
$cfg_info = $db->getGameCfg($home_cfg_id);
if ($cfg_info === FALSE) {
return false;
}
$config_file = SERVER_CONFIG_LOCATION . $cfg_info['home_cfg_file'];
if (!file_exists($config_file) || !is_readable($config_file)) {
return false;
}
$nodes = [];
foreach ($nodesPayload as $path => $data) {
$cleanPath = config_games_normalize_path($path);
if ($cleanPath === '') {
continue;
}
$nodes[$cleanPath] = $data;
}
if (empty($nodes)) {
return false;
}
$dom = new DOMDocument();
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
if (@$dom->load($config_file) === false) {
return false;
}
$xpath = new DOMXPath($dom);
uksort($nodes, function ($a, $b) {
return substr_count($b, '/') <=> substr_count($a, '/');
});
foreach ($nodes as $path => $nodeData) {
$query = '/' . $path;
$nodeList = @$xpath->query($query);
if (!$nodeList || $nodeList->length === 0) {
continue;
}
$domNode = $nodeList->item(0);
$action = $nodeData['action'] ?? 'keep';
if ($action === 'remove') {
if ($domNode->parentNode) {
$domNode->parentNode->removeChild($domNode);
}
continue;
}
$hasChildren = !empty($nodeData['has_children']);
if (array_key_exists('value', $nodeData)) {
while ($domNode->firstChild) {
$domNode->removeChild($domNode->firstChild);
}
if ($nodeData['value'] !== '') {
$domNode->appendChild($dom->createTextNode($nodeData['value']));
}
} elseif (!$hasChildren) {
while ($domNode->firstChild) {
$domNode->removeChild($domNode->firstChild);
}
}
if (isset($nodeData['attributes']) && is_array($nodeData['attributes'])) {
foreach ($nodeData['attributes'] as $attrName => $attrValue) {
$attrNameClean = preg_replace('/[^A-Za-z0-9_\\-:]/', '', (string)$attrName);
if ($attrNameClean === '') {
continue;
}
$attrValue = trim((string)$attrValue);
if ($attrValue === '') {
$domNode->removeAttribute($attrNameClean);
} else {
$domNode->setAttribute($attrNameClean, $attrValue);
}
}
}
if (isset($nodeData['new_attribute']['name']) && $nodeData['new_attribute']['name'] !== '') {
$newName = preg_replace('/[^A-Za-z0-9_\\-:]/', '', (string)$nodeData['new_attribute']['name']);
$newValue = (string)($nodeData['new_attribute']['value'] ?? '');
if ($newName !== '' && $newValue !== '') {
$domNode->setAttribute($newName, $newValue);
}
}
}
if ($dom->save($config_file) === false) {
return false;
}
$config = read_server_config($config_file);
if ($config !== FALSE) {
$db->addGameCfg($config);
}
return true;
}
function exec_ogp_module() {
global $db,$view;
@ -91,6 +290,17 @@ function exec_ogp_module() {
print_success(get_lang('configs_updated_ok'));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_xml']) && isset($_POST['home_cfg_id'])) {
$edit_id = (int)$_POST['home_cfg_id'];
$nodesPayload = isset($_POST['nodes']) && is_array($_POST['nodes']) ? $_POST['nodes'] : [];
if (config_games_save_xml($db, $edit_id, $nodesPayload)) {
print_success(get_lang('configs_updated_ok'));
} else {
print_failure('Failed to save XML configuration.');
}
$_GET['home_cfg_id'] = $edit_id;
}
$game_cfgs = $db->getGameCfgs();
echo "<table class='center'>\n
@ -165,31 +375,19 @@ function exec_ogp_module() {
{
echo "<a href='?m=config_games&home_cfg_id=".$home_cfg_id."&delete'>".get_lang_f('delete_game_config_for',$cfg_info['game_name']." $os $arch")."</a><br>";
$configs = read_server_config($config_file);
echo "<table>\n";
foreach( $configs as $key => $value )
{
echo "<tr><td><b>$key<b></td><td colspan=25 >$value</td></tr>\n";
foreach($value as $subkey => $subvalue )
{
echo "<tr><td><b>$subkey<b></td><td>$subvalue</td>\n";
list($attributes,$attrvalue)=each($subvalue);
foreach($attrvalue as $attrkey => $attrval)
{
echo "<td><b>$attrkey<b></td><td>$attrval</td>\n";
}
echo "</td>";
foreach($subvalue as $option => $options )
{
echo "<td><b>$option<b></td><td>$options</td>\n";
}
}
echo "</tr>\n";
$xml = @simplexml_load_file($config_file);
if ($xml === false) {
print_failure(get_lang_f("error_when_handling_file",$config_file));
} else {
echo "<form action='?m=config_games&amp;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' style='float:right;margin-bottom:10px;'>".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'>".get_lang('save')."</button></div>";
echo "</form>";
echo "<p class='note'>Use the action dropdown to remove entire sections. Attribute values left blank will be removed. Script sections such as post_install are fully editable.</p>";
}
echo "</table>\n";
}
}
}