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:
parent
651c935fa7
commit
176f532737
686 changed files with 92221 additions and 8198 deletions
11
Website/_compat_include.php
Normal file
11
Website/_compat_include.php
Normal 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;
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 & 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');
|
||||
|
|
|
|||
|
|
@ -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 → 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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
957
Website/cart.php
957
Website/cart.php
|
|
@ -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'); ?>¤cy=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');
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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');
|
||||
|
|
|
|||
432
Website/docs.php
432
Website/docs.php
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 it’s 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 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>We’ll 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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Last Updated at 4:03pm on 2026-05-09
|
||||
Last Updated at 1:38pm on 2026-05-18
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue