feat: relocate billing runtime to module and harden updater panel pathing

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/50299e05-4ee0-4b5b-80e4-bc5f872c106e

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-18 13:46:11 +00:00 committed by GitHub
parent 651c935fa7
commit 176f532737
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
686 changed files with 92221 additions and 8198 deletions

View file

@ -0,0 +1,11 @@
<?php
function website_billing_runtime_file(string $relative): string
{
$target = realpath(__DIR__ . '/../Panel/modules/billing/' . ltrim($relative, '/'));
if ($target === false || strpos($target, realpath(__DIR__ . '/../Panel/modules/billing')) !== 0) {
http_response_code(500);
echo 'Billing runtime file not found.';
exit;
}
return $target;
}

View file

@ -1,468 +1,3 @@
<?php
// _website/add_to_cart.php
// Handle Add to Cart posts from order.php
require_once(__DIR__ . '/bootstrap.php');
require_once(__DIR__ . '/includes/login_required.php');
require_once(__DIR__ . '/includes/log.php');
// Variables from config.inc.php (helps IDEs understand scope)
/** @var string $db_host Database host */
/** @var string $db_user Database user */
/** @var string $db_pass Database password */
/** @var string $db_name Database name */
/** @var string $table_prefix Table prefix for database tables */
// Start session if not already
if (session_status() === PHP_SESSION_NONE) {
session_name('opengamepanel_web');
session_start();
}
function billing_generate_password(): string
{
$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$len = strlen($alphabet);
$password = '';
for ($i = 0; $i < 6; $i++) {
try {
$password .= $alphabet[random_int(0, $len - 1)];
} catch (Throwable $e) {
$password .= $alphabet[mt_rand(0, $len - 1)];
}
}
return $password;
}
function billing_normalize_duration(string $duration): array
{
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
}
function billing_money_to_cents(float $amount): int
{
return (int) round($amount * 100);
}
function billing_cents_to_money(int $cents): float
{
return $cents / 100;
}
function billing_rate_from_service(mysqli $db, string $table_prefix, int $service_id, string $rate_type): float
{
if ($service_id <= 0) {
return 0.0;
}
$stmt = $db->prepare("SELECT price_monthly FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
if (!$stmt) {
return 0.0;
}
$stmt->bind_param('i', $service_id);
$stmt->execute();
$stmt->bind_result($price_monthly);
$rate = 0.0;
if ($stmt->fetch()) {
$rate = floatval($price_monthly);
}
$stmt->close();
return $rate;
}
function billing_detect_service_os(string $cfgFile, string $gameKey): string
{
$haystack = strtolower(trim($cfgFile !== '' ? $cfgFile : $gameKey));
if ($haystack === '') {
return 'any';
}
if (preg_match('/(?:^|[_\\-])(win|windows)(?:[_\\-]|$)/i', $haystack)) {
return 'windows';
}
if (preg_match('/(?:^|[_\\-])linux(?:[_\\-]|$)/i', $haystack)) {
return 'linux';
}
return 'any';
}
function billing_normalize_node_os(string $serverOs): string
{
$value = strtolower(trim($serverOs));
if ($value === '' || $value === 'any') {
return 'any';
}
if (str_starts_with($value, 'win')) {
return 'windows';
}
if (str_starts_with($value, 'lin')) {
return 'linux';
}
return $value;
}
function billing_fail_add_to_cart(string $message, array $context = [], ?string $redirect = null): void
{
site_log_error('add_to_cart_failed', array_merge(['message' => $message], $context));
$target = $redirect ?? '/cart.php?error=add_to_cart';
header('Location: ' . $target);
exit;
}
// Immediate request tracing log (helps confirm the script is hit)
@mkdir(__DIR__ . '/logs', 0775, true);
$trace_file = __DIR__ . '/logs/add_to_cart_requests.log';
file_put_contents($trace_file, date('c') . " - REQUEST_METHOD=" . ($_SERVER['REQUEST_METHOD'] ?? '') . " URI=" . ($_SERVER['REQUEST_URI'] ?? '') . "\n", FILE_APPEND);
// Prefer website session id if set (login.php sets website_user_id in debug mode)
$user_id = 0;
if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) {
$user_id = intval($_SESSION['website_user_id']);
} elseif (isset($_SESSION['user_id']) && !empty($_SESSION['user_id'])) {
$user_id = intval($_SESSION['user_id']);
}
// If we don't have a numeric user_id but have a username, try to resolve it from the panel DB
if ($user_id <= 0 && isset($_SESSION['website_username']) && !empty($_SESSION['website_username'])) {
$uname = trim((string)$_SESSION['website_username']);
// attempt to lookup in DB (if connection available later we will set session after connecting)
// We'll set a temporary flag to resolve after DB connection is established below
$resolve_username_for_user_id = $uname;
} else {
$resolve_username_for_user_id = null;
}
/*
if ($user_id <= 0) {
// Not logged in - redirect to login with return
$return = urlencode('/' . trim(str_replace('\\', '/', $_SERVER['REQUEST_URI']), '/'));
header('Location: ' . (isset($SITE_BASE_URL) ? $SITE_BASE_URL : '') . '/_website/login.php?return_to=' . $return);
exit;
}*/
// Basic validation and normalization
$service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
$home_name = isset($_POST['home_name']) ? trim($_POST['home_name']) : '';
$ip_id = isset($_POST['ip_id']) ? intval($_POST['ip_id']) : 0;
$max_players = isset($_POST['max_players']) ? intval($_POST['max_players']) : 0;
$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1;
$invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month';
$display_service_id = isset($_POST['display_service_id']) ? intval($_POST['display_service_id']) : 0;
$display_rate = isset($_POST['display_rate']) ? floatval($_POST['display_rate']) : 0.0;
$posted_total = isset($_POST['calculated_total']) ? floatval($_POST['calculated_total']) : 0.0;
$remote_control_password = isset($_POST['remote_control_password']) ? trim((string)$_POST['remote_control_password']) : '';
$ftp_password = isset($_POST['ftp_password']) ? trim((string)$_POST['ftp_password']) : '';
// Price lookup: try to find service price_monthly
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$db) {
// Log connection error and return user to cart with a friendly error flag
@mkdir(__DIR__ . '/logs', 0775, true);
$trace = __DIR__ . '/logs/add_to_cart.log';
file_put_contents($trace, date('c') . " - mysqli_connect failed: " . mysqli_connect_error() . "\n", FILE_APPEND);
billing_fail_add_to_cart('DB connection failed');
} else {
mysqli_set_charset($db, 'utf8mb4');
// Log that config was loaded (mask password)
@mkdir(__DIR__ . '/logs', 0775, true);
$trace = __DIR__ . '/logs/add_to_cart.log';
$masked_pass = strlen($db_pass) ? '***' : '';
file_put_contents($trace, date('c') . " - DB connected host={$db_host} user={$db_user} pass={$masked_pass} db={$db_name}\n", FILE_APPEND);
}
// If we deferred resolving username to user_id, do it now with the DB connection
if (!empty($resolve_username_for_user_id) && $db) {
$safe_uname = mysqli_real_escape_string($db, $resolve_username_for_user_id);
// users_login is the correct column name in this schema
$q = mysqli_query($db, "SELECT user_id FROM {$table_prefix}users WHERE users_login = '$safe_uname' LIMIT 1");
if ($q && mysqli_num_rows($q) === 1) {
$r = mysqli_fetch_assoc($q);
$user_id = intval($r['user_id'] ?? 0);
// persist into session for subsequent requests
if ($user_id > 0) {
$_SESSION['website_user_id'] = $user_id;
site_log_info('resolved_user_id_from_username', ['username'=>$resolve_username_for_user_id,'user_id'=>$user_id]);
// Also resolve and persist the user's role so menus and admin checks are consistent
$role_q = mysqli_query($db, "SELECT users_role FROM {$table_prefix}users WHERE user_id = " . intval($user_id) . " LIMIT 1");
if ($role_q && mysqli_num_rows($role_q) === 1) {
$role_row = mysqli_fetch_assoc($role_q);
$_SESSION['website_user_role'] = $role_row['users_role'] ?? '';
}
}
} else {
site_log_warn('resolve_user_failed', ['username'=>$resolve_username_for_user_id]);
}
}
$service_name = '';
$base_rate = 0.0;
$slot_min_qty = 1;
$slot_max_qty = 1;
$service_home_cfg_id = 0;
$service_remote_server_csv = '';
$service_cfg_file = '';
$service_game_key = '';
$durationInfo = billing_normalize_duration($invoice_duration);
if ($service_id > 0) {
$stmt = $db->prepare("SELECT bs.service_name, bs.price_monthly, bs.slot_min_qty, bs.slot_max_qty, bs.home_cfg_id, bs.remote_server_id, ch.home_cfg_file, ch.game_key
FROM {$table_prefix}billing_services bs
LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id
WHERE bs.service_id = ? AND bs.enabled = 1
LIMIT 1");
if ($stmt) {
$stmt->bind_param('i', $service_id);
$stmt->execute();
$stmt->bind_result($service_name, $price_monthly, $slot_min_qty, $slot_max_qty, $service_home_cfg_id, $service_remote_server_csv, $service_cfg_file, $service_game_key);
if ($stmt->fetch()) {
$base_rate = floatval($price_monthly);
// constrain slots
if ($max_players < $slot_min_qty) $max_players = $slot_min_qty;
if ($max_players > $slot_max_qty) $max_players = $slot_max_qty;
}
$stmt->close();
}
}
if ($service_id <= 0 || $base_rate < 0) {
billing_fail_add_to_cart('Invalid service selection', ['service_id' => $service_id]);
}
if ($service_name === '') {
billing_fail_add_to_cart('Selected service is not available', ['service_id' => $service_id], '/serverlist.php');
}
if ($ip_id <= 0) {
billing_fail_add_to_cart('No location selected', ['service_id' => $service_id], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode('Please select a server location.'));
}
$allowedServerIds = [];
foreach (explode(',', (string)$service_remote_server_csv) as $part) {
$part = trim($part);
if ($part !== '' && ctype_digit($part)) {
$allowedServerIds[(int)$part] = true;
}
}
if (!isset($allowedServerIds[$ip_id])) {
billing_fail_add_to_cart('Selected location is not allowed for this service', [
'service_id' => $service_id,
'ip_id' => $ip_id,
'remote_server_csv' => $service_remote_server_csv,
], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode('Selected location is not available for this service.'));
}
$hasServerOsColumn = false;
$osColCheck = mysqli_query($db, "SHOW COLUMNS FROM {$table_prefix}remote_servers LIKE 'server_os'");
if ($osColCheck && mysqli_num_rows($osColCheck) > 0) {
$hasServerOsColumn = true;
}
if ($hasServerOsColumn) {
$rsQuery = mysqli_query($db, "SELECT remote_server_id, server_os FROM {$table_prefix}remote_servers WHERE remote_server_id = " . intval($ip_id) . " LIMIT 1");
if ($rsQuery && mysqli_num_rows($rsQuery) === 1) {
$rsRow = mysqli_fetch_assoc($rsQuery);
$serviceOs = billing_detect_service_os((string)$service_cfg_file, (string)$service_game_key);
$nodeOs = billing_normalize_node_os((string)($rsRow['server_os'] ?? 'any'));
if ($serviceOs !== 'any' && $nodeOs !== 'any' && $serviceOs !== $nodeOs) {
$message = $serviceOs === 'windows'
? 'This service requires a Windows server location.'
: 'This service requires a Linux server location.';
billing_fail_add_to_cart('Service and node OS mismatch', [
'service_id' => $service_id,
'home_cfg_id' => $service_home_cfg_id,
'cfg_file' => $service_cfg_file,
'node_os' => $nodeOs,
], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode($message));
}
} else {
billing_fail_add_to_cart('Selected remote server not found', ['service_id' => $service_id, 'ip_id' => $ip_id], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode('Selected server location no longer exists.'));
}
}
if ($base_rate <= 0 && $display_service_id > 0) {
$fallback_rate = billing_rate_from_service($db, $table_prefix, $display_service_id, $durationInfo['rate_type']);
if ($fallback_rate > 0) {
$base_rate = $fallback_rate;
}
}
if ($base_rate <= 0 && $display_rate > 0) {
$base_rate = $display_rate;
}
if ($remote_control_password === '' || strcasecmp($remote_control_password, 'ChangeMe') === 0) {
$remote_control_password = billing_generate_password();
}
if ($ftp_password === '' || strcasecmp($ftp_password, 'ChangeMe') === 0) {
$ftp_password = billing_generate_password();
}
// Insert into {table_prefix}billing_invoices (NOT orders - invoice created first)
$now = date('Y-m-d H:i:s');
$status = 'due'; // Invoice status: due (unpaid), paid
$payment_status = 'unpaid';
$qty = max(1, $qty);
$max_players = max(1, $max_players);
$rate_per_player_cents = max(0, billing_money_to_cents($base_rate));
$subtotal_cents = $rate_per_player_cents * $max_players * $qty;
$posted_total_cents = max(0, billing_money_to_cents($posted_total));
if ($subtotal_cents <= 0 && $posted_total_cents > 0 && $base_rate > 0) {
$subtotal_cents = $posted_total_cents;
}
$subtotal = billing_cents_to_money($subtotal_cents);
$amount = $subtotal;
$period_end = date('Y-m-d H:i:s', strtotime('+' . ($durationInfo['days'] * $qty) . ' days'));
// Normal flow: process POST immediately. If debug=1 is passed, we'll still log SQL and show results in logs.
$debug = (isset($_GET['debug']) && $_GET['debug'] == '1') || (isset($_POST['debug']) && $_POST['debug'] == '1');
// Build and execute the INSERT with prepared statements
@mkdir(__DIR__ . '/logs', 0775, true);
$logfile = __DIR__ . '/logs/add_to_cart.log';
site_log_info('add_to_cart_invoked', ['user_id'=>$user_id, 'service_id'=>$service_id]);
// Get customer name and email from {table_prefix}users
$customer_name = '';
$customer_email = '';
$user_q = mysqli_query($db, "SELECT users_fname, users_lname, users_email FROM {$table_prefix}users WHERE user_id = " . intval($user_id) . " LIMIT 1");
if ($user_q && mysqli_num_rows($user_q) === 1) {
$user_row = mysqli_fetch_assoc($user_q);
$customer_name = trim(($user_row['users_fname'] ?? '') . ' ' . ($user_row['users_lname'] ?? ''));
$customer_email = $user_row['users_email'] ?? '';
}
// Compute due_date = now + 3 days
$due_dt = new DateTime('now');
$due_dt->modify('+3 days');
$due_date = $due_dt->format('Y-m-d H:i:s');
// Escape values
$esc_user_id = intval($user_id);
$esc_service_id = intval($service_id);
$esc_ip_id = intval($ip_id);
$esc_max_players = intval($max_players);
$esc_qty = intval($qty);
$description = trim(($service_name !== '' ? $service_name : 'Game Server') . ': ' . $home_name);
$invoiceTable = $table_prefix . 'billing_invoices';
$invoiceColumns = [];
$columnsResult = mysqli_query($db, "SHOW COLUMNS FROM `{$invoiceTable}`");
if (!$columnsResult) {
billing_fail_add_to_cart('Could not inspect billing invoice schema', ['table' => $invoiceTable, 'error' => mysqli_error($db)]);
}
while ($col = mysqli_fetch_assoc($columnsResult)) {
$invoiceColumns[$col['Field']] = true;
}
mysqli_free_result($columnsResult);
$invoice_duration = $durationInfo['invoice_duration'];
$rate_type = $durationInfo['rate_type'];
$rowData = [
'order_id' => 0,
'user_id' => $esc_user_id,
'service_id' => $esc_service_id,
'home_id' => 0,
'home_name' => $home_name,
'ip' => $esc_ip_id,
'max_players' => $esc_max_players,
'remote_control_password' => $remote_control_password,
'ftp_password' => $ftp_password,
'customer_name' => $customer_name,
'customer_email' => $customer_email,
'amount' => $amount,
'discount_amount' => 0.00,
'currency' => 'USD',
'status' => $status,
'billing_status' => $status,
'invoice_date' => $now,
'due_date' => $due_date,
'description' => $description,
'invoice_duration' => $invoice_duration,
'rate_type' => $rate_type,
'rate_per_player' => (float)$base_rate,
'players' => $max_players,
'period_start' => $now,
'period_end' => $period_end,
'subtotal' => $subtotal,
'total_due' => $amount,
'payment_status' => $payment_status,
'qty' => $esc_qty,
'coupon_id' => 0,
];
$insertColumns = [];
$placeholders = [];
$bindTypes = '';
$bindValues = [];
foreach ($rowData as $column => $value) {
if (!isset($invoiceColumns[$column])) {
continue;
}
$insertColumns[] = "`{$column}`";
$placeholders[] = '?';
if (is_int($value)) {
$bindTypes .= 'i';
} elseif (is_float($value)) {
$bindTypes .= 'd';
} else {
$bindTypes .= 's';
}
$bindValues[] = $value;
}
if (empty($insertColumns)) {
billing_fail_add_to_cart('No compatible invoice columns were found for insert', ['table' => $invoiceTable]);
}
$sql = "INSERT INTO `{$invoiceTable}` (" . implode(', ', $insertColumns) . ")
VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $db->prepare($sql);
$res = false;
$err_no = 0;
$err = '';
if ($stmt) {
$stmt->bind_param($bindTypes, ...$bindValues);
$res = @$stmt->execute();
$err_no = mysqli_errno($db);
$err = mysqli_error($db);
} else {
$err_no = mysqli_errno($db);
$err = mysqli_error($db);
}
site_log_info('add_to_cart_invoice', [
'user_id' => $user_id,
'service_id' => $service_id,
'home_name' => $home_name,
'remote_server_id' => $ip_id,
'players' => $max_players,
'qty' => $qty,
'invoice_duration' => $invoice_duration,
'subtotal' => $subtotal,
'total_due' => $amount,
]);
file_put_contents($logfile, date('c') . " - Creating invoice (not order): status=due total_due={$amount}\n", FILE_APPEND);
if (!$res || $err_no > 0) {
site_log_error('mysqli_query_failed', ['errno'=>$err_no, 'error'=>$err, 'sql'=>$sql]);
file_put_contents($logfile, date('c') . " - ERROR: " . $err . " (errno: {$err_no})\n", FILE_APPEND);
// Log table existence check
$tbl_check = mysqli_query($db, "SHOW TABLES LIKE '{$table_prefix}billing_invoices'");
$tbl_exists = ($tbl_check && mysqli_num_rows($tbl_check) > 0) ? 'yes' : 'no';
site_log_warn('billing_invoices_exists', ['exists'=>$tbl_exists]);
file_put_contents($logfile, date('c') . " - Table exists check: {$tbl_exists}\n", FILE_APPEND);
billing_fail_add_to_cart('Invoice insert failed', ['errno' => $err_no, 'error' => $err]);
} else {
$insert_id = mysqli_insert_id($db);
$affected = mysqli_affected_rows($db);
site_log_info('add_to_cart_insert', ['invoice_id'=>$insert_id, 'affected_rows'=>$affected]);
file_put_contents($logfile, date('c') . " - Invoice created: invoice_id={$insert_id}\n", FILE_APPEND);
}
if ($stmt instanceof mysqli_stmt) {
$stmt->close();
}
// Redirect to cart page
header('Location: cart.php');
exit;
?>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('add_to_cart.php');

View file

@ -1,75 +1,3 @@
<?php
// Admin landing page
require_once(__DIR__ . '/includes/admin_auth.php');
require_once(__DIR__ . '/includes/config_loader.php');
// config_loader.php now always loads billing/includes/config.inc.php first (which contains
// SITE_BASE_URL, SITE_DATA_DIR, PayPal settings, etc.) and then overlays panel DB settings
// when inside a GSP panel tree. Safe defaults are applied by the loader for any missing vars.
include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php');
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Admin Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/header.css">
</head>
<body>
<div class="container-wide panel">
<h1>Admin Dashboard</h1>
<p>Welcome to the admin area. From here you can manage servers, payments, and site settings.</p>
<div class="admin-flex-wrap">
<a class="gsw-btn" href="adminserverlist.php">Manage Servers &amp; Services</a>
<a class="gsw-btn" href="admin_invoices.php">Manage Invoices</a>
<a class="gsw-btn" href="admin_payments.php">Transaction Log</a>
<a class="gsw-btn" href="admin_coupons.php">Manage Coupons</a>
<a class="gsw-btn" href="admin_config.php">Edit Site Config</a>
</div>
<hr>
<h3>Quick usage notes</h3>
<ul>
<li>The <strong>Manage Servers & Services</strong> page allows enabling/disabling nodes and editing service rows.</li>
<li>The <strong>Invoice History</strong> page reads JSON payment records from <code><?php echo h($SITE_DATA_DIR); ?></code>.</li>
<li>The <strong>Edit Site Config</strong> page edits <code>_website/includes/config.inc.php</code>. Edits create a timestamped backup before saving.</li>
</ul>
<h3>Sandbox account (testing)</h3>
<p>Use PayPal sandbox credentials when testing payments. Set your sandbox <code>client_id</code> and <code>client_secret</code> in <code>modules/billing/includes/config.inc.php</code> (the <code>$paypal_client_id</code> and <code>$paypal_client_secret</code> variables). Set <code>$paypal_sandbox = false</code> for live payments.</p>
<ul>
<li>Create a sandbox business account at <a href="https://developer.paypal.com">PayPal Developer</a> and obtain a sandbox client ID/secret.</li>
<li>Update the payment handler config and restart the webserver if required.</li>
<li>Run a checkout using the PayPal JS button on the checkout page after payment completes, the webhook will record a JSON file into <code><?php echo h($SITE_DATA_DIR); ?></code>.</li>
<li>If you need to simulate a webhook locally, drop a JSON file with the same schema into the <code>data/</code> folder (we added a sample: <code>SIMULATED-WEBHOOK-*.json</code>).</li>
</ul>
<h3>Payments: high-level program flow</h3>
<ol>
<li>User adds an item and proceeds to checkout (<code>_website/cart.php</code>).</li>
<li>The checkout page renders the PayPal JS SDK and calls server-side endpoints (create_order/capture_order).</li>
<li>After a successful capture, PayPal sends a webhook event to <code>_website/webhook.php</code> (or the equivalent handler under <code>_website/api/</code>).</li>
<li>The webhook verifies the signature, fetches any missing order details, and writes a JSON record to the <code>data/</code> directory (this powers <code>invoices.php</code> and <code>return.php</code>).</li>
<li>On successful payment we mark the order as PAID in the JSON and the site UI (invoices/returns) reads those JSONs to render receipts.</li>
<li>Admin pages can view invoices at <code>./invoices.php</code> and reconcile or trigger further provisioning via internal panel APIs.</li>
</ol>
<h3>Environment</h3>
<table class="cart-table">
<tr><th>Site Base URL</th><td><?php echo h($SITE_BASE_URL ?: '(empty — using relative paths)'); ?></td></tr>
<tr><th>Data directory</th><td><?php echo h($SITE_DATA_DIR); ?></td></tr>
<tr><th>PHP SAPI</th><td><?php echo h(PHP_SAPI); ?></td></tr>
<tr><th>Writable?</th><td><?php echo is_writable(__DIR__ . '/data') ? 'yes' : 'no'; ?></td></tr>
</table>
</div>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</body>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('admin.php');

View file

@ -1,718 +1,3 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Service Configuration - GSP</title>
<style>
.svc-table { border-collapse: collapse; width: 100%; }
.svc-table th, .svc-table td { border: 1px solid rgba(86,105,130,0.6); padding: 8px 10px; vertical-align: middle; }
/* Sticky header: stays visible while scrolling; dark background with light text for readability */
.svc-table thead th { position: sticky; top: 0; z-index: 10; background: #26354a; color: #f0f0f0; white-space: nowrap; text-align: center; }
.svc-table thead th.game-name { text-align: left; }
.svc-table td.game-name { text-align: left; white-space: nowrap; }
.price-input { width: 80px; }
.slot-input { width: 60px; }
.desc-input { width: 160px; }
.img-input { width: 160px; }
.img-select { max-width: 180px; }
.img-fallback { display: none; max-width: 180px; margin-top: 4px; }
.img-fallback.img-fallback-visible { display: block; }
.muted { color: #999; font-size: 0.85em; }
.flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #155724; }
.flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #721c24; }
.servers-cell { text-align: left; min-width: 160px; max-width: 220px; width: 220px; }
.server-cb-label { display: block; white-space: normal; margin: 2px 0; }
.action-cell { text-align: center; min-width: 120px; }
.btn-row-save, .btn-save-all {
border: 1px solid #3e7ab8;
border-radius: 6px;
background: #2f6dac;
color: #fff;
font-weight: 600;
padding: 6px 10px;
cursor: pointer;
}
.btn-save-all {
padding: 9px 14px;
font-size: 0.95rem;
}
.btn-row-save:hover, .btn-save-all:hover { background: #25598d; }
.sort-link { color: #d8e7ff; text-decoration: none; display: inline-flex; align-items: center; gap: 4px; }
.sort-link:hover { text-decoration: underline; }
.sort-active { color: #ffffff; font-weight: 700; }
</style>
</head>
<body>
<?php
/**
* Admin service configuration page.
*
* On every load this page syncs gsp_billing_services with the panel's game
* config list (config_homes). One billing_services row is maintained per
* config_homes entry; the row is keyed by home_cfg_id. config_mods is NOT
* used as the identity source mods are install-time details that belong in
* the game config tables, not here.
*
* remote_server_id in gsp_billing_services stores a comma-separated list of
* numeric remote server IDs, e.g. "1,3,7". The deprecated
* gsp_billing_service_remote_servers mapping table is never referenced here.
*
* Columns synced from config_homes (read-only in the UI):
* service_name game_name
* description game_name (default; admin may override via separate edit)
* home_cfg_id home_cfg_id (sync key)
*
* Columns that are admin-editable and NEVER overwritten by sync:
* enabled, slot_min_qty, slot_max_qty,
* price_monthly,
* remote_server_id, description, img_url
*/
require_once(__DIR__ . '/bootstrap.php');
require_once(__DIR__ . '/includes/admin_auth.php');
if (session_status() === PHP_SESSION_NONE) {
session_name('opengamepanel_web');
session_start();
}
function h(mixed $s): string
{
return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}
/**
* Return a sorted list of image filenames available in /images/games/.
* Only files with recognised image extensions are included.
*/
function list_game_images(): array
{
$dir = __DIR__ . '/../../images/games';
if (!is_dir($dir)) {
return [];
}
$exts = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
$files = [];
foreach (scandir($dir) as $f) {
if ($f === '.' || $f === '..') continue;
$ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
if (in_array($ext, $exts, true)) {
$files[] = $f;
}
}
natcasesort($files);
return array_values($files);
}
/**
* Normalize a game name or filename stem so that platform/architecture
* suffixes are stripped before comparison.
*
* Examples:
* "7 Days to Die linux64" "7daystodie"
* "arma3_win64" "arma3"
* "dayz_epoch_mod_win32" "dayzepochmod"
*/
function normalize_game_name(string $name): string
{
$name = strtolower($name);
// Strip extension if present
$name = preg_replace('/\.[a-z]{2,4}$/', '', $name);
// Strip common platform/arch suffixes (as whole words or underscore-delimited tokens)
$name = preg_replace('/[\s_\-]*(linux64|linux32|linux|win64|win32|windows|win|x64|x86|32|64)/', '', $name);
// Remove punctuation, spaces and underscores
$name = preg_replace('/[^a-z0-9]/', '', $name);
return $name;
}
/**
* Given a game name (from config_homes.game_name or home_cfg_file), try to find
* a matching image filename from the list of available game images.
* Returns the filename (e.g. "arma_3.jpg") or '' if nothing suitable is found.
*/
function guess_game_image(string $gameName, string $cfgFile, array $availableImages): string
{
if (empty($availableImages)) {
return '';
}
// Build a normalised→filename map for available images
$normMap = [];
foreach ($availableImages as $imgFile) {
$stem = pathinfo($imgFile, PATHINFO_FILENAME);
$key = normalize_game_name($stem);
if ($key !== '') {
// Keep the first match for duplicate normalised keys
$normMap[$key] = $normMap[$key] ?? $imgFile;
}
}
// Candidates to try, in priority order: game display name, then cfg file stem
$candidates = [$gameName];
if ($cfgFile !== '') {
$candidates[] = pathinfo($cfgFile, PATHINFO_FILENAME);
}
foreach ($candidates as $candidate) {
$key = normalize_game_name($candidate);
if ($key !== '' && isset($normMap[$key])) {
return $normMap[$key];
}
// Also try prefix matching: game "dayz epoch" → find "dayz_epochmod"
foreach ($normMap as $normImgKey => $imgFile) {
if (str_starts_with($normImgKey, $key) || str_starts_with($key, $normImgKey)) {
return $imgFile;
}
}
}
return '';
}
$db = billing_get_db();
if (!($db instanceof mysqli)) {
die("Database connection failed.");
}
include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php');
/* -----------------------------------------------------------------------
Auto-sync: keep billing_services in step with config_homes
Source: one row per config_homes entry, keyed by home_cfg_id.
Runs on every page load; INSERT and soft-disable only never hard-delete.
----------------------------------------------------------------------- */
function sync_billing_services(mysqli $db, string $prefix): array
{
$messages = [];
$tableName = $prefix . 'billing_services';
// Schema auto-repair: ensure all expected columns exist.
// col_exists() is provided by bootstrap.php.
$autoRepairCols = [
'home_cfg_id' => "ADD COLUMN `home_cfg_id` INT(11) NOT NULL DEFAULT 0",
'description' => "ADD COLUMN `description` VARCHAR(1000) NOT NULL DEFAULT ''",
'img_url' => "ADD COLUMN `img_url` VARCHAR(255) NOT NULL DEFAULT ''",
'slot_min_qty' => "ADD COLUMN `slot_min_qty` INT(11) NOT NULL DEFAULT 1",
'slot_max_qty' => "ADD COLUMN `slot_max_qty` INT(11) NOT NULL DEFAULT 100",
'price_daily' => "ADD COLUMN `price_daily` FLOAT(15,4) NOT NULL DEFAULT 0",
'price_monthly' => "ADD COLUMN `price_monthly` FLOAT(15,4) NOT NULL DEFAULT 0",
'price_year' => "ADD COLUMN `price_year` FLOAT(15,4) NOT NULL DEFAULT 0",
'remote_server_id' => "ADD COLUMN `remote_server_id` VARCHAR(255) NOT NULL DEFAULT ''",
];
foreach ($autoRepairCols as $col => $alterFragment) {
if (!col_exists($db, $tableName, $col)) {
if ($db->query("ALTER TABLE `{$tableName}` {$alterFragment}")) {
$messages[] = "✔ Auto-repaired: added column '{$col}' to {$tableName}.";
} else {
$messages[] = "✖ Could not add column '{$col}' to {$tableName}: " . $db->error;
}
}
}
// If critical columns are still absent after repair, abort to avoid SQL errors.
foreach (['service_name', 'home_cfg_id', 'enabled'] as $critical) {
if (!col_exists($db, $tableName, $critical)) {
$messages[] = "⚠ Critical column '{$critical}' missing from {$tableName}; skipping sync.";
return $messages;
}
}
// Load all game configs from config_homes — one entry per game XML.
$configHomes = [];
$res = $db->query(
"SELECT home_cfg_id, game_name, home_cfg_file
FROM `{$prefix}config_homes`
ORDER BY game_name"
);
if ($res) {
while ($row = $res->fetch_assoc()) {
$configHomes[(int)$row['home_cfg_id']] = $row;
}
}
if (empty($configHomes)) {
// config_homes is empty or the table does not exist yet — nothing to sync.
return $messages;
}
// Load existing billing_services indexed by home_cfg_id.
$existing = [];
$svcRes = $db->query(
"SELECT service_id, home_cfg_id, enabled
FROM `{$tableName}`"
);
if ($svcRes) {
while ($row = $svcRes->fetch_assoc()) {
$hid = (int)$row['home_cfg_id'];
if ($hid > 0) {
$existing[$hid] = $row;
}
}
}
// Insert a new row for every config_homes entry not yet in billing_services.
// Admin-editable fields (prices, slots, enabled, etc.) get safe defaults so
// the service is visible to the admin but not yet live in the store.
$availableImages = list_game_images();
foreach ($configHomes as $homeCfgId => $ch) {
if (isset($existing[$homeCfgId])) {
continue;
}
$svcName = $db->real_escape_string($ch['game_name']);
$guessedImg = $db->real_escape_string(
guess_game_image((string)$ch['game_name'], (string)($ch['home_cfg_file'] ?? ''), $availableImages)
);
$db->query(
"INSERT INTO `{$tableName}`
(home_cfg_id, mod_cfg_id, service_name, description,
remote_server_id, enabled,
price_daily, price_monthly, price_year,
slot_min_qty, slot_max_qty,
img_url, ftp, install_method, manual_url, access_rights)
VALUES
({$homeCfgId}, 0, '{$svcName}', '{$svcName}',
'', 0,
0.00, 0.00, 0.00,
1, 100,
'{$guessedImg}', '', 'steamcmd', '', '')"
);
$msg = "Added new service: " . $ch['game_name'];
if ($guessedImg !== '') {
$msg .= " (image auto-set: {$guessedImg})";
}
$messages[] = $msg;
}
// Soft-disable billing_services whose home_cfg_id no longer appears in config_homes.
foreach ($existing as $homeCfgId => $svcRow) {
if (!isset($configHomes[$homeCfgId])) {
$sid = (int)$svcRow['service_id'];
$db->query(
"UPDATE `{$tableName}`
SET enabled = 0
WHERE service_id = {$sid} AND enabled = 1"
);
if ($db->affected_rows > 0) {
$messages[] = "Service ID {$sid} disabled — game config no longer in config_homes.";
}
}
}
return $messages;
}
$syncMessages = sync_billing_services($db, $table_prefix);
$flash = [];
$flashType = 'ok';
$sort = strtolower((string)($_GET['sort'] ?? $_POST['sort'] ?? 'game'));
$dir = strtolower((string)($_GET['dir'] ?? $_POST['dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';
$gameMode = strtolower((string)($_GET['game_mode'] ?? $_POST['game_mode'] ?? 'name'));
if (!in_array($sort, ['game', 'config', 'enabled', 'month', 'servers'], true)) {
$sort = 'game';
}
if (!in_array($gameMode, ['name', 'enabled'], true)) {
$gameMode = 'name';
}
$sortQuery = http_build_query([
'sort' => $sort,
'dir' => $dir,
'game_mode' => $gameMode,
]);
function sort_link_params(string $column, string $sort, string $dir, string $gameMode): array
{
$nextDir = ($sort === $column && $dir === 'asc') ? 'desc' : 'asc';
$nextGameMode = $gameMode;
if ($column === 'game' && $sort === 'game' && $gameMode === 'name') {
$nextGameMode = 'enabled';
$nextDir = 'asc';
} elseif ($column === 'game' && $sort === 'game' && $gameMode === 'enabled') {
$nextGameMode = 'name';
$nextDir = 'asc';
} elseif ($column !== 'game') {
$nextGameMode = 'name';
}
return [
'sort' => $column,
'dir' => $nextDir,
'game_mode' => $nextGameMode,
];
}
/* -----------------------------------------------------------------------
SAVE: service configuration form submitted
Only admin-editable fields are updated; service_name and home_cfg_id
are never overwritten here.
----------------------------------------------------------------------- */
if (isset($_POST['save_services']) || isset($_POST['save_row'])) {
// Load valid remote server IDs for validation
$validServerIds = [];
$rsRes = $db->query("SELECT remote_server_id FROM `{$table_prefix}remote_servers`");
while ($rsRes && ($rsRow = $rsRes->fetch_assoc())) {
$validServerIds[] = (int)$rsRow['remote_server_id'];
}
$validSet = array_flip($validServerIds);
$postedServices = $_POST['svc'] ?? [];
$postedServers = $_POST['servers'] ?? [];
$rowOnlyServiceId = isset($_POST['save_row']) ? (int)$_POST['save_row'] : 0;
$updatedCount = 0;
foreach ((array)$postedServices as $sid => $svcData) {
$sid = (int)$sid;
if ($rowOnlyServiceId > 0 && $sid !== $rowOnlyServiceId) {
continue;
}
$enabled = isset($svcData['enabled']) ? 1 : 0;
$priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 2, '.', '');
$slotMin = max(1, (int)($svcData['slot_min_qty'] ?? 1));
$slotMax = max(1, (int)($svcData['slot_max_qty'] ?? 1));
if ($slotMax < $slotMin) { $slotMax = $slotMin; }
$description = $db->real_escape_string(substr((string)($svcData['description'] ?? ''), 0, 1000));
// Merge dropdown and fallback text input:
// - dropdown value "__other__" means use the text fallback field
// - otherwise use the dropdown value (bare filename or '')
$rawImgUrl = (string)($svcData['img_url'] ?? '');
if ($rawImgUrl === '__other__') {
$rawImgUrl = (string)($svcData['img_url_other'] ?? '');
}
$imgUrl = $db->real_escape_string(substr($rawImgUrl, 0, 255));
// Build comma-separated remote_server_id from checkboxes, validating each ID
$checkedIds = [];
foreach ((array)($postedServers[$sid] ?? []) as $rawId) {
$rid = (int)$rawId;
if (isset($validSet[$rid])) {
$checkedIds[] = $rid;
}
}
$remoteServerIdStr = $db->real_escape_string(implode(',', $checkedIds));
$ok = $db->query(
"UPDATE `{$table_prefix}billing_services`
SET enabled = {$enabled},
price_monthly = '{$priceMonthly}',
slot_min_qty = {$slotMin},
slot_max_qty = {$slotMax},
description = '{$description}',
img_url = '{$imgUrl}',
remote_server_id = '{$remoteServerIdStr}'
WHERE service_id = {$sid}"
);
if ($ok) {
$updatedCount++;
}
}
if ($updatedCount > 0) {
if ($rowOnlyServiceId > 0) {
$flash[] = "Service row #{$rowOnlyServiceId} saved.";
} else {
$flash[] = "{$updatedCount} service row(s) saved.";
}
} else {
$flashType = 'err';
if ($rowOnlyServiceId > 0) {
$flash[] = "No changes were saved for service row #{$rowOnlyServiceId}.";
} else {
$flash[] = "No service rows were updated.";
}
}
$_SESSION['billing_adminserverlist_flash'] = ['type' => $flashType, 'messages' => $flash];
header("Location: /adminserverlist.php?{$sortQuery}");
exit;
}
if (!empty($_SESSION['billing_adminserverlist_flash'])) {
$flashData = $_SESSION['billing_adminserverlist_flash'];
unset($_SESSION['billing_adminserverlist_flash']);
$flashType = ($flashData['type'] ?? 'ok') === 'err' ? 'err' : 'ok';
$flash = array_values(array_filter((array)($flashData['messages'] ?? []), 'is_string'));
}
/* -----------------------------------------------------------------------
Load data for display join config_homes to show the config XML filename
----------------------------------------------------------------------- */
$remoteServers = [];
$rsRes = $db->query(
"SELECT remote_server_id, remote_server_name
FROM `{$table_prefix}remote_servers`
ORDER BY remote_server_name"
);
while ($rsRes && ($row = $rsRes->fetch_assoc())) {
$remoteServers[] = $row;
}
$services = [];
$svcRes = $db->query(
"SELECT bs.service_id, bs.service_name, bs.enabled,
bs.price_monthly,
bs.slot_min_qty, bs.slot_max_qty,
bs.remote_server_id, bs.description, bs.img_url,
ch.home_cfg_file
FROM `{$table_prefix}billing_services` bs
LEFT JOIN `{$table_prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id
ORDER BY bs.service_name"
);
while ($svcRes && ($row = $svcRes->fetch_assoc())) {
$services[] = $row;
}
if (!empty($services)) {
usort($services, function (array $a, array $b) use ($sort, $dir, $gameMode): int {
$cmp = 0;
switch ($sort) {
case 'config':
$cmp = strcasecmp((string)($a['home_cfg_file'] ?? ''), (string)($b['home_cfg_file'] ?? ''));
break;
case 'enabled':
$cmp = ((int)($a['enabled'] ?? 0)) <=> ((int)($b['enabled'] ?? 0));
break;
case 'month':
$cmp = ((float)($a['price_monthly'] ?? 0)) <=> ((float)($b['price_monthly'] ?? 0));
break;
case 'servers':
$countA = trim((string)($a['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$a['remote_server_id']), 'strlen'));
$countB = trim((string)($b['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$b['remote_server_id']), 'strlen'));
$cmp = $countA <=> $countB;
break;
case 'game':
default:
if ($gameMode === 'enabled') {
$cmp = ((int)($b['enabled'] ?? 0)) <=> ((int)($a['enabled'] ?? 0));
if ($cmp === 0) {
$cmp = strcasecmp((string)($a['service_name'] ?? ''), (string)($b['service_name'] ?? ''));
}
} else {
$cmp = strcasecmp((string)($a['service_name'] ?? ''), (string)($b['service_name'] ?? ''));
}
break;
}
if ($cmp === 0) {
$cmp = ((int)($a['service_id'] ?? 0)) <=> ((int)($b['service_id'] ?? 0));
}
return $dir === 'desc' ? -$cmp : $cmp;
});
}
?>
<?php foreach (array_merge((array)$syncMessages, (array)$flash) as $msg): ?>
<div class="flash-<?php echo $flashType; ?>"><?php echo h($msg); ?></div>
<?php endforeach; ?>
<h2>Service Configuration</h2>
<p class="muted">
Enable services, configure pricing and slot ranges, and select which remote servers
each game can be installed on. The service list is automatically kept in sync with
the panel game configuration (<code>config_homes</code>). Check one or more servers
to make a game available for purchase; leaving all servers unchecked prevents the
game from appearing in the store.
</p>
<?php if (empty($services)): ?>
<p>No billing services found. Ensure game configs are loaded in the panel (Home &rarr; Games configuration).</p>
<?php else: ?>
<form method="post" action="">
<input type="hidden" name="save_services" value="1">
<input type="hidden" name="sort" value="<?php echo h($sort); ?>">
<input type="hidden" name="dir" value="<?php echo h($dir); ?>">
<input type="hidden" name="game_mode" value="<?php echo h($gameMode); ?>">
<div style="overflow-x:auto;">
<table class="svc-table">
<thead>
<tr>
<th class="game-name">
<?php $p = sort_link_params('game', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'game' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Game Name</a>
</th>
<th>
<?php $p = sort_link_params('config', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'config' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Config XML</a>
</th>
<th>
<?php $p = sort_link_params('enabled', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'enabled' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Enabled</a>
</th>
<th>Min Slots</th>
<th>Max Slots</th>
<th>
<?php $p = sort_link_params('month', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'month' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Month ($)</a>
</th>
<th>Description</th>
<th>Image</th>
<th>
<?php $p = sort_link_params('servers', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'servers' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Available Servers</a>
</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php
$gameImageFiles = list_game_images();
foreach ((array)$services as $svc):
$sid = (int)$svc['service_id'];
$svcEnabled = (int)$svc['enabled'];
$cfgFile = (string)($svc['home_cfg_file'] ?? '');
// Parse existing remote_server_id CSV into a set for fast checkbox lookup
$savedIds = [];
foreach (explode(',', (string)$svc['remote_server_id']) as $part) {
$part = trim($part);
if ($part !== '' && ctype_digit($part)) {
$savedIds[(int)$part] = true;
}
}
?>
<tr>
<td class="game-name">
<?php echo h($svc['service_name']); ?>
<div class="muted">ID: <?php echo $sid; ?></div>
</td>
<td class="muted">
<?php echo $cfgFile !== '' ? h($cfgFile) : '<em>—</em>'; ?>
</td>
<td style="text-align:center;">
<input type="hidden" name="svc[<?php echo $sid; ?>][enabled]" value="0">
<input type="checkbox" name="svc[<?php echo $sid; ?>][enabled]" value="1"
<?php echo $svcEnabled ? 'checked' : ''; ?>>
</td>
<td>
<input type="number" min="1" class="slot-input"
name="svc[<?php echo $sid; ?>][slot_min_qty]"
value="<?php echo (int)$svc['slot_min_qty']; ?>">
</td>
<td>
<input type="number" min="1" class="slot-input"
name="svc[<?php echo $sid; ?>][slot_max_qty]"
value="<?php echo (int)$svc['slot_max_qty']; ?>">
</td>
<td>
<input type="number" step="0.01" min="0" class="price-input"
name="svc[<?php echo $sid; ?>][price_monthly]"
value="<?php echo h(number_format((float)$svc['price_monthly'], 2, '.', '')); ?>">
</td>
<td>
<input type="text" class="desc-input"
name="svc[<?php echo $sid; ?>][description]"
value="<?php echo h($svc['description']); ?>">
</td>
<td>
<?php
// Determine whether saved value is a bare filename (in /images/games/),
// a full external URL, or empty.
$savedImg = (string)($svc['img_url'] ?? '');
$isExternal = (str_starts_with($savedImg, 'http://') || str_starts_with($savedImg, 'https://'));
$inDropdown = !$isExternal && in_array(basename($savedImg), $gameImageFiles, true);
// Value to pre-select in the dropdown: use bare filename, or '' if external/missing
$dropdownVal = (!$isExternal && $savedImg !== '') ? basename($savedImg) : '';
?>
<select name="svc[<?php echo $sid; ?>][img_url]"
class="img-select"
data-fallback-id="imgfb_<?php echo $sid; ?>">
<option value=""> none </option>
<?php foreach ($gameImageFiles as $imgFile): ?>
<option value="<?php echo h($imgFile); ?>"
<?php echo ($dropdownVal === $imgFile) ? 'selected' : ''; ?>>
<?php echo h($imgFile); ?>
</option>
<?php endforeach; ?>
<option value="__other__" <?php echo ($isExternal || (!$inDropdown && $savedImg !== '')) ? 'selected' : ''; ?>>
other / full URL
</option>
</select>
<?php
// Show fallback text input when the saved value is external or not in dropdown
$fbClass = ($isExternal || (!$inDropdown && $savedImg !== '')) ? 'img-fallback img-fallback-visible' : 'img-fallback';
$fbValue = ($isExternal || (!$inDropdown && $savedImg !== '')) ? $savedImg : '';
?>
<input type="text"
id="imgfb_<?php echo $sid; ?>"
class="<?php echo $fbClass; ?>"
name="svc[<?php echo $sid; ?>][img_url_other]"
placeholder="Full URL or filename"
value="<?php echo h($fbValue); ?>">
</td>
<td class="servers-cell">
<?php if (empty($remoteServers)): ?>
<span class="muted">No remote servers configured</span>
<?php else: ?>
<?php foreach ((array)$remoteServers as $rs):
$rid = (int)$rs['remote_server_id'];
$checked = isset($savedIds[$rid]) ? 'checked' : '';
?>
<label class="server-cb-label">
<input type="checkbox"
name="servers[<?php echo $sid; ?>][]"
value="<?php echo $rid; ?>"
<?php echo $checked; ?>>
<?php echo h($rs['remote_server_name']); ?>
<span class="muted">(#<?php echo $rid; ?>)</span>
</label>
<?php endforeach; ?>
<?php endif; ?>
</td>
<td class="action-cell">
<button type="submit" class="btn-row-save" name="save_row" value="<?php echo $sid; ?>">Save Row</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div style="margin-top:14px;">
<button type="submit" class="btn-save-all">Save All Services</button>
</div>
</form>
<?php endif; ?>
<div style="margin-top:20px;" class="panel">
<p><strong>Notes:</strong></p>
<ul>
<li>A service will only appear in the store when <strong>Enabled</strong> is checked
<em>and</em> at least one server is selected.</li>
<li><strong>Price / Month ($)</strong> is the canonical billing price used by cart, checkout, and provisioning.</li>
<li>The <strong>Game Name</strong> and <strong>Config XML</strong> columns are sourced
from <code><?php echo h("{$table_prefix}config_homes"); ?></code> and are read-only
here. To change them, update the game XML config in the panel.</li>
<li>Available servers are stored as a comma-separated list of server IDs in
<code><?php echo h("{$table_prefix}billing_services.remote_server_id"); ?></code>.</li>
<li>The service list is automatically synced with the panel game configuration on
every page load. New games are added with <em>Enabled = off</em> so they do not
appear in the store until you configure and enable them.</li>
<li>Games removed from the panel configuration are disabled automatically; they are
never deleted while orders may reference them.</li>
</ul>
</div>
<?php billing_maybe_close_db($db); ?>
<script>
// Toggle fallback text input when image dropdown changes
document.querySelectorAll('select[data-fallback-id]').forEach((sel) => {
sel.addEventListener('change', function () {
const fb = document.getElementById(this.dataset.fallbackId);
if (!fb) return;
const show = (this.value === '__other__');
fb.classList.toggle('img-fallback-visible', show);
if (!show) fb.value = '';
});
});
</script>
</body>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('adminserverlist.php');

View file

@ -1,433 +1,3 @@
<?php
/**
* PayPal Order Capture Endpoint
* Uses PayPalGateway, BillingService, and BillingRepository.
* Credentials come from config NOT hardcoded here.
*/
ini_set('display_errors', '0');
error_reporting(E_ALL);
ob_start();
require_once __DIR__ . '/../includes/config_loader.php';
require_once __DIR__ . '/../classes/PaymentGatewayInterface.php';
require_once __DIR__ . '/../classes/PayPalGateway.php';
require_once __DIR__ . '/../classes/GatewayFactory.php';
require_once __DIR__ . '/../classes/BillingRepository.php';
require_once __DIR__ . '/../classes/BillingService.php';
// Logging setup
$logDir = __DIR__ . '/../logs';
@mkdir($logDir, 0755, true);
$logFile = $logDir . '/payment_capture.log';
$requestId = uniqid('req_', true);
function cap_log(string $label, $data): void {
global $logFile, $requestId;
$entry = '[' . date('Y-m-d H:i:s') . "] [$requestId] $label\n";
$entry .= is_array($data) || is_object($data) ? print_r($data, true) : (string)$data;
$entry .= "\n" . str_repeat('-', 80) . "\n";
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
}
header('Content-Type: application/json');
// Session (single call)
if (session_status() === PHP_SESSION_NONE) {
session_name('opengamepanel_web');
session_start();
}
$userId = intval($_SESSION['website_user_id'] ?? $_SESSION['user_id'] ?? 0);
if ($userId <= 0) {
cap_log('NO_USER_SESSION', ['session_keys' => array_keys($_SESSION)]);
ob_clean();
echo json_encode([
'success' => false,
'error_code' => 'no_user_session',
'message' => 'You must be logged in to complete payment.',
'timestamp' => date('c'),
'request_id' => $requestId,
]);
exit;
}
// Parse input
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
ob_clean();
echo json_encode([
'success' => false,
'error_code' => 'invalid_json',
'message' => 'Invalid JSON in request body.',
'timestamp' => date('c'),
'request_id' => $requestId,
]);
exit;
}
$paypalOrderId = $input['order_id'] ?? null;
if (!$paypalOrderId) {
ob_clean();
echo json_encode([
'success' => false,
'error_code' => 'missing_order_id',
'message' => 'Missing PayPal order ID.',
'timestamp' => date('c'),
'request_id' => $requestId,
]);
exit;
}
cap_log('REQUEST', ['order_id' => $paypalOrderId, 'user_id' => $userId]);
// DB connection
$port = intval($db_port ?? 3306) ?: 3306;
$mysqli = @mysqli_connect($db_host, $db_user, $db_pass, $db_name, $port);
if (!$mysqli) {
cap_log('DB_FAILED', mysqli_connect_error());
ob_clean();
echo json_encode([
'success' => false,
'error_code' => 'db_connection_failed',
'message' => 'Database connection failed.',
'timestamp' => date('c'),
'request_id' => $requestId,
]);
exit;
}
mysqli_set_charset($mysqli, 'utf8mb4');
$prefix = $table_prefix ?? 'gsp_';
$repo = new BillingRepository($mysqli, $prefix);
function cap_invoice_ids_from_custom_id(?string $customId): array {
if (!is_string($customId) || $customId === '') {
return [];
}
if (ctype_digit($customId)) {
return [intval($customId)];
}
if (stripos($customId, 'cart:') !== 0) {
return [];
}
$parts = explode(',', substr($customId, 5));
$invoiceIds = [];
foreach ($parts as $part) {
$part = trim($part);
if ($part !== '' && ctype_digit($part)) {
$invoiceIds[] = intval($part);
}
}
return array_values(array_unique($invoiceIds));
}
function cap_get_duration_metadata(array $invoice): array {
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
}
function cap_get_end_date(array $invoice, ?string $fromDate = null): string {
$meta = cap_get_duration_metadata($invoice);
$qty = max(1, intval($invoice['qty'] ?? 1));
$baseTs = time();
if (!empty($fromDate)) {
$fromTs = strtotime($fromDate);
if ($fromTs !== false && $fromTs > time()) {
$baseTs = $fromTs;
}
}
return date('Y-m-d H:i:s', $baseTs + ($meta['days'] * $qty * 86400));
}
function cap_discount_map(array $invoices, float $paidAmount): array {
$baseTotals = [];
$baseAmount = 0.0;
foreach ($invoices as $invoice) {
$invoiceId = intval($invoice['invoice_id'] ?? 0);
$lineBase = round((float)($invoice['subtotal'] ?? $invoice['total_due'] ?? $invoice['amount'] ?? 0), 2);
$baseTotals[$invoiceId] = $lineBase;
$baseAmount += $lineBase;
}
$discountTotal = round(max(0, $baseAmount - $paidAmount), 2);
if ($discountTotal <= 0 || $baseAmount <= 0) {
return array_fill_keys(array_keys($baseTotals), 0.0);
}
$discounts = [];
$remaining = $discountTotal;
$lastInvoiceId = array_key_last($baseTotals);
foreach ($baseTotals as $invoiceId => $lineBase) {
if ($invoiceId === $lastInvoiceId) {
$lineDiscount = $remaining;
} else {
$lineDiscount = round($discountTotal * ($lineBase / $baseAmount), 2);
$remaining = round($remaining - $lineDiscount, 2);
}
$discounts[$invoiceId] = min($lineBase, max(0, $lineDiscount));
}
return $discounts;
}
// Capture payment via PayPal gateway
try {
$gateway = GatewayFactory::make('paypal');
} catch (Exception $e) {
cap_log('GATEWAY_ERROR', $e->getMessage());
$repo->logPaypalError([
'context' => 'gateway_init',
'error_code' => 'gateway_init_failed',
'message' => $e->getMessage(),
'order_id' => $paypalOrderId,
'user_id' => $userId,
]);
ob_clean();
echo json_encode([
'success' => false,
'error_code' => 'gateway_init_failed',
'message' => 'Payment gateway initialisation failed.',
'timestamp' => date('c'),
'request_id' => $requestId,
]);
mysqli_close($mysqli);
exit;
}
$capture = $gateway->handleCallback(['order_id' => $paypalOrderId]);
cap_log('CAPTURE_RESULT', ['success' => $capture['success'], 'txid' => $capture['transaction_id'] ?? null]);
if (!$capture['success']) {
cap_log('CAPTURE_FAILED', $capture);
// Sanitize raw capture data before logging — never store secrets
$captureForLog = $capture;
foreach (['client_secret', 'access_token', 'refresh_token'] as $_sk) {
unset($captureForLog[$_sk]);
}
$repo->logPaypalError([
'context' => 'capture_order',
'error_code' => $capture['error'] ?? 'capture_failed',
'message' => $capture['message'] ?? 'PayPal order capture failed.',
'paypal_debug_id' => $capture['debug_id'] ?? null,
'order_id' => $paypalOrderId,
'user_id' => $userId,
'raw_json' => $captureForLog,
]);
ob_clean();
echo json_encode([
'success' => false,
'error_code' => $capture['error'] ?? 'capture_failed',
'message' => $capture['message'] ?? 'PayPal order capture failed. Please try again.',
'debug_id' => $capture['debug_id'] ?? null,
'timestamp' => date('c'),
'request_id' => $requestId,
]);
mysqli_close($mysqli);
exit;
}
$txid = $capture['transaction_id'] ?? '';
$paidAmount = round((float)($capture['amount'] ?? 0), 2);
$capture['payment_method'] = 'paypal';
$invoiceIds = cap_invoice_ids_from_custom_id($capture['custom_id'] ?? null);
$invoices = !empty($invoiceIds)
? $repo->getInvoicesForUserByIds($userId, $invoiceIds, true)
: $repo->getUnpaidInvoicesForUser($userId);
$invoicesPaid = 0;
$ordersCreated = 0;
$newOrderIds = [];
$now = date('Y-m-d H:i:s');
$couponId = intval($_SESSION['cart_coupon_id'] ?? 0);
$discountMap = cap_discount_map($invoices, $paidAmount);
$couponCode = trim((string)($_SESSION['cart_coupon_code'] ?? ''));
if ($couponId <= 0 && $couponCode !== '') {
$coupon = $repo->getCouponByCode($couponCode);
$couponId = intval($coupon['coupon_id'] ?? 0);
}
if (empty($invoices)) {
cap_log('NO_INVOICES', ['user_id' => $userId, 'custom_id' => $capture['custom_id'] ?? null]);
ob_clean();
echo json_encode([
'success' => false,
'error_code' => 'no_matching_invoices',
'message' => 'No matching unpaid invoices were found for this payment.',
'timestamp' => date('c'),
'request_id' => $requestId,
]);
mysqli_close($mysqli);
exit;
}
foreach ($invoices as $inv) {
$invoiceId = intval($inv['invoice_id']);
$homeId = intval($inv['home_id'] ?? 0);
$invoiceBase = round((float)($inv['subtotal'] ?? $inv['total_due'] ?? $inv['amount'] ?? 0), 2);
$lineDiscount = round((float)($discountMap[$invoiceId] ?? 0), 2);
$lineTotal = round(max(0, $invoiceBase - $lineDiscount), 2);
$durationMeta = cap_get_duration_metadata($inv);
$invoiceUpdate = [
'coupon_id' => $couponId,
'discount_amount' => $lineDiscount,
'subtotal' => $invoiceBase,
'amount' => $lineTotal,
'total_due' => $lineTotal,
'status' => 'paid',
'billing_status' => 'Active',
'payment_status' => 'paid',
'payment_txid' => $txid,
'payment_method' => 'paypal',
'paid_date' => $now,
'invoice_duration' => $durationMeta['invoice_duration'],
'rate_type' => $durationMeta['rate_type'],
];
if (!$repo->updateInvoiceFields($invoiceId, $invoiceUpdate)) {
cap_log('INVOICE_PAY_FAILED', ['invoice_id' => $invoiceId, 'db_error' => $mysqli->error]);
continue;
}
$invoicesPaid++;
cap_log('INVOICE_PAID', ['invoice_id' => $invoiceId, 'txid' => $txid, 'amount' => $lineTotal]);
$rawCapture = $capture['raw_response'] ?? [];
if (is_array($rawCapture)) {
unset($rawCapture['client_secret'], $rawCapture['access_token']); // never log secrets
}
// Resolve (or create) the billing_orders row for this invoice so the provisioner can run.
// billing_orders.status='Active' is what create_servers.php queries.
$orderId = intval($inv['order_id'] ?? 0);
$currentHomeId = $homeId;
if ($orderId > 0) {
// Existing order linked to this invoice — extend it and mark Active.
$order = $repo->getOrder($orderId);
if ($order) {
$newEnd = cap_get_end_date($inv, $order['end_date'] ?? null);
$currentHomeId = intval($order['home_id'] ?? 0);
$repo->updateOrderFields($orderId, [
'status' => 'Active',
'end_date' => $newEnd,
'payment_txid' => $txid,
'paid_ts' => $now,
'price' => $lineTotal,
'discount_amount' => $lineDiscount,
'coupon_id' => $couponId,
]);
if ($currentHomeId > 0) {
$repo->updateInvoiceFields($invoiceId, ['home_id' => $currentHomeId]);
}
$ordersCreated++;
if (!in_array($orderId, $newOrderIds, true)) {
$newOrderIds[] = $orderId;
}
cap_log('ORDER_QUEUED_PROVISION', ['order_id' => $orderId, 'home_id' => $currentHomeId]);
}
} else {
// No billing_orders row yet — create one now so the provisioner can run.
$newEnd = cap_get_end_date($inv, null);
$newOrderId = $repo->createOrder([
'user_id' => intval($inv['user_id']),
'service_id' => intval($inv['service_id']),
'home_name' => $inv['home_name'] ?? '',
'ip' => (string)($inv['ip'] ?? '0'),
'qty' => intval($inv['qty'] ?? 1),
'invoice_duration' => $durationMeta['invoice_duration'],
'max_players' => intval($inv['max_players'] ?? 0),
'price' => $lineTotal,
'discount_amount' => $lineDiscount,
'remote_control_password' => $inv['remote_control_password'] ?? '',
'ftp_password' => $inv['ftp_password'] ?? '',
'status' => 'Active',
'end_date' => $newEnd,
'payment_txid' => $txid,
'paid_ts' => $now,
'coupon_id' => $couponId,
]);
if ($newOrderId > 0) {
// Link invoice → order so retried captures are idempotent.
$repo->updateInvoiceOrderId($invoiceId, $newOrderId);
$repo->updateInvoiceFields($invoiceId, ['order_id' => $newOrderId]);
if (!in_array($newOrderId, $newOrderIds, true)) {
$newOrderIds[] = $newOrderId;
}
$ordersCreated++;
cap_log('ORDER_CREATED', ['invoice_id' => $invoiceId, 'order_id' => $newOrderId]);
} else {
cap_log('ORDER_CREATE_FAILED', ['invoice_id' => $invoiceId, 'db_error' => $mysqli->error]);
continue;
}
}
$repo->logTransaction([
'invoice_id' => $invoiceId,
'user_id' => $userId,
'home_id' => $currentHomeId,
'payment_method' => 'paypal',
'transaction_external_id' => $txid,
'amount' => $lineTotal,
'currency' => (string)($inv['currency'] ?? 'USD'),
'status' => 'completed',
'raw_response' => $rawCapture,
]);
}
if ($couponId > 0 && $invoicesPaid > 0) {
$mysqli->query("UPDATE `{$prefix}billing_coupons`
SET current_uses = current_uses + 1
WHERE coupon_id = " . intval($couponId));
}
// Auto-provision new servers (orders without a home_id)
$autoProvision = ['provisioned_count' => 0, 'failed_count' => 0];
if (!empty($newOrderIds)) {
require_once __DIR__ . '/../includes/panel_bridge.php';
$panelCtx = billing_panel_bootstrap();
if ($panelCtx && isset($panelCtx['db'])) {
$GLOBALS['db'] = $panelCtx['db'];
$GLOBALS['settings'] = $panelCtx['settings'];
require_once __DIR__ . '/../create_servers.php';
$autoProvision = billing_invoke_provision(['order_ids' => $newOrderIds, 'user_id' => $userId, 'is_admin' => true]);
if (($autoProvision['failed_count'] ?? 0) > 0) {
cap_log('AUTO_PROVISION_PARTIAL_FAILURE', $autoProvision);
}
} else {
cap_log('AUTO_PROVISION_SKIPPED', 'panel bootstrap failed — orders require manual provisioning: ' . implode(',', $newOrderIds));
$autoProvision = [
'provisioned_count' => 0,
'failed_count' => count($newOrderIds),
'details' => [],
'trace_log_path' => 'modules/billing/logs/provisioning_trace.log',
'trace_error' => 'Panel bootstrap failed before billing provisioning could start.',
];
}
}
if (function_exists('billing_store_provision_session_result')) {
billing_store_provision_session_result($txid, [
'source' => 'api/capture_order.php',
'txid' => $txid,
'order_ids' => $newOrderIds,
'result' => $autoProvision,
]);
}
unset($_SESSION['cart_coupon_code'], $_SESSION['cart_coupon_id']);
mysqli_close($mysqli);
cap_log('COMPLETE', ['invoices_paid' => $invoicesPaid, 'txid' => $txid, 'orders' => $newOrderIds]);
ob_clean();
echo json_encode([
'success' => true,
'status' => 'COMPLETED',
'txid' => $txid,
'invoices_paid' => $invoicesPaid,
'orders_created' => $ordersCreated,
'provisioned' => $autoProvision['provisioned_count'] ?? 0,
'request_id' => $requestId,
]);
require_once __DIR__ . '/../_compat_include.php';
require website_billing_runtime_file('api/capture_order.php');

View file

@ -1,104 +1,3 @@
<?php
/**
* PayPal Create Order API Endpoint
* Uses PayPalGateway class. Credentials come from config NOT hardcoded here.
*/
ini_set('display_errors', '0');
error_reporting(E_ALL);
require_once __DIR__ . '/../includes/config_loader.php';
require_once __DIR__ . '/../classes/PaymentGatewayInterface.php';
require_once __DIR__ . '/../classes/PayPalGateway.php';
require_once __DIR__ . '/../classes/GatewayFactory.php';
// Logging
$logDir = __DIR__ . '/../logs';
@mkdir($logDir, 0755, true);
$logFile = $logDir . '/paypal_create_order.log';
$requestId = uniqid('req_', true);
function co_log(string $label, $data): void {
global $logFile, $requestId;
$entry = '[' . date('Y-m-d H:i:s') . "] [$requestId] $label\n";
$entry .= is_array($data) || is_object($data) ? print_r($data, true) : (string)$data;
$entry .= "\n" . str_repeat('-', 80) . "\n";
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
}
header('Content-Type: application/json');
$rawInput = file_get_contents('php://input');
$in = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE || !$in) {
http_response_code(400);
echo json_encode([
'success' => false,
'error_code' => 'invalid_json',
'message' => 'Invalid JSON in request body.',
'timestamp' => date('c'),
'request_id' => $requestId,
]);
exit;
}
co_log('REQUEST', ['amount' => $in['amount'] ?? null, 'invoice_id' => $in['invoice_id'] ?? null]);
// Resolve site base for return/cancel URLs
$siteBase = rtrim($GLOBALS['SITE_BASE_URL'] ?? '', '/');
$returnUrl = $in['return_url'] ?? '/payment_success.php';
$cancelUrl = $in['cancel_url'] ?? '/payment_cancel.php';
// Ensure absolute URLs
if (strpos($returnUrl, 'http') !== 0) {
$returnUrl = $siteBase . '/' . ltrim($returnUrl, '/');
}
if (strpos($cancelUrl, 'http') !== 0) {
$cancelUrl = $siteBase . '/' . ltrim($cancelUrl, '/');
}
// Build gateway params
$params = [
'amount' => $in['amount'] ?? '0.00',
'currency' => $in['currency'] ?? 'USD',
'invoice_id' => $in['invoice_id'] ?? null,
'custom_id' => $in['custom_id'] ?? $in['invoice_id'] ?? null,
'description' => $in['description'] ?? 'Game Server Order',
'return_url' => $returnUrl,
'cancel_url' => $cancelUrl,
'items' => $in['items'] ?? null,
];
try {
$gateway = GatewayFactory::make('paypal');
$result = $gateway->createPayment($params);
} catch (Exception $e) {
co_log('EXCEPTION', $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error_code' => 'gateway_error',
'message' => $e->getMessage(),
'debug_id' => null,
'timestamp' => date('c'),
'request_id' => $requestId,
]);
exit;
}
if (!$result['success']) {
co_log('CREATE_FAILED', $result);
http_response_code(500);
echo json_encode([
'success' => false,
'error_code' => $result['error'] ?? 'create_failed',
'message' => $result['message'] ?? 'Failed to create PayPal order.',
'debug_id' => $result['debug_id'] ?? null,
'timestamp' => date('c'),
'request_id' => $requestId,
]);
exit;
}
co_log('CREATE_SUCCESS', ['provider_order_id' => $result['provider_order_id']]);
echo json_encode($result['raw_response']);
require_once __DIR__ . '/../_compat_include.php';
require website_billing_runtime_file('api/create_order.php');

View file

@ -1,956 +1,3 @@
<?php
/**
* Shopping Cart - Rebuilt from scratch for reliability
* Displays unpaid invoices and provides PayPal checkout
* Standalone billing module - uses only standard PHP mysqli
*/
// Start session with website session name
if (session_status() === PHP_SESSION_NONE) {
session_name("opengamepanel_web");
session_start();
}
// Load configuration
require_once(__DIR__ . '/bootstrap.php');
function billing_cart_money_to_cents(float $amount): int
{
return (int) round($amount * 100);
}
function billing_cart_cents_to_money(int $cents): float
{
return $cents / 100;
}
// Variables from config.inc.php (helps IDEs understand scope)
/** @var string $db_host Database host */
/** @var string $db_user Database user */
/** @var string $db_pass Database password */
/** @var string $db_name Database name */
/** @var string $table_prefix Table prefix for database tables */
/** @var string $SITE_BASE_URL Site base URL */
/** @var string $SITE_DATA_DIR Data directory path */
// Check if user is logged in
$user_id = 0;
if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) {
$user_id = intval($_SESSION['website_user_id']);
} elseif (isset($_SESSION['user_id']) && !empty($_SESSION['user_id'])) {
$user_id = intval($_SESSION['user_id']);
}
// Redirect to login if not authenticated
if ($user_id <= 0) {
$return_to = urlencode($_SERVER['REQUEST_URI'] ?? '/cart.php');
header('Location: /login.php?return_to=' . $return_to);
exit;
}
// Connect to database (non-fatal)
$db = @mysqli_connect($db_host, $db_user, $db_pass, $db_name);
$db_error = '';
// Initialize variables
$invoices = [];
$total_amount = 0.00;
$total_amount_cents = 0;
$discount_amount = 0.00;
$discount_amount_cents = 0;
$coupon_discount_percent = 0;
$applied_coupon = null;
$error_message = '';
$success_message = '';
if (!$db) {
// record error for UI/debugging but do not die here
$db_error = 'Database connection failed: ' . mysqli_connect_error();
$cart_empty = true;
} else {
// Fetch unpaid invoices for this user. Select only invoice fields to avoid referencing
// columns that may not exist in all deployments (some schemas differ).
$query = "SELECT i.*
FROM {$table_prefix}billing_invoices i
WHERE i.user_id = " . intval($user_id) . "
AND (i.status = 'due' OR i.status = '')
AND (i.payment_status IS NULL OR i.payment_status NOT IN ('paid','cancelled','refunded'))
ORDER BY i.invoice_date ASC";
$result = mysqli_query($db, $query);
if ($result) {
while ($row = mysqli_fetch_assoc($result)) {
$invoices[] = $row;
$lineAmount = (float)($row['total_due'] ?? $row['amount'] ?? 0);
$total_amount_cents += billing_cart_money_to_cents($lineAmount);
}
mysqli_free_result($result);
}
$cart_empty = (count((array)$invoices) === 0);
$total_amount = billing_cart_cents_to_money($total_amount_cents);
}
// Handle coupon application
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['apply_coupon'])) {
$coupon_code = trim($_POST['coupon_code'] ?? '');
if (empty($coupon_code)) {
$error_message = 'Please enter a coupon code.';
} else {
// Validate coupon
if (!$db) {
$error_message = 'Coupon system unavailable: database connection failed.';
} else {
$safe_code = mysqli_real_escape_string($db, $coupon_code);
$coupon_query = "SELECT * FROM {$table_prefix}billing_coupons
WHERE code = '$safe_code' AND is_active = 1";
$coupon_result = mysqli_query($db, $coupon_query);
if ($coupon_result && mysqli_num_rows($coupon_result) === 1) {
$coupon = mysqli_fetch_assoc($coupon_result);
// Check if expired
$expired = false;
if (!empty($coupon['expires'])) {
$expires_time = strtotime($coupon['expires']);
if ($expires_time && $expires_time < time()) {
$expired = true;
}
}
// Check usage limit
$max_uses_reached = false;
if (!empty($coupon['max_uses'])) {
if (intval($coupon['current_uses']) >= intval($coupon['max_uses'])) {
$max_uses_reached = true;
}
}
if ($expired) {
$error_message = 'This coupon has expired.';
} elseif ($max_uses_reached) {
$error_message = 'This coupon has reached its maximum usage limit.';
} else {
// Check game filter
$game_valid = true;
if ($coupon['game_filter_type'] === 'specific_games' && !empty($coupon['game_filter_list'])) {
$allowed_games = json_decode($coupon['game_filter_list'], true);
if (is_array($allowed_games) && count((array)$allowed_games) > 0) {
$has_valid_game = false;
foreach ((array)$invoices as $inv) {
$inv_game_key = isset($inv['game_key']) ? $inv['game_key'] : null;
if ($inv_game_key !== null && in_array($inv_game_key, $allowed_games)) {
$has_valid_game = true;
break;
}
}
if (!$has_valid_game) {
$game_valid = false;
}
}
}
if (!$game_valid) {
$error_message = 'This coupon is not valid for the items in your cart.';
} else {
// Apply coupon
$applied_coupon = $coupon;
$coupon_discount_percent = floatval($coupon['discount_percent']);
$_SESSION['cart_coupon_code'] = $coupon_code;
$_SESSION['cart_coupon_id'] = $coupon['coupon_id'];
$success_message = 'Coupon "' . htmlspecialchars($coupon['name']) . '" applied! You save ' . $coupon_discount_percent . '%';
}
}
mysqli_free_result($coupon_result);
} else {
$error_message = 'Invalid coupon code.';
}
}
}
}
// Handle coupon removal
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_coupon'])) {
unset($_SESSION['cart_coupon_code']);
unset($_SESSION['cart_coupon_id']);
$applied_coupon = null;
$coupon_discount_percent = 0;
}
// Re-validate coupon from session if present
if ($db && empty($applied_coupon) && isset($_SESSION['cart_coupon_code'])) {
$coupon_code = $_SESSION['cart_coupon_code'];
$safe_code = mysqli_real_escape_string($db, $coupon_code);
$coupon_query = "SELECT * FROM {$table_prefix}billing_coupons
WHERE code = '$safe_code' AND is_active = 1";
$coupon_result = mysqli_query($db, $coupon_query);
if ($coupon_result && mysqli_num_rows($coupon_result) === 1) {
$applied_coupon = mysqli_fetch_assoc($coupon_result);
$coupon_discount_percent = floatval($applied_coupon['discount_percent']);
mysqli_free_result($coupon_result);
} else {
// Coupon no longer valid, clear from session
unset($_SESSION['cart_coupon_code']);
unset($_SESSION['cart_coupon_id']);
}
}
// AJAX remove invoice action (hard delete) - returns JSON when remove_invoice_ajax is set
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_invoice_ajax']) && isset($_POST['invoice_id'])) {
header('Content-Type: application/json');
$remove_id = intval($_POST['invoice_id']);
if ($remove_id <= 0) {
echo json_encode(['success' => false, 'error' => 'Invalid invoice id.']);
exit;
}
if (!$db) {
echo json_encode(['success' => false, 'error' => 'Database unavailable.']);
exit;
}
// Verify ownership and that invoice is still unpaid/due
$check_q = "SELECT invoice_id FROM {$table_prefix}billing_invoices WHERE invoice_id = " . intval($remove_id) . " AND user_id = " . intval($user_id) . " AND (status = 'due' OR status = '') AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded')) LIMIT 1";
$check_r = mysqli_query($db, $check_q);
if (!($check_r && mysqli_num_rows($check_r) === 1)) {
echo json_encode(['success' => false, 'error' => 'Invoice not found or cannot be removed.']);
exit;
}
// Hard-delete the invoice row
$del_q = "DELETE FROM {$table_prefix}billing_invoices WHERE invoice_id = " . intval($remove_id) . " AND user_id = " . intval($user_id) . " AND (status = 'due' OR status = '') AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded')) LIMIT 1";
$ok = mysqli_query($db, $del_q);
if ($ok && mysqli_affected_rows($db) > 0) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Failed to delete invoice.']);
}
exit;
}
// Handle non-AJAX remove invoice action (hard delete + redirect)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_invoice']) && isset($_POST['invoice_id'])) {
$remove_id = intval($_POST['invoice_id']);
if ($remove_id <= 0) {
$error_message = 'Invalid invoice id.';
} else {
if (!$db) {
$error_message = 'Unable to remove item: database unavailable.';
} else {
// Verify ownership and that invoice is still unpaid/due
$check_q = "SELECT invoice_id FROM {$table_prefix}billing_invoices WHERE invoice_id = " . intval($remove_id) . " AND user_id = " . intval($user_id) . " AND (status = 'due' OR status = '') AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded')) LIMIT 1";
$check_r = mysqli_query($db, $check_q);
if ($check_r && mysqli_num_rows($check_r) === 1) {
// Hard-delete to remove from cart
$del_q = "DELETE FROM {$table_prefix}billing_invoices WHERE invoice_id = " . intval($remove_id) . " AND user_id = " . intval($user_id) . " AND (status = 'due' OR status = '') AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded')) LIMIT 1";
if (mysqli_query($db, $del_q)) {
// Reload to avoid form re-submission and refresh invoice list
header('Location: /cart.php');
exit;
} else {
$error_message = 'Failed to remove item from cart.';
}
} else {
$error_message = 'Invoice not found or cannot be removed.';
}
}
}
}
// Calculate discount
if ($applied_coupon && $coupon_discount_percent > 0) {
$discount_amount_cents = (int) round($total_amount_cents * ($coupon_discount_percent / 100));
$discount_amount_cents = min($discount_amount_cents, $total_amount_cents);
}
$discount_amount = billing_cart_cents_to_money($discount_amount_cents);
$final_amount_cents = max(0, $total_amount_cents - $discount_amount_cents);
$final_amount = billing_cart_cents_to_money($final_amount_cents);
// PayPal configuration (from config)
$client_id = function_exists('gsp_paypal_get_client_id') ? gsp_paypal_get_client_id() : ($paypal_client_id ?? '');
$sandbox = function_exists('gsp_paypal_is_sandbox') ? gsp_paypal_is_sandbox() : ($paypal_sandbox ?? true);
// Prepare PayPal items
$paypal_items = [];
$paypal_invoice_ids = [];
foreach ((array)$invoices as $inv) {
$game_display = !empty($inv['game_name']) ? $inv['game_name'] : 'Game Server';
$qty = max(1, intval($inv['qty']));
$paypal_invoice_ids[] = intval($inv['invoice_id']);
$lineAmountCents = billing_cart_money_to_cents((float)($inv['total_due'] ?? $inv['amount'] ?? 0));
$lineAmount = billing_cart_cents_to_money($lineAmountCents);
$paypal_items[] = [
'name' => $inv['home_name'] . ' (' . $game_display . ')',
'description' => $inv['description'] ?? '',
'quantity' => $qty,
'unit_amount' => [
'currency_code' => 'USD',
'value' => number_format($lineAmount / $qty, 2, '.', '')
]
];
}
$paypal_custom_id = 'cart:' . implode(',', $paypal_invoice_ids);
// Get site base URL
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$siteBase = $protocol . $host;
// (Do not close the shared DB connection here; menu and other includes may use it.)
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shopping Cart - Game Server Panel</title>
<link rel="stylesheet" href="css/header.css">
<style>
/* Do not override site-wide font or header/menu styles here.
Keep body reset minimal so includes/menu.php can control header styling. */
body {
margin: 0;
padding: 0;
}
.cart-container {
max-width: 900px;
margin: 24px auto;
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
width: min(100%, calc(100% - 24px));
box-sizing: border-box;
}
h1 {
color: #333;
margin-bottom: 30px;
font-size: 2em;
}
.alert {
padding: 12px 20px;
margin-bottom: 20px;
border-radius: 4px;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.cart-empty {
text-align: center;
padding: 60px 20px;
}
.cart-empty h2 {
color: #666;
margin-bottom: 15px;
}
.cart-empty p {
color: #999;
margin-bottom: 30px;
}
.cart-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
table-layout: fixed;
}
.cart-table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
color: #495057;
}
.cart-table td {
padding: 15px 12px;
border-bottom: 1px solid #dee2e6;
}
.cart-table tbody tr:hover {
background: #f8f9fa;
}
.game-name {
font-weight: 600;
color: #007bff;
font-size: 1.05em;
}
.server-name {
color: #666;
font-size: 0.9em;
margin-top: 4px;
}
.description {
color: #999;
font-size: 0.85em;
margin-top: 4px;
}
.price {
font-weight: 600;
color: #28a745;
font-size: 1.1em;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 600;
background: #fff3cd;
color: #856404;
}
.coupon-section {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.coupon-section h3 {
margin-top: 0;
color: #333;
}
.coupon-form {
display: flex;
gap: 10px;
align-items: flex-end;
flex-wrap: wrap;
}
.coupon-form > div {
flex: 1;
}
.coupon-form label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #495057;
}
.coupon-form input {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1em;
}
.coupon-applied {
display: flex;
justify-content: space-between;
align-items: center;
background: #d4edda;
padding: 15px;
border-radius: 4px;
border: 1px solid #c3e6cb;
}
.coupon-applied-text {
color: #155724;
}
.cart-total {
text-align: right;
padding: 20px 0;
border-top: 2px solid #dee2e6;
margin-bottom: 30px;
}
.cart-total-row {
margin-bottom: 10px;
}
.cart-total-label {
font-size: 1.2em;
font-weight: 600;
margin-right: 20px;
color: #495057;
}
.cart-total-amount {
font-size: 1.5em;
font-weight: 700;
color: #28a745;
}
.subtotal-amount {
font-size: 1.2em;
color: #666;
}
.discount-amount {
font-size: 1.2em;
font-weight: 600;
color: #28a745;
}
.btn {
display: inline-block;
padding: 12px 24px;
background: #007bff;
color: white;
text-decoration: none;
border: none;
border-radius: 5px;
font-weight: 600;
cursor: pointer;
font-size: 1em;
}
.btn:hover {
background: #0056b3;
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #545b62;
}
.btn-small {
padding: 8px 16px;
font-size: 0.9em;
}
.checkout-section {
padding: 20px 0;
}
.checkout-section h3 {
color: #333;
margin-bottom: 10px;
}
.checkout-section p {
color: #666;
margin-bottom: 20px;
}
#paypal-button-container {
max-width: 400px;
margin: 20px 0;
}
.status-message {
text-align: center;
padding: 20px;
color: #666;
display: none;
}
.action-buttons {
margin-top: 30px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cart-table-wrap {
width: 100%;
max-width: 100%;
overflow-x: auto;
}
@media (max-width: 768px) {
.cart-container {
width: min(100%, calc(100% - 12px));
padding: 14px;
margin: 12px auto;
}
h1 {
font-size: 1.5rem;
margin-bottom: 18px;
}
.cart-table thead {
display: none;
}
.cart-table,
.cart-table tbody,
.cart-table tr,
.cart-table td {
display: block;
width: 100%;
}
.cart-table tr {
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 12px;
padding: 6px 8px;
background: #fff;
}
.cart-table td {
border: 0;
padding: 6px 4px;
text-align: left !important;
}
.cart-table td[data-label]::before {
content: attr(data-label) ": ";
font-weight: 600;
color: #495057;
}
.coupon-form {
flex-direction: column;
align-items: stretch;
}
.coupon-form button {
width: 100%;
}
.coupon-applied {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.cart-total {
text-align: left;
}
.cart-total-row {
display: flex;
justify-content: space-between;
gap: 10px;
}
.cart-total-label,
.cart-total-amount,
.subtotal-amount,
.discount-amount {
font-size: 1rem;
margin-right: 0;
}
.btn {
width: 100%;
text-align: center;
}
.action-buttons {
margin-top: 16px;
}
}
</style>
<?php // Font Awesome for small icon buttons ?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Favicon -->
<link rel="icon" href="images/logo-sm.png" type="image/png">
<link rel="apple-touch-icon" href="images/logo-sm.png">
<?php if (!$cart_empty && !empty($client_id)): ?>
<script src="https://www.paypal.com/sdk/js?client-id=<?php echo htmlspecialchars($client_id, ENT_QUOTES, 'UTF-8'); ?>&currency=USD&intent=capture<?php echo $sandbox ? '&debug=false' : ''; ?>"></script>
<?php endif; ?>
</head>
<body>
<?php include(__DIR__ . '/includes/top.php'); ?>
<?php include(__DIR__ . '/includes/menu.php'); ?>
<div class="cart-container">
<?php if (!empty($db_error)): ?>
<div class="alert-error" style="margin-bottom:15px;">
<strong>Database error:</strong> <?php echo htmlspecialchars($db_error); ?>
</div>
<?php endif; ?>
<h1>🛒 Shopping Cart</h1>
<?php if ($error_message): ?>
<div class="alert alert-error"><?php echo htmlspecialchars($error_message); ?></div>
<?php endif; ?>
<?php if ($success_message): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($success_message); ?></div>
<?php endif; ?>
<?php if ($cart_empty): ?>
<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="/serverlist.php" class="btn">Browse Servers</a>
</div>
<?php else: ?>
<div class="cart-table-wrap">
<table class="cart-table">
<thead>
<tr>
<th>Game Server</th>
<th>Duration</th>
<th>Quantity</th>
<th>Status</th>
<th style="text-align: right;">Price</th>
<th style="text-align: right;">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$invoices as $inv): ?>
<tr>
<td data-label="Game Server">
<div class="game-name"><?php echo htmlspecialchars($inv['game_name'] ?? 'Game Server'); ?></div>
<div class="server-name"><?php echo htmlspecialchars($inv['home_name']); ?></div>
<?php if (!empty($inv['description'])): ?>
<div class="description"><?php echo htmlspecialchars($inv['description']); ?></div>
<?php endif; ?>
</td>
<td data-label="Duration"><?php echo htmlspecialchars((string)($inv['invoice_duration'] ?? 'month')); ?></td>
<td data-label="Quantity"><?php echo intval($inv['qty'] ?? 1); ?>x</td>
<td data-label="Status"><span class="status-badge"><?php echo htmlspecialchars(strtoupper((string)($inv['status'] ?? 'due'))); ?></span></td>
<td data-label="Price" style="text-align: right;">
<span class="price">$<?php echo number_format(floatval($inv['total_due'] ?? $inv['amount'] ?? 0), 2); ?></span>
</td>
<td data-label="Action" style="text-align: right;">
<button type="button" class="btn btn-secondary btn-small" title="Remove" onclick="removeInvoice(<?php echo intval($inv['invoice_id']); ?>)">
<i class="fa-solid fa-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Coupon Section -->
<div class="coupon-section">
<h3>Coupon Code</h3>
<?php if (!$applied_coupon): ?>
<form method="POST" class="coupon-form">
<div>
<label>Enter Code:</label>
<input type="text" name="coupon_code" placeholder="Enter coupon code" required>
</div>
<button type="submit" name="apply_coupon" class="btn">Apply Coupon</button>
</form>
<?php else: ?>
<div class="coupon-applied">
<div class="coupon-applied-text">
<strong>Coupon Applied:</strong>
<?php echo htmlspecialchars($applied_coupon['name']); ?>
(<?php echo htmlspecialchars($applied_coupon['discount_percent']); ?>% off)
</div>
<form method="POST" style="margin: 0;">
<button type="submit" name="remove_coupon" class="btn btn-secondary btn-small">Remove</button>
</form>
</div>
<?php endif; ?>
</div>
<!-- Cart Total -->
<div class="cart-total">
<?php if ($discount_amount > 0): ?>
<div class="cart-total-row">
<span class="cart-total-label">Subtotal:</span>
<span class="subtotal-amount">$<?php echo number_format($total_amount, 2); ?></span>
</div>
<div class="cart-total-row">
<span class="cart-total-label">Discount (<?php echo $coupon_discount_percent; ?>%):</span>
<span class="discount-amount">-$<?php echo number_format($discount_amount, 2); ?></span>
</div>
<?php endif; ?>
<div class="cart-total-row">
<span class="cart-total-label">Total:</span>
<span class="cart-total-amount">$<?php echo number_format($final_amount, 2); ?></span>
</div>
</div>
<!-- Checkout Section -->
<?php if ($final_amount_cents === 0): ?>
<!-- Zero-dollar checkout: coupon covers the full amount, no PayPal needed -->
<div class="checkout-section">
<h3>🎉 Complete Your Free Order</h3>
<p>Your coupon covers the full amount. Click below to confirm and automatically provision your server(s).</p>
<div id="status-message" class="status-message"></div>
<form method="POST" action="/checkout_free.php" onsubmit="document.getElementById('free-submit-btn').disabled=true; document.getElementById('status-message').style.display='block'; document.getElementById('status-message').textContent='Processing…';">
<input type="hidden" name="coupon_id" value="<?php echo intval($_SESSION['cart_coupon_id'] ?? 0); ?>">
<input type="hidden" name="coupon_code" value="<?php echo htmlspecialchars($_SESSION['cart_coupon_code'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<button id="free-submit-btn" type="submit" class="btn" style="background:#28a745;">
Complete Free Order
</button>
</form>
<div class="action-buttons" style="margin-top:15px;">
<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>
<?php else: ?>
<div class="checkout-section">
<h3>Checkout with PayPal</h3>
<?php if (empty($client_id)): ?>
<div class="alert alert-error">
<strong>Checkout Unavailable:</strong> PayPal has not been configured for this site.
Please contact the site administrator or try again later.
<?php
// Admin hint: only show config link if the current user is an admin
$cart_user_id_check = intval($_SESSION['website_user_id'] ?? 0);
$cart_is_admin = false;
if ($cart_user_id_check > 0 && $db) {
$ar = mysqli_query($db, "SELECT users_role FROM {$table_prefix}users WHERE user_id = " . $cart_user_id_check . " LIMIT 1");
if ($ar && ($arow = mysqli_fetch_assoc($ar))) {
$cart_is_admin = strtolower($arow['users_role'] ?? '') === 'admin';
}
}
if ($cart_is_admin):
?>
<br><small><em>Admin: configure PayPal credentials in <a href="/admin_config.php" style="color:inherit;text-decoration:underline;">Site Config</a>.</em></small>
<?php endif; ?>
</div>
<?php else: ?>
<p>Click the button below to complete your purchase securely through PayPal.</p>
<div id="paypal-button-container"></div>
<div id="status-message" class="status-message"></div>
<?php endif; ?>
<div class="action-buttons">
<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>
<?php endif; ?>
<script>
function setStatus(msg) {
const statusDiv = document.getElementById('status-message');
if (statusDiv) {
statusDiv.textContent = msg;
statusDiv.style.display = 'block';
}
}
</script>
<?php if ($final_amount_cents > 0 && !empty($client_id)): ?>
<script>
function showPaymentError(msg) {
var statusDiv = document.getElementById('status-message');
if (statusDiv) {
statusDiv.textContent = msg;
statusDiv.style.display = 'block';
statusDiv.style.color = '#721c24';
statusDiv.style.background = '#f8d7da';
statusDiv.style.border = '1px solid #f5c6cb';
statusDiv.style.padding = '12px 16px';
statusDiv.style.borderRadius = '4px';
}
}
function logErrorToServer(context, errorCode, message, debugId, orderId) {
try {
fetch('/api/log_error.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
context: context,
error_code: errorCode,
message: message,
paypal_debug_id: debugId || null,
order_id: orderId || null,
timestamp: new Date().toISOString()
})
}).catch(function() {}); // silently ignore logging failures
} catch (e) {}
}
paypal.Buttons({
createOrder: function(data, actions) {
setStatus('Creating order...');
return actions.order.create({
purchase_units: [{
custom_id: '<?php echo htmlspecialchars($paypal_custom_id, ENT_QUOTES, 'UTF-8'); ?>',
amount: {
currency_code: 'USD',
value: '<?php echo number_format($final_amount, 2, '.', ''); ?>',
breakdown: {
item_total: {
currency_code: 'USD',
value: '<?php echo number_format($total_amount, 2, '.', ''); ?>'
}
<?php if ($discount_amount > 0): ?>
,
discount: {
currency_code: 'USD',
value: '<?php echo number_format($discount_amount, 2, '.', ''); ?>'
}
<?php endif; ?>
}
},
items: <?php echo json_encode($paypal_items); ?>
}]
});
},
onApprove: function(data, actions) {
setStatus('Processing payment...');
return fetch('/api/capture_order.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: data.orderID })
})
.then(function(res) {
return res.json().then(function(body) {
return { ok: res.ok, body: body };
}).catch(function() {
return { ok: false, body: { error_code: 'invalid_response', message: 'Server returned non-JSON response (HTTP ' + res.status + ').' } };
});
})
.then(function(result) {
if (!result.ok || result.body.success === false) {
var errCode = result.body.error_code || result.body.error || 'capture_failed';
var errMsg = result.body.message || 'Payment capture failed. Please try again or contact support.';
var debugId = result.body.debug_id || null;
logErrorToServer('cart_capture', errCode, errMsg, debugId, data.orderID);
showPaymentError('Payment failed: ' + errMsg);
return;
}
// status=COMPLETED is the success indicator
if (result.body.status === 'COMPLETED') {
setStatus('Payment successful! Redirecting...');
window.location.href = '/payment_success.php?order_id=' + encodeURIComponent(data.orderID);
} else {
var unexpectedMsg = 'Unexpected payment status: ' + (result.body.status || 'unknown');
logErrorToServer('cart_capture', 'unexpected_status', unexpectedMsg, null, data.orderID);
showPaymentError(unexpectedMsg + '. Please contact support.');
}
})
.catch(function(err) {
var errMsg = err && err.message ? err.message : 'Network error during payment capture.';
logErrorToServer('cart_capture', 'fetch_error', errMsg, null, data.orderID);
showPaymentError('Payment error: ' + errMsg);
});
},
onError: function(err) {
var errMsg = err && err.message ? err.message : String(err);
logErrorToServer('cart_paypal_sdk', 'sdk_error', errMsg, null, null);
showPaymentError('A PayPal error occurred. Please try again or contact support.');
},
onCancel: function(data) {
setStatus('Payment cancelled.');
window.location.href = '/payment_cancel.php';
}
}).render('#paypal-button-container');
</script>
<?php endif; ?>
<script>
// Remove invoice via AJAX and perform a partial reload of the cart container
function removeInvoice(invoiceId) {
if (!confirm('Remove this item from your cart?')) return;
setStatus('Removing item...');
var body = 'remove_invoice_ajax=1&invoice_id=' + encodeURIComponent(invoiceId);
fetch(window.location.href, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data && data.success) {
// Partial reload: fetch the current page and replace the cart container
fetch(window.location.href, { method: 'GET', credentials: 'same-origin' })
.then(function(r) { return r.text(); })
.then(function(html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var newContainer = doc.querySelector('.cart-container');
var oldContainer = document.querySelector('.cart-container');
if (newContainer && oldContainer) {
oldContainer.innerHTML = newContainer.innerHTML;
} else {
// Fallback to full reload
window.location.reload();
}
});
} else {
alert(data && data.error ? data.error : 'Failed to remove item.');
setStatus('');
}
})
.catch(function(err) {
console.error('Remove error', err);
alert('Error removing item. See console for details.');
setStatus('');
});
}
</script>
<?php endif; ?>
</div>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</body>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('cart.php');

View file

@ -1,261 +1,3 @@
<?php
/**
* Free Checkout Handler
*
* Processes a zero-dollar cart when a coupon reduces the total to $0.
* Marks invoices paid (method=coupon, txid=free-<timestamp>),
* creates billing_orders rows, and triggers automatic server provisioning.
*
* POST params: coupon_id, coupon_code
*/
if (session_status() === PHP_SESSION_NONE) {
session_name('opengamepanel_web');
session_start();
}
require_once __DIR__ . '/bootstrap.php';
require_once __DIR__ . '/includes/login_required.php';
function billing_free_money_to_cents(float $amount): int
{
return (int) round($amount * 100);
}
$userId = intval($_SESSION['website_user_id'] ?? $_SESSION['user_id'] ?? 0);
if ($userId <= 0) {
header('Location: /login.php');
exit;
}
// Only accept POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /cart.php');
exit;
}
// DB connection
$mysqli = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$mysqli) {
die('<p>Database connection failed. Please <a href="/serverlist.php">return to the shop</a> or contact support.</p>');
}
mysqli_set_charset($mysqli, 'utf8mb4');
// Fetch unpaid invoices for this user (prepared statement)
$invoices = [];
$stmt = mysqli_prepare($mysqli, "SELECT * FROM {$table_prefix}billing_invoices
WHERE user_id = ?
AND (status = 'due' OR status = '')
AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded'))
ORDER BY invoice_id ASC");
if ($stmt) {
mysqli_stmt_bind_param($stmt, 'i', $userId);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
while ($row = mysqli_fetch_assoc($result)) {
$invoices[] = $row;
}
mysqli_stmt_close($stmt);
}
if (empty($invoices)) {
if ($mysqli instanceof mysqli) {
mysqli_close($mysqli);
}
header('Location: /cart.php?msg=empty');
exit;
}
// Validate coupon from POST / session
$couponId = intval($_POST['coupon_id'] ?? $_SESSION['cart_coupon_id'] ?? 0);
$couponCode = trim($_POST['coupon_code'] ?? $_SESSION['cart_coupon_code'] ?? '');
$discountPct = 0.0;
if ($couponCode !== '') {
$safe = mysqli_real_escape_string($mysqli, $couponCode);
$cr = mysqli_query($mysqli, "SELECT * FROM {$table_prefix}billing_coupons
WHERE code = '$safe' AND is_active = 1 LIMIT 1");
if ($cr && mysqli_num_rows($cr) === 1) {
$coupon = mysqli_fetch_assoc($cr);
$discountPct = (float)($coupon['discount_percent'] ?? 0);
mysqli_free_result($cr);
}
}
// Calculate total and verify it is $0 after discount
$totalAmountCents = 0;
foreach ($invoices as $inv) {
$lineAmount = (float)($inv['total_due'] ?? $inv['amount'] ?? 0);
$totalAmountCents += billing_free_money_to_cents($lineAmount);
}
$discountAmountCents = (int) round($totalAmountCents * ($discountPct / 100.0));
$discountAmountCents = min($discountAmountCents, $totalAmountCents);
$finalAmountCents = max(0, $totalAmountCents - $discountAmountCents);
if ($finalAmountCents !== 0) {
// Coupon no longer covers the full amount — redirect to cart
if ($mysqli instanceof mysqli) {
mysqli_close($mysqli);
}
header('Location: /cart.php?msg=coupon_insufficient');
exit;
}
// Process the free checkout
$now = date('Y-m-d H:i:s');
$txid = 'free-' . time() . '-' . $userId;
require_once __DIR__ . '/classes/BillingRepository.php';
require_once __DIR__ . '/classes/BillingService.php';
$repo = new BillingRepository($mysqli, $table_prefix);
$newOrderIds = [];
$duration_meta = static function (array $invoice): array {
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
};
foreach ($invoices as $inv) {
$invoiceId = intval($inv['invoice_id']);
$invoiceBase = round((float)($inv['subtotal'] ?? $inv['total_due'] ?? $inv['amount'] ?? 0), 2);
$orderId = intval($inv['order_id'] ?? 0);
$meta = $duration_meta($inv);
$repo->updateInvoiceFields($invoiceId, [
'order_id' => $orderId,
'coupon_id' => $couponId,
'discount_amount' => $invoiceBase,
'subtotal' => $invoiceBase,
'amount' => 0.00,
'total_due' => 0.00,
'status' => 'paid',
'billing_status' => 'Active',
'payment_status' => 'paid',
'payment_txid' => $txid,
'payment_method' => 'coupon',
'paid_date' => $now,
'invoice_duration' => $meta['invoice_duration'],
'rate_type' => $meta['rate_type'],
]);
$repo->logTransaction([
'invoice_id' => $invoiceId,
'user_id' => $userId,
'home_id' => 0,
'payment_method' => 'coupon',
'transaction_external_id' => $txid,
'amount' => 0.00,
'currency' => 'USD',
'status' => 'completed',
'raw_response' => ['coupon_id' => $couponId, 'discount_pct' => $discountPct, 'original_amount' => (float)($inv['amount'] ?? 0)],
]);
$currentHomeId = 0;
$extendFrom = null;
if ($orderId > 0) {
$order = $repo->getOrder($orderId);
if ($order) {
$currentHomeId = intval($order['home_id'] ?? 0);
$extendFrom = $order['end_date'] ?? null;
}
}
$baseTs = time();
if (!empty($extendFrom)) {
$extendTs = strtotime($extendFrom);
if ($extendTs !== false && $extendTs > time()) {
$baseTs = $extendTs;
}
}
$endDate = date('Y-m-d H:i:s', $baseTs + ($meta['days'] * max(1, intval($inv['qty'] ?? 1)) * 86400));
if ($orderId > 0) {
$repo->updateOrderFields($orderId, [
'status' => 'Active',
'end_date' => $endDate,
'payment_txid' => $txid,
'paid_ts' => $now,
'price' => 0.00,
'discount_amount' => $invoiceBase,
'coupon_id' => $couponId,
]);
if ($currentHomeId > 0) {
$repo->updateInvoiceFields($invoiceId, ['home_id' => $currentHomeId]);
}
if (!in_array($orderId, $newOrderIds, true)) {
$newOrderIds[] = $orderId;
}
} else {
$newOrderId = $repo->createOrder([
'user_id' => intval($inv['user_id']),
'service_id' => intval($inv['service_id']),
'home_name' => $inv['home_name'] ?? '',
'ip' => (string)($inv['ip'] ?? '0'),
'qty' => intval($inv['qty'] ?? 1),
'invoice_duration' => $meta['invoice_duration'],
'max_players' => intval($inv['max_players'] ?? 0),
'price' => 0.00,
'discount_amount' => $invoiceBase,
'remote_control_password' => $inv['remote_control_password'] ?? '',
'ftp_password' => $inv['ftp_password'] ?? '',
'status' => 'Active',
'end_date' => $endDate,
'payment_txid' => $txid,
'paid_ts' => $now,
'coupon_id' => $couponId,
]);
if ($newOrderId > 0) {
$repo->updateInvoiceOrderId($invoiceId, $newOrderId);
$repo->updateInvoiceFields($invoiceId, ['order_id' => $newOrderId]);
if (!in_array($newOrderId, $newOrderIds, true)) {
$newOrderIds[] = $newOrderId;
}
}
}
}
if ($couponId > 0 && !empty($invoices)) {
mysqli_query($mysqli, "UPDATE {$table_prefix}billing_coupons
SET current_uses = current_uses + 1
WHERE coupon_id = " . intval($couponId));
}
// Clear coupon from session
unset($_SESSION['cart_coupon_code'], $_SESSION['cart_coupon_id']);
// Attempt automatic provisioning via panel bridge
$autoProvision = ['provisioned_count' => 0, 'failed_count' => 0, 'details' => [], 'trace_log_path' => 'modules/billing/logs/provisioning_trace.log'];
if (!empty($newOrderIds)) {
require_once __DIR__ . '/includes/panel_bridge.php';
$panelCtx = billing_panel_bootstrap();
if ($panelCtx && isset($panelCtx['db'])) {
$GLOBALS['db'] = $panelCtx['db'];
$GLOBALS['settings'] = $panelCtx['settings'];
require_once __DIR__ . '/create_servers.php';
$autoProvision = billing_invoke_provision(['order_ids' => $newOrderIds, 'user_id' => $userId, 'is_admin' => true]);
} else {
$autoProvision = [
'provisioned_count' => 0,
'failed_count' => count($newOrderIds),
'details' => [],
'trace_log_path' => 'modules/billing/logs/provisioning_trace.log',
'trace_error' => 'Panel bootstrap failed before billing provisioning could start.',
];
}
// If panel bootstrap fails the order is Active and admins can provision via the orders panel.
}
if (function_exists('billing_store_provision_session_result')) {
billing_store_provision_session_result($txid, [
'source' => 'checkout_free.php',
'txid' => $txid,
'order_ids' => $newOrderIds,
'result' => $autoProvision,
]);
}
if ($mysqli instanceof mysqli) {
mysqli_close($mysqli);
}
header('Location: /payment_success.php?order_id=' . urlencode($txid) . '&source=free');
exit;
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('checkout_free.php');

File diff suppressed because it is too large Load diff

View file

@ -1,433 +1,3 @@
<?php
/*
*
* OGP / GSP - Open Game Panel / Game Server Panel
* Copyright (C) 2008 - 2017 The OGP Development Team
*
* http://www.opengamepanel.org/
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*
* BILLING CRON - Three-Status Lifecycle
* ========================================
*
* Operates on server_homes.billing_status (separate from game-server runtime state).
*
* Status values:
* Active - Server is current; no unpaid renewal invoice.
* Invoiced - Renewal invoice generated; payment due.
* Expired - Invoice not paid by due date; server awaiting deletion.
*
* Steps run each night:
* A. Active -> Invoiced : next_invoice_date has arrived -> create {prefix}invoices record.
* B. Invoiced -> Expired : server_expiration_date passed and invoice unpaid.
* C. Expired -> Deleted : past delete_after_expired_days grace window -> remove server.
* D. Paid invoices (safety net): set server and invoice back to Active.
*
* Prerequisites (run once):
* sql/update_billing_status_active_invoiced_expired.sql
*/
chdir(realpath(dirname(__FILE__))); /* Change to the billing module directory */
chdir("../.."); /* Step back to the OGP/GSP web root */
error_reporting(E_ALL);
ini_set('display_errors', '1');
define("CONFIG_FILE", "includes/config.inc.php");
require_once("includes/functions.php");
require_once("includes/helpers.php");
require_once("includes/html_functions.php");
require_once("modules/config_games/server_config_parser.php");
require_once("includes/lib_remote.php");
require_once(CONFIG_FILE);
// Connect using the panel's DB helper (provides $db with logger(), resultQuery(), etc.)
$db = createDatabaseConnection(
$db_type, $db_host, $db_user, $db_pass, $db_name, $table_prefix,
isset($db_port) ? $db_port : null
);
$panel_settings = $db->getSettings();
if (!empty($panel_settings['time_zone'])) {
date_default_timezone_set($panel_settings['time_zone']);
}
$rundate = date('Y-m-d H:i:s');
$db->logger("BILLING-CRON: ===== Lifecycle automation started at {$rundate} =====");
// ----------------------------------------------------------------
// Load global billing config (grace_days, delete_after_expired_days)
// Falls back to safe defaults when {prefix}billing_config is empty.
// ----------------------------------------------------------------
$cfg_rows = $db->resultQuery(
"SELECT * FROM {$table_prefix}billing_config WHERE game_key IS NULL AND enabled = 1 ORDER BY config_id ASC LIMIT 1"
);
$global_cfg = is_array($cfg_rows) && !empty($cfg_rows) ? $cfg_rows[0] : [];
$grace_days = intval($global_cfg['grace_days'] ?? 0);
$delete_after_days = intval($global_cfg['delete_after_expired_days'] ?? 7);
$default_rate_type = $global_cfg['rate_type'] ?? 'monthly';
$default_price_player = floatval($global_cfg['price_per_player'] ?? 0.00);
$db->logger("BILLING-CRON: Config => grace_days={$grace_days}, delete_after={$delete_after_days}, rate={$default_rate_type}");
// ======================================================================
// STEP A - Active -> Invoiced
// Find billing-enabled servers whose next_invoice_date has arrived
// and that do not already have an open 'Invoiced' renewal invoice.
// ======================================================================
$db->logger("BILLING-CRON: --- Step A: Active -> Invoiced ---");
$due_for_invoice = $db->resultQuery("
SELECT sh.home_id, sh.home_name, sh.user_id_main AS user_id,
sh.next_invoice_date, sh.server_expiration_date,
bo.price, bo.invoice_duration, bo.qty, bo.order_id,
COALESCE(bs.price_monthly, 0) AS svc_price_monthly,
u.users_email,
CONCAT(COALESCE(u.users_fname,''), ' ', COALESCE(u.users_lname,'')) AS customer_name
FROM {$table_prefix}server_homes sh
LEFT JOIN {$table_prefix}users u ON u.user_id = sh.user_id_main
LEFT JOIN {$table_prefix}billing_orders bo
ON bo.home_id = sh.home_id AND bo.status = 'Active'
LEFT JOIN {$table_prefix}billing_services bs ON bs.service_id = bo.service_id
WHERE sh.billing_enabled = 1
AND sh.billing_status = 'Active'
AND sh.next_invoice_date IS NOT NULL
AND sh.next_invoice_date <= NOW()
AND NOT EXISTS (
SELECT 1 FROM {$table_prefix}billing_invoices inv
WHERE inv.home_id = sh.home_id AND inv.billing_status = 'Invoiced'
)
ORDER BY sh.home_id ASC
");
if (is_array($due_for_invoice)) {
foreach ($due_for_invoice as $srv) {
$home_id = intval($srv['home_id']);
$user_id = intval($srv['user_id']);
$home_name = $srv['home_name'] ?? 'Server #' . $home_id;
$qty = max(1, intval($srv['qty'] ?? 1));
// Normalise rate_type to the ENUM values used in {prefix}invoices
$raw_rate = strtolower($srv['invoice_duration'] ?? $default_rate_type);
$rate_map = ['day' => 'daily', 'month' => 'monthly', 'year' => 'yearly'];
$rate_type = $rate_map[$raw_rate] ?? $raw_rate;
// Pricing: billing_config > billing_orders flat price
$price_per_player = $default_price_player;
$player_slots = max(0, intval($srv['qty'] ?? 0));
$subtotal = $price_per_player * max(1, $player_slots);
if ($subtotal == 0.00 && floatval($srv['price'] ?? 0) > 0) {
$subtotal = floatval($srv['price']);
}
$total_due = $subtotal;
// Calculate due_date: now + 1 billing period
$period_map = ['daily' => '+1 day', 'monthly' => '+1 month', 'yearly' => '+1 year'];
$due_date_ts = strtotime($period_map[$rate_type], time());
$due_date = date('Y-m-d H:i:s', $due_date_ts);
// Guard: skip if an invoice for this exact period already exists
$exists = $db->resultQuery("
SELECT invoice_id FROM {$table_prefix}billing_invoices
WHERE home_id = {$home_id}
AND due_date = '" . $db->realEscapeSingle($due_date) . "'
LIMIT 1
");
if (is_array($exists) && !empty($exists)) {
$db->logger("BILLING-CRON: Step A - SKIP home {$home_id}: invoice for this period already exists");
continue;
}
// Create renewal invoice in {prefix}billing_invoices
$db->query("
INSERT INTO {$table_prefix}billing_invoices
(home_id, user_id, due_date, billing_status, rate_type,
rate_per_player, players, qty, subtotal, total_due)
VALUES (
{$home_id}, {$user_id},
'" . $db->realEscapeSingle($due_date) . "',
'Invoiced',
'" . $db->realEscapeSingle($rate_type) . "',
" . number_format($price_per_player, 2, '.', '') . ",
{$player_slots},
{$qty},
" . number_format($subtotal, 2, '.', '') . ",
" . number_format($total_due, 2, '.', '') . "
)
");
$new_invoice_id = $db->lastInsertId();
// Update server_homes: set Invoiced, store invoice id and expiration date
$db->query("
UPDATE {$table_prefix}server_homes
SET billing_status = 'Invoiced',
server_expiration_date = '" . $db->realEscapeSingle($due_date) . "',
last_invoice_id = " . intval($new_invoice_id) . "
WHERE home_id = {$home_id}
");
$db->logger("BILLING-CRON: Step A - INVOICED home {$home_id} (invoice #{$new_invoice_id}, due {$due_date})");
// Send renewal notice
if (!empty($srv['users_email'])) {
$settings = $db->getSettings();
$subject = "Renewal Invoice for {$home_name} - " . ($panel_settings['panel_name'] ?? 'Game Server Panel');
$message = "Your server '{$home_name}' (ID: {$home_id}) has a renewal invoice due on "
. date('F j, Y', $due_date_ts) . "."
. "<br><br>Amount Due: \$" . number_format($total_due, 2)
. "<br>Due Date: " . date('F j, Y', $due_date_ts)
. "<br><br>Please log in to pay your invoice and keep your server active."
. "<br><br>Thank you!";
if (!mymail($srv['users_email'], $subject, $message, $settings)) {
$db->logger("BILLING-CRON: Step A - Email FAILED for home {$home_id}");
}
}
}
}
// ======================================================================
// STEP B - Invoiced -> Expired
// Servers whose expiration date has passed and whose last invoice
// is still unpaid.
// ======================================================================
$db->logger("BILLING-CRON: --- Step B: Invoiced -> Expired (grace_days={$grace_days}) ---");
$past_due = $db->resultQuery("
SELECT sh.home_id, sh.home_name, sh.user_id_main AS user_id,
sh.last_invoice_id, sh.server_expiration_date,
u.users_email
FROM {$table_prefix}server_homes sh
LEFT JOIN {$table_prefix}users u ON u.user_id = sh.user_id_main
WHERE sh.billing_enabled = 1
AND sh.billing_status = 'Invoiced'
AND sh.server_expiration_date IS NOT NULL
AND DATE(sh.server_expiration_date) < DATE_SUB(CURDATE(), INTERVAL {$grace_days} DAY)
AND (
sh.last_invoice_id IS NULL
OR EXISTS (
SELECT 1 FROM {$table_prefix}billing_invoices inv
WHERE inv.invoice_id = sh.last_invoice_id
AND inv.billing_status = 'Invoiced'
AND inv.paid_date IS NULL
)
)
ORDER BY sh.home_id ASC
");
if (is_array($past_due)) {
foreach ($past_due as $srv) {
$home_id = intval($srv['home_id']);
$last_invoice_id = intval($srv['last_invoice_id'] ?? 0);
// Mark server Expired
$db->query("
UPDATE {$table_prefix}server_homes
SET billing_status = 'Expired'
WHERE home_id = {$home_id}
");
// Mark matching invoice Expired (if still unpaid)
if ($last_invoice_id > 0) {
$db->query("
UPDATE {$table_prefix}billing_invoices
SET billing_status = 'Expired'
WHERE invoice_id = {$last_invoice_id}
AND billing_status = 'Invoiced'
AND paid_date IS NULL
");
}
$db->logger("BILLING-CRON: Step B - EXPIRED home {$home_id}");
// Notify user
if (!empty($srv['users_email'])) {
$settings = $db->getSettings();
$home_name = $srv['home_name'] ?? 'Server #' . $home_id;
$subject = "Server Expired - {$home_name} - " . ($panel_settings['panel_name'] ?? 'Game Server Panel');
$message = "Your server '{$home_name}' (ID: {$home_id}) has expired due to non-payment."
. "<br><br>The server will be permanently deleted in {$delete_after_days} day(s) if payment is not received."
. "<br><br>Please log in and pay your outstanding invoice to restore service."
. "<br><br>Thank you.";
if (!mymail($srv['users_email'], $subject, $message, $settings)) {
$db->logger("BILLING-CRON: Step B - Email FAILED for home {$home_id}");
}
}
}
}
// ======================================================================
// STEP C - Expired -> Deleted
// Servers that have been Expired longer than delete_after_expired_days.
// ======================================================================
$db->logger("BILLING-CRON: --- Step C: Expired -> Deleted (window={$delete_after_days}d) ---");
$to_delete = $db->resultQuery("
SELECT sh.home_id, sh.home_name, sh.user_id_main AS user_id,
sh.server_expiration_date,
u.users_email
FROM {$table_prefix}server_homes sh
LEFT JOIN {$table_prefix}users u ON u.user_id = sh.user_id_main
WHERE sh.billing_enabled = 1
AND sh.billing_status = 'Expired'
AND sh.server_expiration_date IS NOT NULL
AND DATE(sh.server_expiration_date) < DATE_SUB(CURDATE(), INTERVAL {$delete_after_days} DAY)
ORDER BY sh.home_id ASC
");
if (is_array($to_delete)) {
foreach ($to_delete as $srv) {
$home_id = intval($srv['home_id']);
$user_id = intval($srv['user_id']);
$home_name = $srv['home_name'] ?? 'Server #' . $home_id;
// Fetch home info for remote deletion
$home_info = $db->getGameHomeWithoutMods($home_id);
if ($home_info) {
$server_info = $db->getRemoteServerById($home_info['remote_server_id']);
if ($server_info) {
$remote = new OGPRemoteLibrary(
$server_info['agent_ip'],
$server_info['agent_port'],
$server_info['encryption_key'],
$server_info['timeout']
);
// Stop the running server process
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
$control_type = isset($server_xml->control_protocol_type)
? (string)$server_xml->control_protocol_type : "";
$addresses = $db->getHomeIpPorts($home_id);
foreach ((array)$addresses as $addr) {
$remote->remote_stop_server(
$home_id, $addr['ip'], $addr['port'],
$server_xml->control_protocol,
$home_info['control_password'],
$control_type,
$home_info['home_path']
);
}
// Disable FTP
$ftp_login = !empty($home_info['ftp_login']) ? $home_info['ftp_login'] : $home_id;
$remote->ftp_mgr("userdel", $ftp_login);
$db->changeFtpStatus('disabled', $home_id);
// Unassign from user
$db->unassignHomeFrom("user", $user_id, $home_id);
// Delete home record from panel DB
$db->deleteGameHome($home_id);
// Remove server files on remote agent
$remote->remove_home($home_info['home_path']);
// Drop any per-server database/user accounts
@$db->query("DROP USER 'user_{$home_id}'@'%'");
@$db->query("DROP USER 'user_{$home_id}'@'localhost'");
@$db->query("DROP USER 'server_{$home_id}'@'%'");
@$db->query("DROP USER 'server_{$home_id}'@'localhost'");
@$db->query("DROP DATABASE IF EXISTS user_{$home_id}");
@$db->query("DROP DATABASE IF EXISTS server_{$home_id}");
} else {
$db->logger("BILLING-CRON: Step C - WARNING: no remote server info for home {$home_id}; removing panel record only");
$db->deleteGameHome($home_id);
}
} else {
$db->logger("BILLING-CRON: Step C - WARNING: home {$home_id} not found in panel DB (already removed)");
}
// Mark billing_orders record as Expired and clear home_id reference
$db->query("
UPDATE {$table_prefix}billing_orders
SET status = 'Expired',
home_id = '0'
WHERE home_id = '{$home_id}'
");
// Mark any open billing_invoices for this home as Expired
$db->query("
UPDATE {$table_prefix}billing_invoices
SET billing_status = 'Expired'
WHERE home_id = {$home_id}
AND billing_status = 'Invoiced'
");
$db->logger("BILLING-CRON: Step C - DELETED home {$home_id}");
// Notify user
if (!empty($srv['users_email'])) {
$settings = $db->getSettings();
$subject = "Server Permanently Deleted - {$home_name} - " . ($panel_settings['panel_name'] ?? 'Game Server Panel');
$message = "Your server '{$home_name}' (ID: {$home_id}) has been permanently deleted."
. "<br><br>The server expired and was removed after the grace period."
. "<br><br>If this was an error, contact us immediately - we may be able to restore from backup."
. "<br><br>Thank you for being a customer. We hope to serve you again.";
if (!mymail($srv['users_email'], $subject, $message, $settings)) {
$db->logger("BILLING-CRON: Step C - Email FAILED for home {$home_id}");
}
}
}
}
// ======================================================================
// STEP D - Paid invoice safety net
// If a payment was recorded on a {prefix}invoices row but the
// server_home was not updated (e.g. race condition at capture time),
// correct it here so the server is restored to Active.
// ======================================================================
$db->logger("BILLING-CRON: --- Step D: Paid invoice safety-net ---");
$paid_invoices = $db->resultQuery("
SELECT inv.invoice_id, inv.home_id, inv.rate_type,
sh.billing_status
FROM {$table_prefix}billing_invoices inv
INNER JOIN {$table_prefix}server_homes sh ON sh.home_id = inv.home_id
WHERE inv.billing_status = 'Invoiced'
AND sh.billing_status = 'Invoiced'
AND (inv.paid_date IS NOT NULL OR inv.payment_txid IS NOT NULL)
ORDER BY inv.invoice_id ASC
");
if (is_array($paid_invoices)) {
foreach ($paid_invoices as $inv) {
$home_id = intval($inv['home_id']);
$invoice_id = intval($inv['invoice_id']);
$rate_type = $inv['rate_type'] ?? 'monthly';
// Calculate next_invoice_date based on rate_type
$period_map = ['daily' => '+1 day', 'monthly' => '+1 month', 'yearly' => '+1 year'];
$next_invoice_date = date('Y-m-d H:i:s', strtotime($period_map[$rate_type] ?? '+1 month'));
$db->query("
UPDATE {$table_prefix}billing_invoices
SET billing_status = 'Active'
WHERE invoice_id = {$invoice_id}
");
$db->query("
UPDATE {$table_prefix}server_homes
SET billing_status = 'Active',
next_invoice_date = '" . $db->realEscapeSingle($next_invoice_date) . "',
server_expiration_date = NULL
WHERE home_id = {$home_id}
");
$db->logger("BILLING-CRON: Step D - RESTORED home {$home_id} to Active via paid invoice #{$invoice_id}");
}
}
$db->logger("BILLING-CRON: ===== Lifecycle automation completed at " . date('Y-m-d H:i:s') . " =====");
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('cron-shop.php');

View file

@ -1,431 +1,3 @@
<?php
/**
* Documentation Browser
* Displays a list of documentation categories and allows viewing individual docs
*/
// Start session using the website session name to match the rest of the site
if (session_status() === PHP_SESSION_NONE) {
session_name("opengamepanel_web");
session_start();
}
// Include config
require_once(__DIR__ . '/includes/config_loader.php');
// Set the docs directory
$docsDir = __DIR__ . '/docs';
// Get action and doc parameters
$action = $_GET['action'] ?? 'list';
$doc = $_GET['doc'] ?? '';
$docsPagePath = '/docs.php';
/**
* Get all documentation folders with their metadata
*/
function getDocCategories($docsDir) {
$categories = [];
if (!is_dir($docsDir)) {
return $categories;
}
$folders = array_diff(scandir($docsDir), ['.', '..']);
foreach ((array)$folders as $folder) {
$folderPath = $docsDir . '/' . $folder;
// Skip if not a directory
if (!is_dir($folderPath)) {
continue;
}
// Check for required files
$indexPath = $folderPath . '/index.php';
$metadataPath = $folderPath . '/metadata.json';
if (!file_exists($indexPath) || !file_exists($metadataPath)) {
continue;
}
// Read metadata
$metadataContent = file_get_contents($metadataPath);
// Remove UTF-8 BOM if present
$metadataContent = preg_replace('/^\xEF\xBB\xBF/', '', $metadataContent);
$metadata = json_decode($metadataContent, true);
if (!$metadata) {
$metadata = [];
}
// Get display name (no TODO prefix - just display all docs)
$displayName = $metadata['name'] ?? ucfirst($folder);
// Find icon file
$icon = '';
if (file_exists($folderPath . '/icon.png')) {
$icon = '/docs/' . $folder . '/icon.png';
} elseif (file_exists($folderPath . '/icon.jpg')) {
$icon = '/docs/' . $folder . '/icon.jpg';
}
$categories[] = [
'folder' => $folder,
'name' => $displayName,
'description' => $metadata['description'] ?? '',
'category' => trim($metadata['category'] ?? 'other'),
'order' => $metadata['order'] ?? 999,
'icon' => $icon
];
}
// Sort alphabetically by name within categories
usort($categories, function($a, $b) {
if ($a['category'] !== $b['category']) {
// Keep category grouping (game, mods, other)
return strcmp($a['category'], $b['category']);
}
// Sort alphabetically by name (case-insensitive)
return strcasecmp($a['name'], $b['name']);
});
return $categories;
}
// Get all categories
$categories = getDocCategories($docsDir);
// Group by category
$grouped = [];
foreach ((array)$categories as $cat) {
$category = $cat['category'];
if (!isset($grouped[$category])) {
$grouped[$category] = [];
}
$grouped[$category][] = $cat;
}
// Category labels - can be extended via JSON
$categoryLabels = [
'todo' => 'TODO',
'game' => 'Game Servers',
'mods' => 'Mods & Plugins',
'panel' => 'Panel Documentation',
'troubleshooting' => 'Troubleshooting',
'other' => 'Other'
];
// Define category display order
$categoryOrder = ['todo', 'panel', 'game', 'mods', 'troubleshooting', 'other'];
// Sort categories by defined order
uksort($grouped, function($a, $b) use ($categoryOrder) {
$posA = array_search($a, $categoryOrder);
$posB = array_search($b, $categoryOrder);
// If not in order array, put at end
if ($posA === false) $posA = 999;
if ($posB === false) $posB = 999;
return $posA - $posB;
});
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title><?php echo htmlspecialchars('Documentation - GSP', ENT_QUOTES, 'UTF-8'); ?></title>
<link rel="stylesheet" href="css/header.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
/* Documentation-specific styles - consistent with site theme */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.header {
margin-bottom: 40px;
}
.header h1 {
font-size: 32px;
margin: 0 0 12px;
color: #fff;
}
.header p {
color: rgba(255,255,255,0.7);
margin: 0;
}
.back-button {
display: inline-block;
padding: 10px 20px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
color: #7fb3ff;
text-decoration: none;
margin-bottom: 20px;
transition: all 0.2s;
}
.back-button:hover {
background: rgba(255,255,255,0.06);
border-color: #667eea;
}
.category-section {
margin-bottom: 40px;
}
.category-title {
font-size: 24px;
color: #667eea;
margin: 0 0 20px;
padding-bottom: 10px;
border-bottom: 2px solid rgba(255,255,255,0.1);
}
.docs-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.doc-card {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 20px;
transition: all 0.2s;
text-decoration: none;
display: flex;
flex-direction: column;
}
.doc-card:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.doc-icon-wrapper {
width: 60px;
height: 60px;
background: rgba(0,0,0,0.3);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
}
.doc-icon {
max-width: 100%;
max-height: 100%;
border-radius: 6px;
}
.doc-icon-placeholder {
font-size: 28px;
color: rgba(255,255,255,0.6);
}
.doc-title {
font-size: 18px;
font-weight: 600;
color: #fff;
margin: 0 0 8px;
}
.doc-description {
font-size: 14px;
color: rgba(255,255,255,0.7);
margin: 0;
flex-grow: 1;
}
.doc-view-container {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 30px;
min-height: 400px;
}
.doc-view-container h1,
.doc-view-container h2,
.doc-view-container h3,
.doc-view-container h4 {
color: #fff;
}
.doc-view-container a {
color: #7fb3ff;
}
.doc-view-container code {
background: rgba(0,0,0,0.3);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
color: #a5b4fc;
}
.doc-view-container pre {
background: rgba(0,0,0,0.3);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
}
.doc-view-container pre code {
background: none;
padding: 0;
}
.nav-links {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.nav-links h3 {
margin: 0 0 15px;
color: #667eea;
font-size: 18px;
}
.nav-links a {
display: inline-block;
padding: 8px 15px;
margin: 5px 10px 5px 0;
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 5px;
color: #7fb3ff;
text-decoration: none;
transition: all 0.2s;
}
.nav-links a:hover {
background: #667eea;
color: #fff;
border-color: #667eea;
}
.return-to-top {
text-align: center;
margin: 30px 0;
}
.return-to-top a {
display: inline-block;
padding: 10px 20px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
color: #7fb3ff;
text-decoration: none;
transition: all 0.2s;
}
.return-to-top a:hover {
background: rgba(255,255,255,0.06);
border-color: #667eea;
}
</style>
</head>
<body>
<?php include(__DIR__ . '/includes/menu.php'); ?>
<div class="container">
<?php if ($action === 'view' && !empty($doc)): ?>
<!-- View specific documentation -->
<a href="<?php echo htmlspecialchars($docsPagePath, ENT_QUOTES, 'UTF-8'); ?>" class="back-button"> Back to Documentation List</a>
<div class="doc-view-container">
<?php
// Sanitize doc parameter to prevent directory traversal
$doc = basename($doc);
$docPath = $docsDir . '/' . $doc . '/index.php';
if (file_exists($docPath)) {
include($docPath);
} else {
echo '<p style="color: #ef4444;">Documentation not found.</p>';
}
?>
</div>
<?php else: ?>
<!-- List all documentation categories -->
<div class="header">
<h1 id="top">Documentation</h1>
<p>Browse our comprehensive documentation for game servers, panel features, and troubleshooting guides.</p>
</div>
<?php if (empty($grouped)): ?>
<div class="doc-view-container">
<p>No documentation available yet. Documentation folders should contain:</p>
<ul>
<li><code>index.php</code> - The documentation content</li>
<li><code>metadata.json</code> - Category and ordering information</li>
<li><code>icon.png</code> or <code>icon.jpg</code> - Category icon</li>
</ul>
</div>
<?php else: ?>
<!-- Navigation Links -->
<div class="nav-links">
<h3>Jump to Section:</h3>
<?php foreach ((array)$grouped as $category => $docs): ?>
<a href="#<?php echo htmlspecialchars($category); ?>">
<?php echo htmlspecialchars($categoryLabels[$category] ?? ucfirst($category)); ?>
(<?php echo count((array)$docs); ?>)
</a>
<?php endforeach; ?>
</div>
<?php foreach ((array)$grouped as $category => $docs): ?>
<div class="category-section" id="<?php echo htmlspecialchars($category); ?>">
<h2 class="category-title"><?php echo htmlspecialchars($categoryLabels[$category] ?? ucfirst($category)); ?></h2>
<div class="docs-grid">
<?php foreach ((array)$docs as $doc): ?>
<a href="<?php echo htmlspecialchars($docsPagePath . '?action=view&doc=' . urlencode($doc['folder']), ENT_QUOTES, 'UTF-8'); ?>" class="doc-card">
<div class="doc-icon-wrapper">
<?php if (!empty($doc['icon'])): ?>
<img src="<?php echo htmlspecialchars($doc['icon']); ?>" alt="" class="doc-icon">
<?php else: ?>
<span class="doc-icon-placeholder">📄</span>
<?php endif; ?>
</div>
<h3 class="doc-title"><?php echo htmlspecialchars($doc['name']); ?></h3>
<?php if (!empty($doc['description'])): ?>
<p class="doc-description"><?php echo htmlspecialchars($doc['description']); ?></p>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
<div class="return-to-top">
<a href="#top"> Return to Top</a>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php endif; ?>
</div>
</body>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('docs.php');

View file

@ -1,98 +1,3 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GameServers.World - Virtual Private Gameservers</title>
<style>
.gsw-outer-full{box-sizing:border-box;width:100vw!important;margin-left:calc(50% - 50vw)!important;margin-right:calc(50% - 50vw)!important}
.gsw-page-center{display:flex;justify-content:center;padding:24px 12px}
.gsw-wrap{width:min(95vw,1100px);margin:0 auto;line-height:1.55}
.gsw-hero{display:grid;gap:10px;margin-bottom:18px;text-align:center;justify-items:center}
.gsw-hero h1{margin:0;font-size:2rem;letter-spacing:.2px}
.gsw-hero p{margin:0;font-size:1.05rem}
.gsw-badge{display:inline-block;margin-top:6px;padding:6px 10px;border:1px solid;border-radius:999px;font-weight:600;font-size:.92rem}
.gsw-callout{margin:14px 0 4px;padding:12px 14px;border:1px dashed;border-radius:10px;text-align:center;font-size:1rem}
.gsw-locations{margin:12px 0 20px}
.gsw-locations h2{margin:0 0 10px;font-size:1.15rem;text-transform:uppercase;letter-spacing:.6px}
.gsw-locations-list{display:grid;gap:8px;grid-template-columns:1fr;list-style:disc;margin:0;padding-left:22px}
@media(min-width:680px){.gsw-locations-list{grid-template-columns:repeat(2,1fr)}}
.gsw-grid{display:grid;gap:14px;grid-template-columns:1fr}
@media(min-width:720px){.gsw-grid{grid-template-columns:repeat(3,1fr)}}
.gsw-card{padding:16px;border:1px solid;border-radius:10px}
.gsw-card h3{margin:0 0 6px;font-size:1.1rem}
.gsw-card p{margin:0}
.gsw-cta{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px;justify-content:center}
.gsw-btn{border:1px solid;border-radius:8px;padding:10px 14px;text-decoration:none;display:inline-block;font-weight:600}
.gsw-fine{font-size:.92rem;opacity:.9;text-align:center;margin-top:10px}
</style>
</head>
<body>
<?php include(__DIR__ . '/includes/top.php'); ?>
<?php include(__DIR__ . '/includes/menu.php'); ?>
<!-- Page banner -->
<div class="center mb-18">
<img src="images/banner.png" alt="Banner" class="gsw-banner">
</div>
<div class="gsw-outer-full">
<div class="gsw-page-center">
<section class="gsw-wrap" aria-label="GameServers.World">
<header class="gsw-hero">
<h1>Virtual Private Gameservers</h1>
<p>Just like running on your own dedicated box <strong>full configurability</strong> with <strong>help when you need it</strong>.</p>
<span class="gsw-badge" aria-label="Never oversold">Never Oversold Capacity</span>
<p class="muted mt-6">We also specialize in classics <strong>50+ older/community-favorite games</strong> hosted right.</p>
</header>
<div class="gsw-callout" role="note">
Your server gets the resources its promised. No cramming, no noisy neighbors predictable performance for your game and mods.
</div>
<section class="gsw-locations" aria-label="Current locations">
<h2>Current Locations</h2>
<ul class="gsw-locations-list">
<li><strong>Los Angeles, USA</strong> la-game-1.iaregamer.com</li>
<li><strong>Kansas City, USA</strong> kc-game-2.iaregamer.com</li>
<li><strong>Dallas, USA</strong> dal-game-1.iaregamer.com</li>
<li><strong>New York City, USA</strong> nyc-game-1.iaregamer.com</li>
<li><strong>Dublin, Ireland</strong> dub-game-1.iaregamer.com</li>
</ul>
</section>
<section class="gsw-grid" aria-label="Highlights">
<article class="gsw-card">
<h3>Built for the classics</h3>
<p>Low-latency routing and high-clock CPUs keep legacy engines smooth. We support favorites like CS&nbsp;1.6, Urban Terror, DayZ Mod and dozens more.</p>
</article>
<article class="gsw-card">
<h3>Simple, affordable plans</h3>
<p>Clear options, month-to-month flexibility, and room to scale as your community grows.</p>
</article>
<article class="gsw-card">
<h3>Real humans, fast setup</h3>
<p>Well help with configs, common mods, and a clean starter rotation so you can go live quickly.</p>
</article>
</section>
<nav class="gsw-cta" aria-label="Primary actions">
<a class="gsw-btn" href="serverlist.php">Browse Game Servers</a>
<a class="gsw-btn" href="contact/">Talk to Support</a>
</nav>
<p class="gsw-fine">Looking for a specific title or region? Tell us what you need we add games and locations regularly.</p>
</section>
</div>
</div>
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>
<?php
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('index.php');

View file

@ -1,401 +1,3 @@
<?php
// Start a separate session for the website (not the panel session)
session_name("opengamepanel_web");
session_start();
// We'll compute a site root below (up to /_website) and define a strict sanitizer after config is loaded
// Include billing bootstrap (loads database configuration)
require_once(__DIR__ . '/bootstrap.php');
require_once(__DIR__ . '/includes/log.php');
// Variables from config.inc.php (helps IDEs understand scope)
/** @var string $db_host Database host */
/** @var string $db_user Database user */
/** @var string $db_pass Database password */
/** @var string $db_name Database name */
/** @var string $table_prefix Table prefix for database tables */
// Determine site root (directory of this script) for building absolute redirects within this site
$script = $_SERVER['SCRIPT_NAME'] ?? '';
$SITE_ROOT_PATH = rtrim(dirname($script), '/\\');
// Strict sanitizer that returns an absolute path under $SITE_ROOT_PATH or empty string on invalid
$sanitize_return_path = function($p) use ($SITE_ROOT_PATH) {
$p = trim((string)$p);
if ($p === '') return '';
// disallow absolute URLs or protocol-relative paths
if (preg_match('#^(https?:)?//#i', $p)) return '';
if (strpos($p, "\n") !== false || strpos($p, "\r") !== false) return '';
// Reject path traversal
if (strpos($p, '..') !== false) return '';
// Normalize: if it starts with '/', treat as absolute path and ensure it's under SITE_ROOT_PATH
if (substr($p,0,1) === '/') {
// simple character whitelist
if (!preg_match('#^/[A-Za-z0-9_./?&=%:\-]+$#', $p)) return '';
// Disallow entry to 'dashboard' (panel area) explicitly
if (stripos($p, '/dashboard') !== false) return '';
return $p;
}
// Relative path: restrict characters and build absolute under site root
if (!preg_match('#^[A-Za-z0-9_./?&=%:\-]+$#', $p)) return '';
// Disallow references to panel dashboard
if (stripos($p, 'dashboard') !== false) return '';
return $SITE_ROOT_PATH . '/' . ltrim($p, '/');
};
// Create database connection
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$db) {
die("Connection failed: " . mysqli_connect_error());
}
// Logger function
function logger($logtext){
file_put_contents(__DIR__ . "/logfile.txt", $logtext . PHP_EOL, FILE_APPEND);
}
// If user already has a website session, redirect to index.php (no return_to)
if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) {
header('Location: ' . $SITE_ROOT_PATH . '/index.php');
exit();
}
// Initialize error message
$error_message = '';
$success_message = '';
$debug_messages = [];
// Process login form submission: simplified for debugging
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['ulogin'] ?? '');
$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'] ?? '']);
$debug_messages[] = 'missing username or password';
} else {
$safe = mysqli_real_escape_string($db, $username);
$sql = "SELECT * 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) {
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();
}
}
$error_message = 'Invalid username or password.';
site_log_warn('login_failed_invalid_credentials', ['username'=>$username, 'ip'=>$_SERVER['REMOTE_ADDR'] ?? '']);
}
}
// Keep DB connection open for includes (menu.php may query the DB). The
// connection lifecycle is handled centrally; avoid closing here.
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - GameServers.World</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: radial-gradient(circle at top, #1f3551 0%, #0f1724 58%, #0b111b 100%);
min-height: 100vh;
display: block;
margin: 0;
padding: 0;
}
.content{
display:flex;
align-items:center;
justify-content:center;
min-height: calc(100vh - 220px);
padding: 24px 16px 32px;
}
.login-container {
background: #f8fbff;
border-radius: 12px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
width: 100%;
max-width: 420px;
padding: 32px 28px;
border: 1px solid rgba(40,70,110,0.25);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
font-size: 1.8rem;
color: #111111; /* high contrast */
margin-bottom: 8px;
}
.login-header p {
color: #4b5563; /* neutral dark gray */
font-size: 0.95rem;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #111111 !important; /* ensure labels are dark and readable (override external styles) */
font-weight: 600;
font-size: 0.95rem;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.15s, box-shadow 0.15s;
color: #111111;
background-color: #ffffff;
-webkit-appearance: none;
}
.form-group input:focus {
outline: none;
border-color: #4f46e5;
box-shadow: 0 6px 20px rgba(79,70,229,0.12);
}
.btn-login {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #3168a4 0%, #214978 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(38, 84, 136, 0.4);
}
.btn-login:active {
transform: translateY(0);
}
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 0.95rem;
}
.alert-error {
background-color: #fee;
border: 1px solid #fcc;
color: #c33;
}
.alert-success {
background-color: #efe;
border: 1px solid #cfc;
color: #3c3;
}
.footer-links {
margin-top: 24px;
text-align: center;
word-break: break-word;
}
.footer-links a {
color: #3168a4;
text-decoration: none;
font-size: 0.9rem;
}
.footer-links a:hover {
text-decoration: underline;
}
.divider {
margin: 20px 0;
text-align: center;
color: #999;
font-size: 0.85rem;
}
.login-links {
margin-top: 12px;
text-align: center;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 6px 10px;
line-height: 1.5;
}
.login-links a {
color: #3168a4;
text-decoration: none;
}
.login-links a:hover {
text-decoration: underline;
}
@media (max-width: 600px) {
.content {
min-height: auto;
padding: 14px 10px 20px;
}
.login-container {
max-width: 100%;
padding: 20px 16px;
border-radius: 10px;
}
.login-header {
margin-bottom: 18px;
}
.login-header h1 {
font-size: 1.35rem;
}
.login-header p,
.form-group label,
.form-group input,
.btn-login {
font-size: 0.95rem;
}
.form-group {
margin-bottom: 14px;
}
.form-group input {
padding: 10px 12px;
}
.btn-login {
padding: 11px 12px;
}
}
</style>
</head>
<body>
<?php include(__DIR__ . '/includes/top.php'); ?>
<?php include(__DIR__ . '/includes/menu.php'); ?>
<div class="content">
<div class="login-container">
<div class="login-header">
<h1>Welcome Back</h1>
<p>Sign in to your GameServers account</p>
</div>
<?php if (!empty($error_message)): ?>
<div class="alert alert-error"><?php echo htmlspecialchars($error_message); ?></div>
<?php endif; ?>
<?php if (!empty($success_message)): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($success_message); ?></div>
<?php endif; ?>
<?php
// Capture a return_to GET parameter so we can send users back after login
$return_to_raw = $_GET['return_to'] ?? '';
// ensure we don't break if not set; the sanitizer is defined above
?>
<form method="POST" action="login.php">
<input type="hidden" name="return_to" value="<?php echo htmlspecialchars($return_to_raw); ?>">
<div class="form-group">
<label>Username</label>
<input type="text" id="ulogin" name="ulogin" required autofocus>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="upassword" name="upassword" required>
</div>
<button type="submit" name="login" class="btn-login">Sign In</button>
</form>
<div class="login-links">
<a href="register.php">Register</a>
<span aria-hidden="true">|</span>
<a href="forgot_password.php">Forgot Password?</a>
</div>
<div class="divider">or</div>
<div class="footer-links">
<a href="index.php">Back to Home</a> |
<a href="../index.php">Panel Login</a>
</div>
</div>
</div>
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('login.php');

View file

@ -1,398 +1,3 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Account - GameServers.World</title>
</head>
<body>
<?php
// Start session to check login status
if (session_status() === PHP_SESSION_NONE) {
session_name("opengamepanel_web");
session_start();
}
// Enable error display during debugging so runtime errors show in the page
@ini_set('display_errors', 1);
@ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Check if user is logged in
$is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) || (isset($_SESSION['website_username']) && !empty($_SESSION['website_username']));
// If not logged in, show login page instead
if (!$is_logged_in) {
include(__DIR__ . '/login.php');
exit;
}
// Include database configuration
require_once(__DIR__ . '/bootstrap.php');
// Variables from config.inc.php (helps IDEs understand scope)
/** @var string $db_host Database host */
/** @var string $db_user Database user */
/** @var string $db_pass Database password */
/** @var string $db_name Database name */
/** @var string $table_prefix Table prefix for database tables */
// Create database connection
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$db) {
die("Connection failed: " . mysqli_connect_error());
}
// Include top bar and menu
include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php');
// (debug markers removed)
// Initialize messages
$error_message = '';
$success_message = '';
// Get user ID from session
$user_id = intval($_SESSION['website_user_id'] ?? 0);
// Fetch user information from database
$user_info = null;
if ($user_id > 0) {
$query = "SELECT user_id, users_login, users_email, users_fname, users_lname FROM {$table_prefix}users WHERE user_id = $user_id LIMIT 1";
$result = mysqli_query($db, $query);
if ($result && mysqli_num_rows($result) === 1) {
$user_info = mysqli_fetch_assoc($result);
}
}
// Handle password change
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['change_password'])) {
$current_password = $_POST['current_password'] ?? '';
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
if (empty($current_password) || empty($new_password) || empty($confirm_password)) {
$error_message = 'All password fields are required.';
} elseif ($new_password !== $confirm_password) {
$error_message = 'New passwords do not match.';
} elseif (strlen($new_password) < 6) {
$error_message = 'New password must be at least 6 characters long.';
} else {
// Verify current password (using MD5 as per panel legacy)
$current_hash = md5($current_password);
$verify_query = "SELECT user_id FROM {$table_prefix}users WHERE user_id = $user_id AND users_passwd = '$current_hash' LIMIT 1";
$verify_result = mysqli_query($db, $verify_query);
if ($verify_result && mysqli_num_rows($verify_result) === 1) {
// Update password
$new_hash = md5($new_password);
$update_query = "UPDATE {$table_prefix}users SET users_passwd = '$new_hash' WHERE user_id = $user_id LIMIT 1";
if (mysqli_query($db, $update_query)) {
$success_message = 'Password changed successfully!';
} else {
$error_message = 'Failed to update password. Please try again.';
}
} else {
$error_message = 'Current password is incorrect.';
}
}
}
// Handle account info update
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_info'])) {
$fname = mysqli_real_escape_string($db, trim($_POST['fname'] ?? ''));
$lname = mysqli_real_escape_string($db, trim($_POST['lname'] ?? ''));
$email = mysqli_real_escape_string($db, trim($_POST['email'] ?? ''));
if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error_message = 'Invalid email address.';
} else {
$update_query = "UPDATE {$table_prefix}users SET users_fname = '$fname', users_lname = '$lname', users_email = '$email' WHERE user_id = $user_id LIMIT 1";
if (mysqli_query($db, $update_query)) {
$success_message = 'Account information updated successfully!';
// Refresh user info
$query = "SELECT user_id, users_login, users_email, users_fname, users_lname FROM {$table_prefix}users WHERE user_id = $user_id LIMIT 1";
$result = mysqli_query($db, $query);
if ($result && mysqli_num_rows($result) === 1) {
$user_info = mysqli_fetch_assoc($result);
}
} else {
$error_message = 'Failed to update account information. Please try again.';
}
}
}
// Fetch user's orders from billing_orders. Keep this simple: select orders for the user and join service name.
// Avoid joins to remote server fields that do not exist on the orders table.
$servers_query = "SELECT
o.order_id,
o.home_name,
o.status,
o.price,
o.invoice_duration,
o.home_id,
o.end_date,
bs.service_name
FROM {$table_prefix}billing_orders o
LEFT JOIN {$table_prefix}billing_services bs ON o.service_id = bs.service_id
WHERE o.user_id = $user_id
ORDER BY o.order_id DESC";
$servers_result = mysqli_query($db, $servers_query);
// Debug: Log query execution and errors
if (!$servers_result) {
error_log("My Account Error - User ID: $user_id, Query failed: " . mysqli_error($db));
} else {
error_log("My Account Debug - User ID: $user_id, Servers Found: " . mysqli_num_rows($servers_result));
}
// Fetch invoices (from data directory JSON files)
$dataDir = (isset($SITE_DATA_DIR) && $SITE_DATA_DIR) ? $SITE_DATA_DIR : realpath(__DIR__ . '/') . DIRECTORY_SEPARATOR . 'data';
$invoices = [];
if (is_dir($dataDir)) {
foreach (glob($dataDir . '/*.json') as $file) {
$j = json_decode(file_get_contents($file), true);
if (!$j || !is_array($j)) continue;
// Try to match by user email or user_id in custom field
$match = false;
if ($user_info && !empty($user_info['users_email'])) {
if (!empty($j['payer']) && stripos($j['payer'], $user_info['users_email']) !== false) $match = true;
if (!$match && !empty($j['custom']) && stripos($j['custom'], $user_info['users_email']) !== false) $match = true;
}
if ($match) {
$invoices[] = $j;
}
}
}
// Sort invoices by invoice/order id (newest order id first) when available,
// otherwise fall back to timestamp (newest first).
usort($invoices, function($a, $b) {
$getOrderId = function($inv) {
if (!empty($inv['invoice']) && is_numeric($inv['invoice'])) return intval($inv['invoice']);
if (!empty($inv['custom']) && is_numeric($inv['custom'])) return intval($inv['custom']);
return null;
};
$aId = $getOrderId($a);
$bId = $getOrderId($b);
if ($aId !== null || $bId !== null) {
// If either has a numeric order id, prefer numeric comparison (desc)
if ($aId === $bId) {
return strtotime($b['ts'] ?? 0) - strtotime($a['ts'] ?? 0);
}
if ($aId === null) return 1; // b has id -> b before a
if ($bId === null) return -1; // a has id -> a before b
return $bId - $aId; // numeric desc
}
// Fallback: newest timestamp first
return strtotime($b['ts'] ?? 0) - strtotime($a['ts'] ?? 0);
});
// Organize invoices by status
$invoices_by_status = [];
foreach ((array)$invoices as $inv) {
$status = strtolower($inv['status'] ?? 'pending');
if (!isset($invoices_by_status[$status])) {
$invoices_by_status[$status] = [];
}
$invoices_by_status[$status][] = $inv;
}
// Define status display order and labels
$status_config = [
'pending' => ['label' => 'Pending Invoices', 'class' => 'pending'],
'paid' => ['label' => 'Paid Invoices', 'class' => 'paid'],
'completed' => ['label' => 'Completed Invoices', 'class' => 'paid'],
'in-cart' => ['label' => 'In Cart', 'class' => 'pending'],
'installed' => ['label' => 'Active', 'class' => 'paid'],
'expired' => ['label' => 'Expired Invoices', 'class' => 'expired'],
'cancelled' => ['label' => 'Cancelled Invoices', 'class' => 'expired'],
];
?>
<!-- Debug marker: Page rendering started -->
<div class="site-panel">
<div class="site-panel-title">
My Account
<a href="logout.php" class="gsw-btn" style="float:right;">Logout</a>
</div>
<!-- Debug: User ID = <?php echo $user_id; ?>, User Info: <?php echo $user_info ? 'Loaded' : 'NULL'; ?> -->
<?php if (!empty($error_message)): ?>
<div class="alert alert-error"><?php echo htmlspecialchars($error_message); ?></div>
<?php endif; ?>
<?php if (!empty($success_message)): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($success_message); ?></div>
<?php endif; ?>
<!-- Account Information Section -->
<div class="account-section">
<h2>Account Information</h2>
<?php if ($user_info): ?>
<div class="account-info-grid">
<div class="account-info-item">
<div class="account-info-label">Username</div>
<div class="account-info-value"><?php echo htmlspecialchars($user_info['users_login'] ?? 'N/A'); ?></div>
</div>
<div class="account-info-item">
<div class="account-info-label">Email</div>
<div class="account-info-value"><?php echo htmlspecialchars($user_info['users_email'] ?? 'N/A'); ?></div>
</div>
<div class="account-info-item">
<div class="account-info-label">First Name</div>
<div class="account-info-value"><?php echo htmlspecialchars($user_info['users_fname'] ?? 'N/A'); ?></div>
</div>
<div class="account-info-item">
<div class="account-info-label">Last Name</div>
<div class="account-info-value"><?php echo htmlspecialchars($user_info['users_lname'] ?? 'N/A'); ?></div>
</div>
</div>
<!-- Edit Account Information Form -->
<details>
<summary class="account-edit-summary">Edit Account Information</summary>
<form method="POST" class="mt-12">
<div class="form-group">
<label for="fname">First Name</label>
<input type="text" id="fname" name="fname" value="<?php echo htmlspecialchars($user_info['users_fname'] ?? ''); ?>">
</div>
<div class="form-group">
<label for="lname">Last Name</label>
<input type="text" id="lname" name="lname" value="<?php echo htmlspecialchars($user_info['users_lname'] ?? ''); ?>">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" value="<?php echo htmlspecialchars($user_info['users_email'] ?? ''); ?>" required>
</div>
<button type="submit" name="update_info" class="btn-primary">Update Information</button>
</form>
</details>
<?php else: ?>
<div class="no-data">Unable to load account information.</div>
<?php endif; ?>
</div>
<!-- Change Password Section -->
<div class="account-section">
<h2>Change Password</h2>
<form method="POST">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" required>
</div>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" required>
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" name="change_password" class="btn-primary">Change Password</button>
</form>
</div>
<!-- My Game Servers Section -->
<div class="account-section">
<h2>My Game Servers</h2>
<?php if ($servers_result && mysqli_num_rows($servers_result) > 0): ?>
<?php while ($server = mysqli_fetch_assoc($servers_result)): ?>
<div class="server-item">
<div class="server-name"><?php echo htmlspecialchars($server['home_name'] ?? 'Unnamed Server'); ?></div>
<div class="server-details">
<div class="server-detail">
<span class="server-detail-label">Game:</span>
<span class="server-detail-value"><?php echo htmlspecialchars($server['service_name'] ?? 'N/A'); ?></span>
</div>
<div class="server-detail">
<span class="server-detail-label">Location / Home ID:</span>
<span class="server-detail-value"><?php echo htmlspecialchars($server['remote_server_name'] ?? $server['home_id'] ?? 'N/A'); ?></span>
</div>
<div class="server-detail">
<span class="server-detail-label">Status:</span>
<span class="server-detail-value"><?php echo htmlspecialchars(ucfirst($server['status'] ?? 'pending')); ?></span>
</div>
<div class="server-detail">
<span class="server-detail-label">Price:</span>
<span class="server-detail-value">$<?php echo number_format($server['price'] ?? 0, 2); ?>/<?php echo htmlspecialchars($server['invoice_duration'] ?? 'month'); ?></span>
</div>
<div class="server-detail">
<span class="server-detail-label">Order ID:</span>
<span class="server-detail-value">#<?php echo htmlspecialchars($server['order_id'] ?? 'N/A'); ?></span>
</div>
<div class="server-detail">
<span class="server-detail-label">Expires:</span>
<span class="server-detail-value"><?php echo !empty($server['end_date']) && $server['end_date'] != '0' ? date('M d, Y', strtotime($server['end_date'])) : 'N/A'; ?></span>
</div>
</div>
<div class="server-actions">
<?php
// Show Renew action for servers that can be renewed
// Status comparison is case-insensitive (strtolower). Canonical
// values are 'Active' and 'Invoiced'; legacy values are included
// as a fallback until normalize_billing_order_status.sql has run.
$renewable_statuses = array('active','invoiced','paid','installed','suspended');
if (!empty($server['status']) && in_array(strtolower($server['status']), $renewable_statuses)): ?>
<a href="renew_server.php?order_id=<?php echo intval($server['order_id']); ?>" class="gsw-btn renew-btn">Renew</a>
<?php endif; ?>
</div>
</div>
<?php endwhile; ?>
<?php else: ?>
<div class="no-data">
<p>You don't have any game servers yet.</p>
<a href="serverlist.php" class="gsw-btn mt-10">Browse Game Servers</a>
</div>
<?php endif; ?>
</div>
<!-- Invoices Section - Organized by Status -->
<?php if (!empty($invoices_by_status)): ?>
<?php foreach ((array)$status_config as $status_key => $status_info): ?>
<?php if (isset($invoices_by_status[$status_key]) && !empty($invoices_by_status[$status_key])): ?>
<div class="account-section">
<h2><?php echo htmlspecialchars($status_info['label']); ?></h2>
<?php foreach ((array)$invoices_by_status[$status_key] as $invoice): ?>
<div class="invoice-item">
<div>
<div class="invoice-id">Invoice #<?php echo htmlspecialchars($invoice['invoice'] ?? $invoice['custom'] ?? 'N/A'); ?></div>
<div class="invoice-date"><?php echo htmlspecialchars($invoice['ts'] ?? 'N/A'); ?></div>
</div>
<div>
<span class="invoice-amount"><?php echo htmlspecialchars(($invoice['currency'] ?? 'USD') . ' ' . number_format((float)($invoice['amount'] ?? 0), 2)); ?></span>
<span class="invoice-status invoice-status-<?php echo htmlspecialchars($status_info['class']); ?>">
<?php echo htmlspecialchars(ucfirst($status_key)); ?>
</span>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
<?php else: ?>
<div class="account-section">
<h2>Invoices</h2>
<div class="no-data">No invoices found.</div>
</div>
<?php endif; ?>
</div>
<?php
// Close database connection
billing_maybe_close_db($db);
?>
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('my_account.php');

View file

@ -1,439 +1,3 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Order Server - GameServers.World</title>
<link rel="stylesheet" href="css/header.css">
<style>
body { margin: 0; padding: 0; }
.order-shell { width: min(1100px, 100% - 24px); margin: 20px auto 28px; }
.order-catalog-item { margin: 0 0 20px; }
.order-layout { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; }
.order-media { flex: 0 1 310px; max-width: 100%; }
.order-media img { width: 100%; max-width: 280px; height: auto; border-radius: 8px; display: block; }
.order-media-title { margin: 10px 0 6px; text-align: center; }
.order-media-desc { color: #c6c6c6; max-width: 100%; word-break: break-word; }
.order-form-card { flex: 1 1 500px; max-width: 100%; background: rgba(0,0,0,0.25); border-radius: 10px; padding: 14px; }
.order-form-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.order-form-table td { padding: 8px 6px; vertical-align: top; }
.order-form-table td:first-child { width: 34%; }
.order-form-table input[type="text"],
.order-form-table input[type="number"],
.order-form-table select,
.order-form-table textarea {
width: 100%;
min-height: 40px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.2);
padding: 8px 10px;
}
.order-form-table input[type="radio"] { margin-right: 8px; }
.location-option { margin-bottom: 8px; }
.slidecontainer { max-width: 100%; }
.slidecontainer .slider { width: 100%; }
.order-pricing { line-height: 1.5; word-break: break-word; }
.order-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.order-actions .gsw-btn,
.order-actions .gsw-btn-secondary { width: auto; }
.order-back-form { margin: 0; }
@media (max-width: 768px) {
.order-shell { width: min(100%, calc(100% - 16px)); margin: 12px auto 20px; }
.order-layout { gap: 12px; }
.order-media { flex: 1 1 100%; display: flex; flex-direction: column; align-items: center; }
.order-media img { max-width: min(100%, 260px); }
.order-form-card { flex: 1 1 100%; padding: 10px; }
.order-form-table,
.order-form-table tbody,
.order-form-table tr,
.order-form-table td { display: block; width: 100%; }
.order-form-table td:first-child { width: 100%; padding-bottom: 2px; }
.order-form-table td { padding: 6px 4px; }
.order-actions { flex-direction: column; }
.order-actions .gsw-btn,
.order-actions .gsw-btn-secondary { width: 100%; text-align: center; }
}
</style>
</head>
<body>
<?php
/*
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.
Each enabled billing service row is listed and purchased as its own exact variant.
The selected service_id remains the source of truth for checkout and provisioning.
*/
// Require login for ordering
require_once(__DIR__ . '/includes/login_required.php');
// Include billing bootstrap (loads config and DB helper)
require_once(__DIR__ . '/bootstrap.php');
// Variables from config.inc.php (helps IDEs understand scope)
/** @var string $db_host Database host */
/** @var string $db_user Database user */
/** @var string $db_pass Database password */
/** @var string $db_name Database name */
/** @var string $table_prefix Table prefix for database tables */
// Create database connection
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$db) {
die("Connection failed: " . mysqli_connect_error());
}
// Include top bar and menu
include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php');
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();
}
}
function order_price_is_free($value): bool
{
return ((int) round(((float)$value) * 100)) === 0;
}
function order_detect_service_os(string $cfgFile, string $gameKey): string
{
$haystack = strtolower(trim($cfgFile !== '' ? $cfgFile : $gameKey));
if ($haystack === '') {
return 'any';
}
if (preg_match('/(?:^|[_\\-])(win|windows)(?:[_\\-]|$)/i', $haystack)) {
return 'windows';
}
if (preg_match('/(?:^|[_\\-])linux(?:[_\\-]|$)/i', $haystack)) {
return 'linux';
}
return 'any';
}
function order_variant_label(string $serviceOs): string
{
if ($serviceOs === 'windows') {
return 'Windows';
}
if ($serviceOs === 'linux') {
return 'Linux';
}
return '';
}
// --- Fetch the requested service with config_homes join for canonical game info ---
$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 {
$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, ch.home_cfg_file AS cfg_file
FROM {$table_prefix}billing_services bs
LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id
{$where_service_id}
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, NULL AS cfg_file
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();
}
$order_error_message = isset($_GET['error_message']) ? trim((string)$_GET['error_message']) : '';
?>
<div class="order-shell">
<div class="clearfix">
<?php
foreach ($serviceRows as $row)
{
if (!isset($_REQUEST['service_id']))
{
?>
<div class="float-left p-30-20 order-catalog-item">
<?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 (order_price_is_free($row['price_monthly'] ?? 0)) {
echo "FREE";
} else {
echo "$" . number_format(floatval($row['price_monthly']), 2) . " Monthly";
}
?>
<br>
<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
{
// Determine exact selected service display and OS label from config metadata.
$svcGameKey = (string)($row['cfg_game_key'] ?? '');
$cfgFile = (string)($row['cfg_file'] ?? '');
$svcGameOs = order_detect_service_os($cfgFile, $svcGameKey);
$canonicalGameName = (string)($row['cfg_game_name'] ?? $row['service_name']);
$variantLabel = order_variant_label($svcGameOs);
$displayName = $canonicalGameName;
if ($variantLabel !== '' && stripos($displayName, $variantLabel) === false) {
$displayName .= ' - ' . $variantLabel;
}
?>
<div class="order-layout">
<div class="order-media 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'); ?>" alt="<?php echo htmlspecialchars($displayName, ENT_QUOTES, 'UTF-8'); ?>"
onerror="this.src='/images/games/default_server.png'; this.onerror=null;">
<center class="order-media-title"><b><?php echo htmlspecialchars($displayName, 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 class='order-media-desc'>" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "</p>";
}
?>
</div>
<div class="order-form-card">
<?php if ($order_error_message !== ''): ?>
<p class="error"><?php echo htmlspecialchars($order_error_message, ENT_QUOTES, 'UTF-8'); ?></p>
<?php endif; ?>
<table class="order-form-table">
<form method="post" action="add_to_cart.php">
<input type="hidden" id="order_service_id" name="service_id" value="<?php echo intval($row['service_id']); ?>">
<input type="hidden" name="display_service_id" value="<?php echo intval($row['service_id']); ?>">
<input type="hidden" name="remote_control_password" value="">
<input type="hidden" name="ftp_password" value="">
<input type="hidden" name="display_rate" id="displayRateInput" value="<?php echo number_format(floatval($row['price_monthly']), 2, '.', ''); ?>">
<input type="hidden" name="calculated_total" id="calculatedTotalInput" 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 exact selected service from billing_services.remote_server_id.
$available_server = false;
$remoteIdsCsv = (string)($row['remote_server_id'] ?? '');
$allAllowedIds = [];
foreach (explode(',', $remoteIdsCsv) as $part) {
$part = trim($part);
if ($part !== '' && ctype_digit($part)) {
$allAllowedIds[] = (int)$part;
}
}
$allAllowedIds = array_unique($allAllowedIds);
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');
$rsOsRaw = strtolower((string)($rs['server_os'] ?? 'any'));
$rsOs = str_starts_with($rsOsRaw, 'win') ? 'windows' : (str_starts_with($rsOsRaw, 'lin') ? 'linux' : ($rsOsRaw === '' ? 'any' : $rsOsRaw));
$checked = $firstServer ? ' checked' : '';
if ($svcGameOs !== 'any' && $rsOs !== 'any' && $rsOs !== $svcGameOs) {
continue;
}
$available_server = true;
$firstServer = false;
$safeOs = htmlspecialchars($rsOs, ENT_QUOTES, 'UTF-8');
echo "<div class='location-option'>\n"
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' data-os='{$safeOs}' required{$checked}>\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">
<p class="order-pricing">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>
(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, '.', ''); ?>;
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" : "");
var total = (slots * months * pricePerSlot).toFixed(2);
price.innerHTML = "Total Price: $" + total;
var totalInput = document.getElementById("calculatedTotalInput");
if (totalInput) {
totalInput.value = total;
}
}
recalc();
slider.oninput = recalc;
invoiceslider.oninput = recalc;
})();
</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): ?>
<div class="order-actions">
<button type="submit" name="add_to_cart" class="gsw-btn">Add to Cart</button>
</div>
<?php elseif (!$is_logged_in): ?>
<div class="login-placeholder">Please <a href="login.php">login</a> to order</div>
<?php else: ?>
<p class="error">
<?php
if ($svcGameOs === 'windows') {
echo 'This service requires a Windows server location.';
} elseif ($svcGameOs === 'linux') {
echo 'This service requires a Linux server location.';
} else {
echo 'No available server locations for this service.';
}
?>
</p>
<?php endif; ?>
</form>
</td>
</tr>
<tr>
<td align="left" colspan="2">
<form action="serverlist.php" method="GET" class="order-back-form">
<div class="order-actions">
<button class="gsw-btn-secondary">Back to List</button>
</div>
</form>
</td>
</tr>
</table>
</div>
</div>
<?php
}
}
?>
</div>
</div>
<?php
// Close database connection
billing_maybe_close_db($db);
?>
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('order.php');

View file

@ -1,51 +1,3 @@
<?php
/**
* Payment Cancelled Page
* User lands here if they cancel the PayPal payment
*/
session_start();
require_once(__DIR__ . '/includes/header.php');
require_once(__DIR__ . '/includes/config_loader.php');
$invoice_ref = isset($_GET['invoice']) ? $_GET['invoice'] : '';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Cancelled - Game Server Panel</title>
<link rel="stylesheet" href="includes/style.css">
</head>
<body>
<div class="container" style="max-width: 800px; margin: 40px auto; padding: 20px;">
<div class="warning-box" style="background: #fff3cd; border: 1px solid #ffeaa7; color: #856404; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
<h1 style="margin-top: 0;">Payment Cancelled</h1>
<p>Your payment was cancelled. No charges have been made to your account.</p>
<?php if ($invoice_ref): ?>
<p><strong>Invoice Reference:</strong> <?php echo htmlspecialchars($invoice_ref); ?></p>
<?php endif; ?>
</div>
<div class="info-box" style="background: #f8f9fa; border: 1px solid #dee2e6; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
<h2>What would you like to do?</h2>
<ul>
<li><strong>Return to Cart:</strong> Your items are still in your cart. You can complete the payment anytime.</li>
<li><strong>Continue Shopping:</strong> Browse our game server options and add more to your cart.</li>
<li><strong>Need Help?:</strong> Contact our support team if you encountered any issues during checkout.</li>
</ul>
</div>
<div class="actions" style="margin-top: 30px; text-align: center;">
<a href="cart.php" style="display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;">Return to Cart</a>
<a href="order.php" style="display: inline-block; padding: 12px 24px; background: #28a745; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;">Continue Shopping</a>
<a href="support.php" style="display: inline-block; padding: 12px 24px; background: #6c757d; color: white; text-decoration: none; border-radius: 5px;">Contact Support</a>
</div>
</div>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</body>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('payment_cancel.php');

View file

@ -1,360 +1,3 @@
<?php
/**
* Payment Success Page
* Shows order confirmation after successful PayPal payment
* Standalone billing module - uses only standard PHP mysqli
*/
if (session_status() === PHP_SESSION_NONE) {
session_name('opengamepanel_web');
session_start();
}
require_once(__DIR__ . '/includes/config_loader.php');
// Variables from config.inc.php (helps IDEs understand scope)
/** @var string $db_host Database host */
/** @var string $db_user Database user */
/** @var string $db_pass Database password */
/** @var string $db_name Database name */
/** @var string $table_prefix Table prefix for database tables */
// Get PayPal order ID from URL
$paypal_order_id = isset($_GET['order_id']) ? trim($_GET['order_id']) : '';
$success_source = isset($_GET['source']) ? trim($_GET['source']) : '';
// Get user ID from session
$user_id = isset($_SESSION['website_user_id']) ? intval($_SESSION['website_user_id']) :
(isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0);
$is_admin_viewer = strtolower((string)($_SESSION['users_group'] ?? '')) === 'admin';
$provision_summary = $_SESSION['billing_provision_results'][$paypal_order_id] ?? null;
// Connect to database
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
$orders = [];
$total_paid = 0;
function billing_payment_success_provision_state(array $order): array
{
$homeId = intval($order['home_id'] ?? 0);
$hasHome = intval($order['has_home'] ?? 0) > 0;
$ipPortCount = intval($order['ip_port_count'] ?? 0);
$modCount = intval($order['mod_count'] ?? 0);
if ($homeId <= 0) {
return ['label' => 'PENDING', 'message' => 'Server record is queued for provisioning.', 'class' => 'status-badge status-pending'];
}
// home_id exists but server_homes row does not: orphaned consistency failure.
if (!$hasHome) {
return ['label' => 'FAILED', 'message' => 'Server setup failed. Please contact support with your order ID.', 'class' => 'status-badge status-failed'];
}
if ($ipPortCount <= 0 || $modCount <= 0) {
return ['label' => 'PENDING', 'message' => 'Server created; install is pending final IP/mod setup.', 'class' => 'status-badge status-pending'];
}
return ['label' => 'INSTALL STARTED', 'message' => 'Server created and install/update trigger has been started or queued.', 'class' => 'status-badge'];
}
function billing_payment_success_banner(array $summary, array $orders): array
{
if (!empty($summary['result']['failed_count'])) {
return ['class' => 'status-failed', 'message' => 'Provisioning failed; support has been notified.'];
}
foreach ($orders as $order) {
if (($order['provision_state']['label'] ?? '') === 'FAILED') {
return ['class' => 'status-failed', 'message' => 'Provisioning failed; support has been notified.'];
}
}
return ['class' => 'status-pending', 'message' => 'Your server is being installed.'];
}
if ($db && $user_id > 0) {
// Get recent orders for this user (just paid)
$query = "SELECT o.*, s.service_name,
CASE WHEN sh.home_id IS NULL THEN 0 ELSE 1 END AS has_home,
(SELECT COUNT(*) FROM {$table_prefix}home_ip_ports hip WHERE hip.home_id = o.home_id) AS ip_port_count,
(SELECT COUNT(*) FROM {$table_prefix}game_mods gm WHERE gm.home_id = o.home_id) AS mod_count
FROM {$table_prefix}billing_orders o
LEFT JOIN {$table_prefix}billing_services s ON o.service_id = s.service_id
LEFT JOIN {$table_prefix}server_homes sh ON sh.home_id = o.home_id
WHERE o.user_id = $user_id
AND o.status = 'Active'
ORDER BY o.order_date DESC
LIMIT 10";
$result = mysqli_query($db, $query);
if ($result) {
while ($row = mysqli_fetch_assoc($result)) {
$row['provision_state'] = billing_payment_success_provision_state($row);
$orders[] = $row;
$total_paid += floatval($row['price']);
}
}
mysqli_close($db);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Successful - Game Server Panel</title>
<style>
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
margin: 0;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.success-header {
text-align: center;
padding: 30px 0;
border-bottom: 2px solid #28a745;
margin-bottom: 30px;
}
.success-icon {
font-size: 64px;
color: #28a745;
margin-bottom: 20px;
}
h1 {
color: #28a745;
margin: 0 0 10px 0;
}
.subtitle {
color: #666;
font-size: 1.1em;
}
.info-box {
background: #e7f3ff;
border-left: 4px solid #007bff;
padding: 20px;
margin: 30px 0;
border-radius: 4px;
}
.info-box h3 {
margin-top: 0;
color: #007bff;
}
.info-box ul {
margin: 10px 0;
padding-left: 20px;
}
.info-box li {
margin: 8px 0;
line-height: 1.6;
}
.orders-table {
width: 100%;
border-collapse: collapse;
margin: 30px 0;
}
.orders-table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
}
.orders-table td {
padding: 15px 12px;
border-bottom: 1px solid #dee2e6;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85em;
font-weight: 600;
background: #28a745;
color: white;
}
.status-pending {
background: #f0ad4e;
}
.status-failed {
background: #d9534f;
}
.provision-banner {
margin: 20px 0 0;
padding: 14px 18px;
border-radius: 6px;
color: #fff;
font-weight: 600;
}
.provision-debug {
margin-top: 20px;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 14px 16px;
background: #f8f9fa;
}
.provision-debug summary {
cursor: pointer;
font-weight: 600;
}
.provision-debug-table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
}
.provision-debug-table th,
.provision-debug-table td {
border-bottom: 1px solid #dee2e6;
padding: 8px 6px;
text-align: left;
font-size: 0.92em;
vertical-align: top;
}
.btn {
display: inline-block;
padding: 12px 24px;
margin: 10px 10px 10px 0;
background: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
font-weight: 600;
}
.btn:hover {
background: #0056b3;
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #545b62;
}
.actions {
text-align: center;
margin-top: 40px;
padding-top: 30px;
border-top: 2px solid #dee2e6;
}
</style>
</head>
<body>
<?php $banner = billing_payment_success_banner((array)$provision_summary, $orders); ?>
<div class="container">
<div class="success-header">
<div class="success-icon"></div>
<h1>Payment Successful!</h1>
<p class="subtitle">Your payment has been processed successfully</p>
<?php if ($paypal_order_id): ?>
<p style="color: #999; font-size: 0.9em; margin-top: 10px;">
Transaction ID: <?php echo htmlspecialchars($paypal_order_id); ?>
</p>
<?php endif; ?>
</div>
<div class="info-box">
<h3>What Happens Next?</h3>
<ul>
<li><strong> Payment Confirmed:</strong> Your payment has been captured by PayPal</li>
<li><strong>⚙️ Server Provisioning:</strong> Your game server(s) are queued for automatic install now; if a node is unavailable they remain clearly marked as pending install</li>
<li><strong>📧 Email Notification:</strong> You'll receive a confirmation email with your order details</li>
<li><strong>🎮 Access Your Servers:</strong> Log into the Game Server Panel to manage your new servers</li>
</ul>
<div class="provision-banner <?php echo htmlspecialchars($banner['class']); ?>">
<?php echo htmlspecialchars($banner['message']); ?>
</div>
<?php if ($is_admin_viewer && !empty($provision_summary['result'])): ?>
<details class="provision-debug" <?php echo !empty($provision_summary['result']['failed_count']) ? 'open' : ''; ?>>
<summary>Provisioning Debug Summary</summary>
<p><strong>Provisioning started:</strong> <?php echo !empty($provision_summary['order_ids']) ? 'yes' : 'no'; ?></p>
<p><strong>Provisioning succeeded:</strong> <?php echo intval($provision_summary['result']['failed_count'] ?? 0) === 0 ? 'yes' : 'no'; ?></p>
<p><strong>Provisioning failed:</strong> <?php echo intval($provision_summary['result']['failed_count'] ?? 0) > 0 ? 'yes' : 'no'; ?></p>
<p><strong>Log file path:</strong> <?php echo htmlspecialchars($provision_summary['result']['trace_log_path'] ?? 'modules/billing/logs/provisioning_trace.log'); ?></p>
<?php if (!empty($provision_summary['result']['trace_error'])): ?>
<p><strong>Trace error:</strong> <?php echo htmlspecialchars($provision_summary['result']['trace_error']); ?></p>
<?php endif; ?>
<?php if (!empty($provision_summary['result']['details']) && is_array($provision_summary['result']['details'])): ?>
<table class="provision-debug-table">
<thead>
<tr>
<th>Order</th>
<th>Status</th>
<th>Home ID</th>
<th>Mod ID</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<?php foreach ($provision_summary['result']['details'] as $detail): ?>
<tr>
<td>#<?php echo htmlspecialchars((string)($detail['order_id'] ?? '0')); ?></td>
<td><?php echo htmlspecialchars((string)($detail['install_result'] ?? 'pending')); ?></td>
<td><?php echo htmlspecialchars((string)($detail['home_id'] ?? '0')); ?></td>
<td><?php echo htmlspecialchars((string)($detail['mod_id'] ?? '0')); ?></td>
<td><?php echo htmlspecialchars((string)($detail['error'] ?? $detail['install_message'] ?? '')); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</details>
<?php elseif ($success_source === 'free' && !empty($provision_summary['result'])): ?>
<div style="margin-top:14px;color:#555;font-size:0.95em;">
Free checkout completed. <?php echo htmlspecialchars($banner['message']); ?>
</div>
<?php endif; ?>
</div>
<?php if (count((array)$orders) > 0): ?>
<h2>Your Orders</h2>
<table class="orders-table">
<thead>
<tr>
<th>Order ID</th>
<th>Server Name</th>
<th>Game</th>
<th>Duration</th>
<th>Status</th>
<th title="Server Setup Status" aria-label="Server Setup Status">Provisioning</th>
<th style="text-align: right;">Price</th>
</tr>
</thead>
<tbody>
<?php foreach ((array)$orders as $order): ?>
<tr>
<td>#<?php echo htmlspecialchars($order['order_id']); ?></td>
<td><?php echo htmlspecialchars($order['home_name']); ?></td>
<td><?php echo htmlspecialchars($order['service_name'] ?? 'Game Server'); ?></td>
<td><?php echo htmlspecialchars($order['qty']); ?>x <?php echo htmlspecialchars($order['invoice_duration']); ?></td>
<td><span class="status-badge">PAID</span></td>
<td>
<span class="<?php echo htmlspecialchars($order['provision_state']['class'] ?? 'status-badge'); ?>">
<?php echo htmlspecialchars($order['provision_state']['label'] ?? 'PENDING'); ?>
</span>
<div style="margin-top:6px;color:#555;font-size:0.9em;">
<?php echo htmlspecialchars($order['provision_state']['message'] ?? 'Provisioning state unavailable.'); ?>
</div>
</td>
<td style="text-align: right; font-weight: 600; color: #28a745;">
$<?php echo number_format(floatval($order['price']), 2); ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="info-box" style="background: #fff3cd; border-left-color: #856404;">
<p><strong>Note:</strong> Your orders are being processed. If you don't see them listed above, please log into your account or contact support.</p>
</div>
<?php endif; ?>
<div class="actions">
<a href="/my_account.php" class="btn">View My Account</a>
<a href="/order.php" class="btn btn-secondary">Order Another Server</a>
<a href="/index.php" class="btn btn-secondary">Back to Home</a>
</div>
</div>
</body>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('payment_success.php');

View file

@ -1,752 +1,3 @@
<?php
/**
* GSP PayPal Webhook Receiver
*
* Public URL: $SITE_BASE_URL + $paypal_webhook_path (e.g. https://gameservers.world/paypal/webhook.php)
*
* This endpoint:
* 1. Reads raw POST JSON from PayPal
* 2. Verifies the webhook signature using PayPal's verify-webhook-signature API
* 3. Checks for duplicate events (idempotency) via billing_paypal_webhook_events table
* 4. Processes supported event types and updates billing_orders / triggers provisioning
* 5. Returns appropriate HTTP status codes
*
* HTTP status codes returned:
* 200 success (or duplicate event safely ignored)
* 400 missing / invalid JSON body
* 401 PayPal signature verification failed or OAuth failed
* 500 internal error (DB unavailable, etc.)
*/
ini_set('display_errors', '0');
error_reporting(E_ALL);
// ---------------------------------------------------------------------------
// Bootstrap: load config and DB
// ---------------------------------------------------------------------------
$_billing_dir = dirname(__DIR__);
require_once $_billing_dir . '/includes/config_loader.php';
// Log helper — writes to logs/paypal_webhook.log; never logs secrets.
$_wh_log_file = $_billing_dir . '/logs/paypal_webhook.log';
@mkdir(dirname($_wh_log_file), 0755, true);
function wh_log(string $level, string $message, array $context = []): void
{
global $_wh_log_file;
$ctx = empty($context) ? '' : ' ' . json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$line = '[' . date('c') . '] [' . strtoupper($level) . '] ' . $message . $ctx . "\n";
@file_put_contents($_wh_log_file, $line, FILE_APPEND | LOCK_EX);
}
// ---------------------------------------------------------------------------
// 1. Read raw input
// ---------------------------------------------------------------------------
$raw = (string)file_get_contents('php://input');
$headers = array_change_key_case((array)(getallheaders() ?: []), CASE_UPPER);
wh_log('info', 'webhook_received', ['ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'bytes' => strlen($raw)]);
if ($raw === '') {
wh_log('warn', 'empty_body');
http_response_code(400);
echo json_encode(['error' => 'empty_body']);
exit;
}
$evt = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($evt)) {
wh_log('warn', 'invalid_json', ['json_error' => json_last_error_msg()]);
http_response_code(400);
echo json_encode(['error' => 'invalid_json']);
exit;
}
// ---------------------------------------------------------------------------
// 2. DB connection (needed for idempotency log and order updates)
// ---------------------------------------------------------------------------
$db_port_int = intval($db_port ?? 3306) ?: 3306;
$wh_db = @mysqli_connect($db_host ?? 'localhost', $db_user ?? '', $db_pass ?? '', $db_name ?? '', $db_port_int);
if (!$wh_db) {
wh_log('error', 'db_connect_failed', ['error' => mysqli_connect_error()]);
http_response_code(500);
echo json_encode(['error' => 'db_unavailable']);
exit;
}
mysqli_set_charset($wh_db, 'utf8mb4');
$pfx = $table_prefix ?? 'gsp_';
// ---------------------------------------------------------------------------
// 2a. Ensure the webhook event log table exists (idempotent DDL)
// ---------------------------------------------------------------------------
wh_ensure_event_table($wh_db, $pfx);
// ---------------------------------------------------------------------------
// 3. PayPal OAuth token
// ---------------------------------------------------------------------------
$api_base = gsp_paypal_get_api_base();
$client_id = gsp_paypal_get_client_id();
$client_secret = gsp_paypal_get_client_secret();
$webhook_id = gsp_paypal_get_webhook_id();
if (empty($client_id) || empty($client_secret)) {
wh_log('error', 'paypal_not_configured');
http_response_code(500);
echo json_encode(['error' => 'paypal_not_configured']);
mysqli_close($wh_db);
exit;
}
$access_token = wh_get_access_token($api_base, $client_id, $client_secret);
if (!$access_token) {
wh_log('warn', 'oauth_failed');
http_response_code(401);
echo json_encode(['error' => 'oauth_failed']);
mysqli_close($wh_db);
exit;
}
// ---------------------------------------------------------------------------
// 4. Verify webhook signature (skip only if webhook_id is empty)
// ---------------------------------------------------------------------------
if (!empty($webhook_id)) {
$verified = wh_verify_signature($api_base, $access_token, $webhook_id, $headers, $evt);
if (!$verified) {
wh_log('warn', 'signature_invalid', [
'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '',
'event_type' => $evt['event_type'] ?? '',
]);
http_response_code(401);
echo json_encode(['error' => 'signature_invalid']);
mysqli_close($wh_db);
exit;
}
wh_log('info', 'signature_ok');
} else {
wh_log('warn', 'signature_skipped_no_webhook_id');
}
// ---------------------------------------------------------------------------
// 5. Idempotency check
// ---------------------------------------------------------------------------
$paypal_event_id = $evt['id'] ?? '';
$event_type = $evt['event_type'] ?? '';
$resource = $evt['resource'] ?? [];
if ($paypal_event_id !== '') {
$existing = wh_get_event($wh_db, $pfx, $paypal_event_id);
if ($existing && $existing['processing_status'] === 'processed') {
wh_log('info', 'duplicate_event_ignored', ['paypal_event_id' => $paypal_event_id, 'event_type' => $event_type]);
http_response_code(200);
echo json_encode(['status' => 'duplicate_ignored']);
mysqli_close($wh_db);
exit;
}
}
// Log the event as received (upsert — so retries update the record)
$log_id = wh_log_event($wh_db, $pfx, [
'paypal_event_id' => $paypal_event_id,
'event_type' => $event_type,
'resource_id' => $resource['id'] ?? '',
'order_id' => '',
'capture_id' => '',
'billing_order_id' => 0,
'processing_status' => 'received',
'raw_json' => $raw,
]);
// ---------------------------------------------------------------------------
// 6. Process event
// ---------------------------------------------------------------------------
$result = wh_process_event($wh_db, $pfx, $event_type, $resource, $evt, $access_token, $api_base, $raw, $_billing_dir);
// Update log entry with final status
if ($log_id > 0) {
wh_update_event_status($wh_db, $pfx, $log_id, $result['status'], $result['billing_order_id'] ?? 0);
}
wh_log('info', 'event_processed', [
'event_type' => $event_type,
'status' => $result['status'],
'billing_order_id' => $result['billing_order_id'] ?? 0,
]);
http_response_code(200);
echo json_encode(['status' => $result['status']]);
mysqli_close($wh_db);
exit;
// ============================================================================
// Helper functions
// ============================================================================
/**
* Get OAuth access token from PayPal.
*/
function wh_get_access_token(string $api_base, string $client_id, string $client_secret): ?string
{
$ch = curl_init($api_base . '/v1/oauth2/token');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => 'grant_type=client_credentials',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERPWD => $client_id . ':' . $client_secret,
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200 || !$body) {
return null;
}
$data = json_decode($body, true);
return $data['access_token'] ?? null;
}
/**
* Verify PayPal webhook signature.
* Returns true only when PayPal confirms verification_status = SUCCESS.
*/
function wh_verify_signature(
string $api_base,
string $access_token,
string $webhook_id,
array $headers,
array $evt
): bool {
$payload = [
'auth_algo' => $headers['PAYPAL-AUTH-ALGO'] ?? '',
'cert_url' => $headers['PAYPAL-CERT-URL'] ?? '',
'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '',
'transmission_sig' => $headers['PAYPAL-TRANSMISSION-SIG'] ?? '',
'transmission_time'=> $headers['PAYPAL-TRANSMISSION-TIME'] ?? '',
'webhook_id' => $webhook_id,
'webhook_event' => $evt,
];
$ch = curl_init($api_base . '/v1/notifications/verify-webhook-signature');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $access_token,
],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200 || !$resp) {
return false;
}
$data = json_decode($resp, true);
return ($data['verification_status'] ?? '') === 'SUCCESS';
}
/**
* Process a single webhook event. Returns ['status' => string, 'billing_order_id' => int].
*/
function wh_process_event(
mysqli $db,
string $pfx,
string $event_type,
array $resource,
array $evt,
string $access_token,
string $api_base,
string $raw_json,
string $billing_dir = ''
): array {
switch ($event_type) {
case 'CHECKOUT.ORDER.APPROVED':
return wh_handle_order_approved($db, $pfx, $resource, $evt);
case 'PAYMENT.CAPTURE.COMPLETED':
case 'PAYMENT.SALE.COMPLETED':
return wh_handle_capture_completed($db, $pfx, $resource, $evt, $access_token, $api_base, $billing_dir);
case 'PAYMENT.CAPTURE.DENIED':
case 'PAYMENT.SALE.DENIED':
return wh_handle_capture_denied($db, $pfx, $resource, $evt);
case 'PAYMENT.CAPTURE.REFUNDED':
case 'PAYMENT.SALE.REFUNDED':
return wh_handle_capture_refunded($db, $pfx, $resource, $evt);
default:
wh_log('info', 'unhandled_event_type', ['event_type' => $event_type]);
return ['status' => 'ignored_unhandled', 'billing_order_id' => 0];
}
}
/**
* CHECKOUT.ORDER.APPROVED buyer approved the order but capture not yet done.
* We log this for auditing; the actual fulfillment happens on CAPTURE.COMPLETED.
*/
function wh_handle_order_approved(mysqli $db, string $pfx, array $resource, array $evt): array
{
$paypal_order_id = $resource['id'] ?? ($evt['resource']['id'] ?? '');
wh_log('info', 'order_approved', ['paypal_order_id' => $paypal_order_id]);
return ['status' => 'approved_logged', 'billing_order_id' => 0];
}
/**
* PAYMENT.CAPTURE.COMPLETED payment fully captured; provision the server.
*/
function wh_handle_capture_completed(
mysqli $db,
string $pfx,
array $resource,
array $evt,
string $access_token,
string $api_base,
string $billing_dir = ''
): array {
$capture_id = $resource['id'] ?? '';
$amount = $resource['amount']['value'] ?? null;
$currency = $resource['amount']['currency_code'] ?? 'USD';
// Extract PayPal order ID from supplementary_data or links
$paypal_order_id = $resource['supplementary_data']['related_ids']['order_id'] ?? '';
if (empty($paypal_order_id) && isset($resource['links']) && is_array($resource['links'])) {
foreach ($resource['links'] as $lnk) {
if (!empty($lnk['href']) && stripos($lnk['href'], '/v2/checkout/orders/') !== false) {
$paypal_order_id = basename(parse_url($lnk['href'], PHP_URL_PATH));
break;
}
}
}
// Extract invoice/custom from resource or fetch the full order
$invoice_ref = $resource['invoice_id'] ?? ($resource['invoice_number'] ?? null);
$custom_id = $resource['custom_id'] ?? ($resource['custom'] ?? null);
// If we have a PayPal order ID, fetch the order to get invoice/custom IDs
if (!empty($paypal_order_id) && (empty($invoice_ref) || empty($custom_id))) {
$order_data = wh_fetch_paypal_order($api_base, $access_token, $paypal_order_id);
if ($order_data) {
$pu = $order_data['purchase_units'][0] ?? [];
if (empty($invoice_ref)) $invoice_ref = $pu['invoice_id'] ?? null;
if (empty($custom_id)) $custom_id = $pu['custom_id'] ?? null;
}
}
wh_log('info', 'capture_completed', [
'capture_id' => $capture_id,
'paypal_order_id' => $paypal_order_id,
'invoice_ref' => $invoice_ref,
'custom_id' => $custom_id,
'amount' => $amount,
]);
// Find matching billing invoice(s) and process payment
$billing_order_id = wh_fulfill_payment($db, $pfx, [
'capture_id' => $capture_id,
'paypal_order_id' => $paypal_order_id,
'invoice_ref' => $invoice_ref,
'custom_id' => $custom_id,
'amount' => $amount,
'currency' => $currency,
], $billing_dir);
return ['status' => 'processed', 'billing_order_id' => $billing_order_id];
}
/**
* PAYMENT.CAPTURE.DENIED capture was denied (e.g. failed fraud check).
*/
function wh_handle_capture_denied(mysqli $db, string $pfx, array $resource, array $evt): array
{
$capture_id = $resource['id'] ?? '';
wh_log('warn', 'capture_denied', ['capture_id' => $capture_id]);
// Find the billing order for this capture and mark it denied, if still pending
if ($capture_id !== '') {
$esc = mysqli_real_escape_string($db, $capture_id);
$sql = "UPDATE `{$pfx}billing_orders`
SET status = 'payment_denied'
WHERE payment_txid = '{$esc}'
AND status NOT IN ('Active','cancelled')
LIMIT 1";
mysqli_query($db, $sql);
}
return ['status' => 'denied_logged', 'billing_order_id' => 0];
}
/**
* PAYMENT.CAPTURE.REFUNDED payment was refunded.
*/
function wh_handle_capture_refunded(mysqli $db, string $pfx, array $resource, array $evt): array
{
$refund_id = $resource['id'] ?? '';
$capture_id = $resource['links'] ? (function () use ($resource) {
foreach ($resource['links'] as $l) {
if (($l['rel'] ?? '') === 'up' && stripos($l['href'] ?? '', '/captures/') !== false) {
return basename(parse_url($l['href'], PHP_URL_PATH));
}
}
return '';
})() : '';
wh_log('info', 'capture_refunded', ['refund_id' => $refund_id, 'capture_id' => $capture_id]);
// Log the refund; do not automatically cancel the server unless the billing lifecycle supports it.
return ['status' => 'refunded_logged', 'billing_order_id' => 0];
}
/**
* Fetch a PayPal order by ID. Returns decoded array or null.
*/
function wh_fetch_paypal_order(string $api_base, string $access_token, string $order_id): ?array
{
$ch = curl_init($api_base . '/v2/checkout/orders/' . urlencode($order_id));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $access_token,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200 || !$body) {
wh_log('warn', 'order_fetch_failed', ['order_id' => $order_id, 'http' => $code]);
return null;
}
$data = json_decode($body, true);
return is_array($data) ? $data : null;
}
/**
* Match the PayPal capture to a billing invoice, mark it paid, create/extend billing_orders,
* and trigger server provisioning. Returns the billing_order_id or 0.
*/
function wh_invoice_ids_from_custom_id($custom_id): array
{
if (!is_string($custom_id) || $custom_id === '') {
return [];
}
if (ctype_digit($custom_id)) {
return [intval($custom_id)];
}
if (stripos($custom_id, 'cart:') !== 0) {
return [];
}
$invoice_ids = [];
foreach (explode(',', substr($custom_id, 5)) as $part) {
$part = trim($part);
if ($part !== '' && ctype_digit($part)) {
$invoice_ids[] = intval($part);
}
}
return array_values(array_unique($invoice_ids));
}
function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $billing_dir = ''): int
{
$txid = $payment['capture_id'] ?? '';
$custom_id = $payment['custom_id'] ?? null;
$invoice_ref = $payment['invoice_ref'] ?? null;
$amount = isset($payment['amount']) ? floatval($payment['amount']) : null;
$now = date('Y-m-d H:i:s');
$esc_txid = mysqli_real_escape_string($db, (string)$txid);
// Find matching invoices
$invoices = [];
// 1) Match by numeric custom_id (which we set to invoice_id when creating the PayPal order)
$custom_invoice_ids = wh_invoice_ids_from_custom_id($custom_id);
if (!empty($custom_invoice_ids)) {
$id_list = implode(',', array_map('intval', $custom_invoice_ids));
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE invoice_id IN ({$id_list}) AND status = 'due' ORDER BY invoice_id ASC");
if ($res) {
while ($row = mysqli_fetch_assoc($res)) {
$invoices[] = $row;
}
}
}
elseif (!empty($custom_id) && ctype_digit((string)$custom_id)) {
$inv_id = intval($custom_id);
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE invoice_id = {$inv_id} AND status = 'due' LIMIT 1");
if ($res && $row = mysqli_fetch_assoc($res)) {
$invoices[] = $row;
}
}
// 2) Match by invoice reference in description
if (empty($invoices) && !empty($invoice_ref)) {
$esc_ref = mysqli_real_escape_string($db, (string)$invoice_ref);
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE status = 'due' AND description LIKE '%{$esc_ref}%'");
if ($res) {
while ($row = mysqli_fetch_assoc($res)) {
$invoices[] = $row;
}
}
}
// 3) Fallback: match by exact amount
if (empty($invoices) && $amount !== null) {
$esc_amount = number_format($amount, 2, '.', '');
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE status = 'due' AND amount = {$esc_amount}");
if ($res) {
while ($row = mysqli_fetch_assoc($res)) {
$invoices[] = $row;
}
}
}
if (empty($invoices)) {
wh_log('warn', 'no_matching_invoices', ['custom_id' => $custom_id, 'invoice_ref' => $invoice_ref, 'amount' => $amount]);
return 0;
}
$last_order_id = 0;
$applied_coupon_id = 0;
foreach ($invoices as $inv) {
$invoice_id = intval($inv['invoice_id']);
$order_id = intval($inv['order_id'] ?? 0);
$user_id = intval($inv['user_id']);
$service_id = intval($inv['service_id'] ?? 0);
$duration = $inv['invoice_duration'] ?? 'month';
$qty = max(1, intval($inv['qty'] ?? 1));
// Mark invoice paid
$stmt = mysqli_prepare($db, "UPDATE `{$pfx}billing_invoices` SET status='paid', payment_status='paid', paid_date=?, payment_txid=?, payment_method='paypal' WHERE invoice_id=? LIMIT 1");
if ($stmt) {
mysqli_stmt_bind_param($stmt, 'ssi', $now, $esc_txid, $invoice_id);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
}
// Increment coupon usage if applicable
$coupon_id = intval($inv['coupon_id'] ?? 0);
if ($coupon_id > 0) {
$applied_coupon_id = $coupon_id;
}
$duration_days = 31 * $qty;
if (stripos($duration, 'day') !== false) {
$duration_days = $qty;
} elseif (stripos($duration, 'year') !== false) {
$duration_days = 365 * $qty;
}
if ($order_id > 0) {
// Renewal: extend existing order
$res = mysqli_query($db, "SELECT end_date, home_id FROM `{$pfx}billing_orders` WHERE order_id = {$order_id} LIMIT 1");
if ($res && $row = mysqli_fetch_assoc($res)) {
$current_end = $row['end_date'] ?? $now;
$extend_from = (strtotime($current_end) > time()) ? $current_end : $now;
$dt = new DateTime($extend_from);
$dt->modify('+' . $duration_days . ' days');
$new_end = $dt->format('Y-m-d H:i:s');
$stmt = mysqli_prepare($db, "UPDATE `{$pfx}billing_orders` SET end_date=?, status='Active', payment_txid=?, paid_ts=? WHERE order_id=? LIMIT 1");
if ($stmt) {
mysqli_stmt_bind_param($stmt, 'sssi', $new_end, $esc_txid, $now, $order_id);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
}
$last_order_id = $order_id;
$existing_home_id = intval($row['home_id'] ?? 0);
wh_log('info', 'order_renewed', ['order_id' => $order_id, 'new_end' => $new_end, 'home_id' => $existing_home_id]);
$dir = ($billing_dir !== '') ? $billing_dir : dirname(__DIR__);
wh_try_provision($dir, $order_id, $user_id);
}
} else {
// New order: create billing_orders row
$dt = new DateTime($now);
$dt->modify('+' . $duration_days . ' days');
$end_date = $dt->format('Y-m-d H:i:s');
$invoice_amount = floatval($inv['amount'] ?? $inv['total_due'] ?? 0);
$price = number_format($invoice_amount, 2, '.', '');
$esc_home = mysqli_real_escape_string($db, $inv['home_name'] ?? '');
$esc_dur = mysqli_real_escape_string($db, $duration);
$esc_rcon = mysqli_real_escape_string($db, $inv['remote_control_password'] ?? '');
$esc_ftp = mysqli_real_escape_string($db, $inv['ftp_password'] ?? '');
$ip_val = intval($inv['ip'] ?? 0);
$max_pl = intval($inv['max_players'] ?? 0);
$sql = sprintf(
"INSERT INTO `%sbilling_orders` (user_id, service_id, home_name, ip, max_players, qty, invoice_duration, price, remote_control_password, ftp_password, status, order_date, end_date, payment_txid, paid_ts)
VALUES (%d, %d, '%s', %d, %d, %d, '%s', %s, '%s', '%s', 'Active', '%s', '%s', '%s', '%s')",
$pfx,
$user_id, $service_id, $esc_home, $ip_val, $max_pl, $qty,
$esc_dur, $price, $esc_rcon, $esc_ftp, $now, $end_date, $esc_txid, $now
);
if (mysqli_query($db, $sql)) {
$new_order_id = (int)mysqli_insert_id($db);
// Link invoice → order
$stmt = mysqli_prepare($db, "UPDATE `{$pfx}billing_invoices` SET order_id=? WHERE invoice_id=? LIMIT 1");
if ($stmt) {
mysqli_stmt_bind_param($stmt, 'ii', $new_order_id, $invoice_id);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
}
$last_order_id = $new_order_id;
wh_log('info', 'order_created', ['order_id' => $new_order_id, 'invoice_id' => $invoice_id]);
// Attempt provisioning via panel bridge
$dir = ($billing_dir !== '') ? $billing_dir : dirname(__DIR__);
wh_try_provision($dir, $new_order_id, $user_id);
} else {
wh_log('error', 'order_insert_failed', ['db_error' => mysqli_error($db), 'invoice_id' => $invoice_id]);
}
}
}
if ($applied_coupon_id > 0) {
mysqli_query($db, "UPDATE `{$pfx}billing_coupons` SET current_uses = current_uses + 1 WHERE coupon_id = {$applied_coupon_id}");
}
return $last_order_id;
}
/**
* Attempt to provision a newly created server via the panel bridge.
* Non-fatal: logs warnings on failure.
*/
function wh_try_provision(string $billing_dir, int $order_id, int $user_id): void
{
$bridge = $billing_dir . '/includes/panel_bridge.php';
$create = $billing_dir . '/create_servers.php';
if (!is_file($bridge) || !is_file($create)) {
wh_log('info', 'provision_skipped_no_bridge', ['order_id' => $order_id]);
return;
}
try {
require_once $bridge;
if (!function_exists('billing_panel_bootstrap')) {
wh_log('warn', 'provision_no_bootstrap_fn', ['order_id' => $order_id]);
return;
}
$ctx = billing_panel_bootstrap();
if (!$ctx || empty($ctx['db'])) {
wh_log('warn', 'provision_panel_bootstrap_failed', ['order_id' => $order_id]);
return;
}
$GLOBALS['db'] = $ctx['db'];
$GLOBALS['settings'] = $ctx['settings'] ?? [];
require_once $create;
if (function_exists('billing_invoke_provision')) {
$r = billing_invoke_provision(['order_ids' => [$order_id], 'user_id' => $user_id, 'is_admin' => true]);
wh_log('info', 'provision_result', ['order_id' => $order_id, 'result' => $r]);
}
} catch (Throwable $e) {
wh_log('error', 'provision_exception', ['order_id' => $order_id, 'error' => $e->getMessage()]);
}
}
// ============================================================================
// Webhook event log table helpers
// ============================================================================
/**
* Ensure billing_paypal_webhook_events table exists (idempotent, no ALTER on existing tables).
*/
function wh_ensure_event_table(mysqli $db, string $pfx): void
{
$table = $pfx . 'billing_paypal_webhook_events';
$res = mysqli_query($db, "SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$table}'");
if ($res && $row = mysqli_fetch_assoc($res)) {
if (intval($row['cnt']) > 0) {
return; // table exists
}
}
$sql = "CREATE TABLE IF NOT EXISTS `{$table}` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`paypal_event_id` VARCHAR(100) NOT NULL DEFAULT '',
`event_type` VARCHAR(100) NOT NULL DEFAULT '',
`resource_id` VARCHAR(100) NOT NULL DEFAULT '',
`order_id` VARCHAR(100) NOT NULL DEFAULT '',
`capture_id` VARCHAR(100) NOT NULL DEFAULT '',
`billing_order_id` INT(11) NOT NULL DEFAULT 0,
`processing_status` VARCHAR(50) NOT NULL DEFAULT 'received',
`raw_json` MEDIUMTEXT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`processed_at` DATETIME NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_paypal_event_id` (`paypal_event_id`),
KEY `idx_event_type` (`event_type`),
KEY `idx_billing_order_id` (`billing_order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
mysqli_query($db, $sql);
}
/**
* Retrieve an existing webhook event log row by paypal_event_id.
*/
function wh_get_event(mysqli $db, string $pfx, string $paypal_event_id): ?array
{
if ($paypal_event_id === '') return null;
$esc = mysqli_real_escape_string($db, $paypal_event_id);
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_paypal_webhook_events` WHERE paypal_event_id = '{$esc}' LIMIT 1");
if (!$res) return null;
$row = mysqli_fetch_assoc($res);
return $row ?: null;
}
/**
* Insert or update a webhook event log row. Returns the row id.
*/
function wh_log_event(mysqli $db, string $pfx, array $data): int
{
$paypal_event_id = mysqli_real_escape_string($db, $data['paypal_event_id'] ?? '');
$event_type = mysqli_real_escape_string($db, $data['event_type'] ?? '');
$resource_id = mysqli_real_escape_string($db, $data['resource_id'] ?? '');
$order_id_str = mysqli_real_escape_string($db, $data['order_id'] ?? '');
$capture_id = mysqli_real_escape_string($db, $data['capture_id'] ?? '');
$billing_order_id = intval($data['billing_order_id'] ?? 0);
$processing_status = mysqli_real_escape_string($db, $data['processing_status'] ?? 'received');
$raw_json = mysqli_real_escape_string($db, $data['raw_json'] ?? '');
$now = date('Y-m-d H:i:s');
if ($paypal_event_id === '') {
// No stable event ID — always insert
$sql = "INSERT INTO `{$pfx}billing_paypal_webhook_events`
(paypal_event_id, event_type, resource_id, order_id, capture_id, billing_order_id, processing_status, raw_json, created_at)
VALUES ('{$paypal_event_id}', '{$event_type}', '{$resource_id}', '{$order_id_str}', '{$capture_id}', {$billing_order_id}, '{$processing_status}', '{$raw_json}', '{$now}')";
mysqli_query($db, $sql);
return (int)mysqli_insert_id($db);
}
// Upsert: insert or update existing row
$sql = "INSERT INTO `{$pfx}billing_paypal_webhook_events`
(paypal_event_id, event_type, resource_id, order_id, capture_id, billing_order_id, processing_status, raw_json, created_at)
VALUES ('{$paypal_event_id}', '{$event_type}', '{$resource_id}', '{$order_id_str}', '{$capture_id}', {$billing_order_id}, '{$processing_status}', '{$raw_json}', '{$now}')
ON DUPLICATE KEY UPDATE
processing_status = VALUES(processing_status),
billing_order_id = VALUES(billing_order_id)";
mysqli_query($db, $sql);
$insert_id = (int)mysqli_insert_id($db);
if ($insert_id > 0) {
return $insert_id;
}
// Row already existed — fetch its id
$existing = wh_get_event($db, $pfx, $data['paypal_event_id']);
return $existing ? intval($existing['id']) : 0;
}
/**
* Update processing_status and processed_at on an event log row.
*/
function wh_update_event_status(mysqli $db, string $pfx, int $log_id, string $status, int $billing_order_id): void
{
$esc_status = mysqli_real_escape_string($db, $status);
$now = date('Y-m-d H:i:s');
$bod = intval($billing_order_id);
mysqli_query($db, "UPDATE `{$pfx}billing_paypal_webhook_events`
SET processing_status = '{$esc_status}', billing_order_id = {$bod}, processed_at = '{$now}'
WHERE id = {$log_id} LIMIT 1");
}
require_once __DIR__ . '/../_compat_include.php';
require website_billing_runtime_file('paypal/webhook.php');

View file

@ -1,73 +1,3 @@
<?php
session_name("opengamepanel_web");
session_start();
require_once(__DIR__ . '/bootstrap.php');
// Variables from config.inc.php (helps IDEs understand scope)
/** @var string $db_host Database host */
/** @var string $db_user Database user */
/** @var string $db_pass Database password */
/** @var string $db_name Database name */
/** @var string $table_prefix Table prefix for database tables */
// Simple registration form (creates a user in {table_prefix}users with MD5 password)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['username']) && !empty($_POST['password'])) {
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if ($db) {
$username = trim($_POST['username']);
$password = $_POST['password'];
$email = trim($_POST['email']);
// basic validation
if ($username === '' || $password === '' || $email === '') {
$error = 'All fields are required.';
} else {
// Store legacy MD5 for panel compatibility, and also store a modern hash
$md5pw = md5($password);
$modern = password_hash($password, PASSWORD_DEFAULT);
// Try to insert with shadow column if it exists
$has_shadow = false;
$res = $db->query("SHOW COLUMNS FROM {$table_prefix}users LIKE 'users_pass_hash'");
if ($res && $res->num_rows > 0) {
$has_shadow = true;
}
if ($has_shadow) {
$stmt = $db->prepare("INSERT INTO {$table_prefix}users (users_login, users_passwd, users_pass_hash, users_email, users_role) VALUES (?, ?, ?, ?, 'user')");
$stmt->bind_param('ssss', $username, $md5pw, $modern, $email);
} else {
$stmt = $db->prepare("INSERT INTO {$table_prefix}users (users_login, users_passwd, users_email, users_role) VALUES (?, ?, ?, 'user')");
$stmt->bind_param('sss', $username, $md5pw, $email);
}
if ($stmt->execute()) {
// Redirect to absolute login URL
$script = $_SERVER['SCRIPT_NAME'] ?? '';
$pos = strpos($script, '/_website');
$siteRoot = $pos !== false ? substr($script, 0, $pos + strlen('/_website')) : rtrim(dirname($script), '/\\');
header('Location: ' . $siteRoot . '/login.php?registered=1');
exit;
} else {
$error = 'Could not create user. Maybe the name is taken.';
}
}
}
}
?>
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Register - GameServers.World</title></head>
<body>
<?php include(__DIR__ . '/includes/top.php'); include(__DIR__ . '/includes/menu.php'); ?>
<h2>Register</h2>
<?php if (!empty($error)) echo '<div class="muted text-danger">'.htmlspecialchars($error).'</div>'; ?>
<form method="post" action="register.php">
<label>Username<br><input type="text" name="username" required></label><br>
<label>Email<br><input type="email" name="email"></label><br>
<label>Password<br><input type="password" name="password" required></label><br>
<button type="submit">Register</button>
</form>
</body>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('register.php');

View file

@ -1,210 +1,3 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server List - GameServers.World</title>
</head>
<body>
<?php
// Include database configuration
require_once(__DIR__ . '/bootstrap.php');
// Variables from config.inc.php (helps IDEs understand scope)
/** @var string $db_host Database host */
/** @var string $db_user Database user */
/** @var string $db_pass Database password */
/** @var string $db_name Database name */
/** @var string $table_prefix Table prefix for database tables */
// Create database connection
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$db) {
die("Connection failed: " . mysqli_connect_error());
}
function billing_service_price_is_free($value): bool
{
return ((int) round(((float)$value) * 100)) === 0;
}
function billing_detect_variant_label(array $row): string
{
$haystack = strtolower(trim((string)($row['cfg_file'] ?? $row['cfg_game_key'] ?? '')));
if ($haystack === '') {
return '';
}
if (preg_match('/(?:^|[_\-])(win|windows)(?:[_\-]|$)/i', $haystack)) {
return 'Windows';
}
if (preg_match('/(?:^|[_\-])linux(?:[_\-]|$)/i', $haystack)) {
return 'Linux';
}
return '';
}
// Save new description if admin
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 = ?");
$stmt->bind_param("si", $new_description, $service);
$stmt->execute();
$stmt->close();
}
// Fetch enabled services, keeping one row per billing service.
$service_id = isset($_REQUEST['service_id']) ? intval($_REQUEST['service_id']) : 0;
if ($service_id !== 0) {
$where_clause = "WHERE bs.enabled = 1 AND bs.service_id = {$service_id} AND bs.remote_server_id != '' AND bs.remote_server_id IS NOT NULL";
} else {
$where_clause = "WHERE bs.enabled = 1 AND bs.remote_server_id != '' AND bs.remote_server_id IS NOT NULL";
}
$qry_services = "SELECT bs.*, ch.game_name AS cfg_game_name, ch.game_key AS cfg_game_key, ch.home_cfg_file AS cfg_file
FROM {$table_prefix}billing_services bs
LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id
{$where_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 service_id, home_cfg_id, enabled, service_name, description,
img_url, price_monthly, slot_min_qty, slot_max_qty,
remote_server_id,
NULL AS cfg_game_name, NULL AS cfg_game_key, NULL AS cfg_file
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;
}
$serviceRows = [];
while ($row = $result_services->fetch_assoc()) {
$serviceRows[] = $row;
}
$result_services->free();
// Include top bar and menu
include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php');
?>
<!-- Services container: clearfix to contain floated service cards so footer clears correctly -->
<div class="clearfix container-wide">
<?php foreach ($serviceRows as $row): ?>
<?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'] ?? ''));
// 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>
<?php
$serviceDisplayName = (string)($row['cfg_game_name'] ?? $row['service_name']);
$variantLabel = billing_detect_variant_label($row);
if ($variantLabel !== '' && stripos($serviceDisplayName, $variantLabel) === false) {
$serviceDisplayName .= ' - ' . $variantLabel;
}
?>
<strong><?php echo htmlspecialchars($serviceDisplayName, ENT_QUOTES, 'UTF-8'); ?></strong><br>
<?php
echo billing_service_price_is_free($row['price_monthly'] ?? 0) ? "FREE" : "$" . number_format((float)$row['price_monthly'], 2) . " Monthly";
?>
<br>
<a href="order.php?service_id=<?php echo urlencode($row['service_id']); ?>" class="gsw-btn">Order Now</a>
</div>
<?php else: ?>
<!-- Single service detail view -->
<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;"><br>
<?php
$detailDisplayName = (string)($row['cfg_game_name'] ?? $row['service_name']);
$detailVariantLabel = billing_detect_variant_label($row);
if ($detailVariantLabel !== '' && stripos($detailDisplayName, $detailVariantLabel) === false) {
$detailDisplayName .= ' - ' . $detailVariantLabel;
}
?>
<center><b><?php echo htmlspecialchars($detailDisplayName, ENT_QUOTES, 'UTF-8'); ?></b></center>
<?php
$isAdmin = false;
if ($isAdmin) {
if (!isset($_POST['edit'])) {
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='" . intval($row['service_id']) . "'>
<input type='submit' name='edit' value='Edit'>
</form>";
} else {
$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='" . 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>
<!-- Order Form -->
<form method="post" action="order_server.php">
<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 htmlspecialchars((string)($row['service_name'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"></td>
</tr>
<!-- Add other form fields as needed -->
<tr>
<td colspan="2">
<?php
// Only show Add to Cart when the user is logged in
$is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) || (isset($_SESSION['website_username']) && !empty($_SESSION['website_username']));
if ($is_logged_in):
?>
<button type="submit" 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; ?>
</td>
</tr>
</table>
</form>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php
// Close database connection
billing_maybe_close_db($db);
?>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</body>
</html>
require_once __DIR__ . '/_compat_include.php';
require website_billing_runtime_file('serverlist.php');

View file

@ -1 +1 @@
Last Updated at 4:03pm on 2026-05-09
Last Updated at 1:38pm on 2026-05-18