'; } function website_verify_csrf(): bool { website_start_session(); $token = (string)($_POST['csrf_token'] ?? ''); return $token !== '' && hash_equals((string)($_SESSION['website_csrf_token'] ?? ''), $token); } function website_require_login(string $returnPath = 'account.php'): ?array { $user = website_current_user(); if ($user === null) { $_SESSION['website_login_return'] = website_safe_return_path($returnPath, 'account.php'); header('Location: ' . website_login_url($returnPath), true, 302); exit; } return $user; } function website_require_staff(): array { $user = website_require_login('staff.php'); if ($user === null || !website_current_user_is_staff()) { http_response_code(403); website_render('message.php', [ 'activePage' => 'account', 'pageTitle' => 'Access Denied - Gameservers.World', 'heading' => 'Access Denied', 'message' => 'This page is available only to authorized website staff.', ]); exit; } return $user; } function website_column_exists(string $tableName, string $columnName): bool { return isset(website_table_columns($tableName)[$columnName]); } function website_billing_migrations(): array { $prefix = website_table_prefix(); return [ "CREATE TABLE IF NOT EXISTS `{$prefix}billing_invoices` ( `invoice_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `status` VARCHAR(24) NOT NULL DEFAULT 'due', `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `currency` CHAR(3) NOT NULL DEFAULT 'USD', `description` VARCHAR(255) DEFAULT NULL, `invoice_date` DATETIME NOT NULL, `due_date` DATETIME DEFAULT NULL, `paid_date` DATETIME DEFAULT NULL, `payment_method` VARCHAR(40) DEFAULT NULL, `payment_txid` VARCHAR(128) DEFAULT NULL, `paypal_order_id` VARCHAR(128) DEFAULT NULL, `paypal_capture_id` VARCHAR(128) DEFAULT NULL, `payer_email` VARCHAR(255) DEFAULT NULL, `coupon_id` INT DEFAULT NULL, `discount_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `order_id` INT DEFAULT NULL, `metadata_json` TEXT DEFAULT NULL, PRIMARY KEY (`invoice_id`), KEY `idx_invoice_user_status` (`user_id`, `status`), KEY `idx_invoice_payment` (`paypal_order_id`, `paypal_capture_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `{$prefix}billing_orders` ( `order_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `invoice_id` INT UNSIGNED DEFAULT NULL, `user_id` INT NOT NULL, `service_id` INT NOT NULL, `home_id` INT DEFAULT NULL, `home_name` VARCHAR(120) NOT NULL, `remote_server_id` INT DEFAULT NULL, `max_players` INT NOT NULL DEFAULT 0, `qty` INT NOT NULL DEFAULT 1, `invoice_duration` VARCHAR(24) NOT NULL DEFAULT 'month', `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `status` VARCHAR(32) NOT NULL DEFAULT 'paid', `order_date` DATETIME NOT NULL, `end_date` DATETIME DEFAULT NULL, `payment_txid` VARCHAR(128) DEFAULT NULL, `paid_ts` DATETIME DEFAULT NULL, `provisioning_claim` VARCHAR(64) DEFAULT NULL, `provisioning_error` TEXT DEFAULT NULL, `metadata_json` TEXT DEFAULT NULL, PRIMARY KEY (`order_id`), UNIQUE KEY `uniq_invoice_service` (`invoice_id`, `service_id`, `home_name`), KEY `idx_order_user_status` (`user_id`, `status`), KEY `idx_order_home` (`home_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `{$prefix}billing_payments` ( `payment_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `invoice_id` INT UNSIGNED DEFAULT NULL, `user_id` INT DEFAULT NULL, `provider` VARCHAR(40) NOT NULL, `provider_order_id` VARCHAR(128) DEFAULT NULL, `provider_capture_id` VARCHAR(128) DEFAULT NULL, `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `currency` CHAR(3) NOT NULL DEFAULT 'USD', `status` VARCHAR(40) NOT NULL, `payer_email` VARCHAR(255) DEFAULT NULL, `created_at` DATETIME NOT NULL, `metadata_json` TEXT DEFAULT NULL, PRIMARY KEY (`payment_id`), UNIQUE KEY `uniq_provider_capture` (`provider`, `provider_capture_id`), KEY `idx_payment_invoice` (`invoice_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `{$prefix}billing_coupons` ( `coupon_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `code` VARCHAR(64) NOT NULL, `name` VARCHAR(120) NOT NULL, `description` TEXT DEFAULT NULL, `discount_type` VARCHAR(16) NOT NULL DEFAULT 'percent', `discount_value` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `starts_at` DATETIME DEFAULT NULL, `expires` DATETIME DEFAULT NULL, `max_uses` INT DEFAULT NULL, `current_uses` INT NOT NULL DEFAULT 0, `minimum_amount` DECIMAL(10,2) DEFAULT NULL, `service_filter_json` TEXT DEFAULT NULL, `created_at` DATETIME NOT NULL, PRIMARY KEY (`coupon_id`), UNIQUE KEY `uniq_coupon_code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `{$prefix}website_settings` ( `setting_key` VARCHAR(120) NOT NULL, `setting_value` TEXT DEFAULT NULL, `is_secret` TINYINT(1) NOT NULL DEFAULT 0, `updated_at` DATETIME NOT NULL, PRIMARY KEY (`setting_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `{$prefix}password_reset_tokens` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `token_hash` CHAR(64) NOT NULL, `expires_at` DATETIME NOT NULL, `used_at` DATETIME DEFAULT NULL, `created_at` DATETIME NOT NULL, `originating_ip` VARCHAR(64) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uniq_reset_token_hash` (`token_hash`), KEY `idx_reset_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `{$prefix}payment_webhook_events` ( `event_id` VARCHAR(128) NOT NULL, `provider` VARCHAR(40) NOT NULL, `event_type` VARCHAR(120) DEFAULT NULL, `received_at` DATETIME NOT NULL, `processed_at` DATETIME DEFAULT NULL, `status` VARCHAR(40) NOT NULL DEFAULT 'received', `metadata_json` TEXT DEFAULT NULL, PRIMARY KEY (`event_id`, `provider`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `{$prefix}provisioning_attempts` ( `attempt_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `order_id` INT UNSIGNED NOT NULL, `status` VARCHAR(40) NOT NULL, `message` TEXT DEFAULT NULL, `created_at` DATETIME NOT NULL, PRIMARY KEY (`attempt_id`), KEY `idx_provision_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", ]; } function website_run_billing_migrations(): array { $db = website_db(); if (!$db instanceof mysqli) { return ['Database connection is unavailable.']; } $errors = []; foreach (website_billing_migrations() as $sql) { if (!$db->query($sql)) { $errors[] = $db->error; } } $serviceTable = website_table('billing_services'); if (website_table_exists($serviceTable)) { $serviceColumns = [ 'description' => 'TEXT DEFAULT NULL', 'img_url' => 'VARCHAR(255) DEFAULT NULL', 'price_monthly' => 'DECIMAL(10,2) NOT NULL DEFAULT 0.00', 'slot_min_qty' => 'INT NOT NULL DEFAULT 16', 'slot_max_qty' => 'INT NOT NULL DEFAULT 0', 'enabled' => 'TINYINT(1) NOT NULL DEFAULT 1', 'remote_server_id' => 'VARCHAR(255) DEFAULT NULL', ]; $existing = website_table_columns($serviceTable); foreach ($serviceColumns as $column => $definition) { if (isset($existing[$column])) { continue; } $safeTable = str_replace('`', '``', $serviceTable); $safeColumn = str_replace('`', '``', $column); if (!$db->query("ALTER TABLE `{$safeTable}` ADD COLUMN `{$safeColumn}` {$definition}")) { $errors[] = $db->error; } } } return $errors; } function website_setting(string $key, ?string $default = null): ?string { $db = website_db(); if (!$db instanceof mysqli || !website_table_exists(website_table('website_settings'))) { return $default; } $table = website_table('website_settings'); $stmt = $db->prepare("SELECT `setting_value` FROM `{$table}` WHERE `setting_key` = ? LIMIT 1"); if (!$stmt) { return $default; } $stmt->bind_param('s', $key); $stmt->execute(); $result = $stmt->get_result(); $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null; $stmt->close(); return is_array($row) ? (string)$row['setting_value'] : $default; } function website_set_setting(string $key, string $value, bool $secret = false): bool { $db = website_db(); if (!$db instanceof mysqli || !website_table_exists(website_table('website_settings'))) { return false; } $table = website_table('website_settings'); $isSecret = $secret ? 1 : 0; $stmt = $db->prepare("INSERT INTO `{$table}` (`setting_key`, `setting_value`, `is_secret`, `updated_at`) VALUES (?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE `setting_value` = VALUES(`setting_value`), `is_secret` = VALUES(`is_secret`), `updated_at` = VALUES(`updated_at`)"); if (!$stmt) { return false; } $stmt->bind_param('ssi', $key, $value, $isSecret); $ok = $stmt->execute(); $stmt->close(); return $ok; } function website_paypal_config(): array { return [ 'enabled' => website_setting('paypal_enabled', '0') === '1', 'sandbox' => website_setting('paypal_sandbox', '1') === '1', 'client_id' => (string)(getenv('GSP_WEBSITE_PAYPAL_CLIENT_ID') ?: website_setting('paypal_client_id', '')), 'client_secret' => (string)(getenv('GSP_WEBSITE_PAYPAL_CLIENT_SECRET') ?: website_setting('paypal_client_secret', '')), 'webhook_id' => (string)(getenv('GSP_WEBSITE_PAYPAL_WEBHOOK_ID') ?: website_setting('paypal_webhook_id', '')), 'currency' => (string)website_setting('paypal_currency', 'USD'), 'description_prefix' => (string)website_setting('paypal_description_prefix', 'Gameservers.World order'), ]; } function website_paypal_api_base(array $config): string { return $config['sandbox'] ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com'; } function website_paypal_oauth(array $config): ?string { if ($config['client_id'] === '' || $config['client_secret'] === '') { return null; } $ch = curl_init(website_paypal_api_base($config) . '/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 => $config['client_id'] . ':' . $config['client_secret'], CURLOPT_TIMEOUT => 20, ]); $response = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http !== 200 || !is_string($response)) { return null; } $json = json_decode($response, true); return is_array($json) ? (string)($json['access_token'] ?? '') : null; } function website_services_have_columns(array $columns): bool { $table = website_table('billing_services'); if (!website_table_exists($table)) { return false; } $existing = website_table_columns($table); foreach ($columns as $column) { if (!isset($existing[$column])) { return false; } } return true; } function website_fetch_remote_servers(): array { $db = website_db(); $table = website_table('remote_servers'); if (!$db instanceof mysqli || !website_table_exists($table)) { return []; } $rows = []; $result = @$db->query("SELECT `remote_server_id`, `remote_server_name`, `agent_ip`, `enabled` FROM `{$table}` ORDER BY `remote_server_name` ASC"); if ($result instanceof mysqli_result) { while ($row = $result->fetch_assoc()) { $rows[] = $row; } $result->free(); } return $rows; } function website_fetch_invoices_for_user(int $userId): array { $db = website_db(); $table = website_table('billing_invoices'); if (!$db instanceof mysqli || !website_table_exists($table)) { return []; } $stmt = $db->prepare("SELECT * FROM `{$table}` WHERE `user_id` = ? ORDER BY `invoice_id` DESC"); if (!$stmt) { return []; } $stmt->bind_param('i', $userId); $stmt->execute(); $result = $stmt->get_result(); $rows = []; while ($result instanceof mysqli_result && ($row = $result->fetch_assoc())) { $rows[] = $row; } $stmt->close(); return $rows; } function website_fetch_orders_for_user(int $userId): array { $db = website_db(); $table = website_table('billing_orders'); if (!$db instanceof mysqli || !website_table_exists($table)) { return []; } $stmt = $db->prepare("SELECT * FROM `{$table}` WHERE `user_id` = ? ORDER BY `order_id` DESC"); if (!$stmt) { return []; } $stmt->bind_param('i', $userId); $stmt->execute(); $result = $stmt->get_result(); $rows = []; while ($result instanceof mysqli_result && ($row = $result->fetch_assoc())) { $rows[] = $row; } $stmt->close(); return $rows; } function website_create_invoice_from_cart(int $userId): ?int { $db = website_db(); if (!$db instanceof mysqli || !website_table_exists(website_table('billing_invoices')) || !website_table_exists(website_table('billing_orders'))) { return null; } $items = website_cart_items(); if (empty($items)) { return null; } $validItems = []; $total = 0.0; foreach ($items as $item) { $service = website_fetch_service_by_id((int)($item['service_id'] ?? 0)); if (!$service) { continue; } $locations = website_service_locations($service); $locationId = (string)($item['location_id'] ?? ''); $slots = (int)($item['slots'] ?? 0); $min = website_service_min_slots($service); $max = website_service_max_slots($service); if ($slots < $min || ($max > 0 && $slots > $max) || !isset($locations[$locationId])) { continue; } $price = (float)($service['price_monthly'] ?? 0); $durationMonths = max(1, (int)($item['duration_months'] ?? 1)); $line = [ 'service_id' => (int)$service['service_id'], 'service_name' => website_service_name($service), 'home_name' => trim((string)($item['server_name'] ?? $item['service_name'] ?? website_service_name($service))), 'remote_server_id' => (int)$locationId, 'slots' => $slots, 'duration_months' => $durationMonths, 'price' => $price * $durationMonths, 'price_monthly' => $price, ]; $validItems[] = $line; $total += (float)$line['price']; } if (empty($validItems)) { return null; } $invoiceTable = website_table('billing_invoices'); $orderTable = website_table('billing_orders'); $currency = website_paypal_config()['currency'] ?: 'USD'; $metadata = json_encode(['items' => $validItems], JSON_UNESCAPED_SLASHES); $db->begin_transaction(); try { $description = 'Gameservers.World server order'; $stmt = $db->prepare("INSERT INTO `{$invoiceTable}` (`user_id`, `status`, `amount`, `currency`, `description`, `invoice_date`, `due_date`, `metadata_json`) VALUES (?, 'due', ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 3 DAY), ?)"); if (!$stmt) { throw new RuntimeException('invoice prepare failed'); } $stmt->bind_param('idsss', $userId, $total, $currency, $description, $metadata); $stmt->execute(); $invoiceId = (int)$db->insert_id; $stmt->close(); foreach ($validItems as $line) { $status = 'pending_payment'; $duration = (string)$line['duration_months']; $stmt = $db->prepare("INSERT INTO `{$orderTable}` (`invoice_id`, `user_id`, `service_id`, `home_name`, `remote_server_id`, `max_players`, `qty`, `invoice_duration`, `price`, `status`, `order_date`, `metadata_json`) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, NOW(), ?)"); if (!$stmt) { throw new RuntimeException('order prepare failed'); } $lineMeta = json_encode($line, JSON_UNESCAPED_SLASHES); $stmt->bind_param('iiisiisdss', $invoiceId, $userId, $line['service_id'], $line['home_name'], $line['remote_server_id'], $line['slots'], $duration, $line['price'], $status, $lineMeta); $stmt->execute(); $stmt->close(); } $db->commit(); $_SESSION['website_cart'] = []; website_log_activity('Website invoice created from cart #' . $invoiceId, $userId, 'invoice_created'); return $invoiceId; } catch (Throwable $e) { $db->rollback(); website_log('Invoice creation failed: ' . $e->getMessage()); return null; } } function website_mark_invoice_paid(int $invoiceId, string $provider, string $providerOrderId, string $captureId, string $payerEmail, array $metadata = []): bool { $db = website_db(); if (!$db instanceof mysqli) { return false; } $invoiceTable = website_table('billing_invoices'); $orderTable = website_table('billing_orders'); $paymentTable = website_table('billing_payments'); if (!website_table_exists($invoiceTable) || !website_table_exists($orderTable) || !website_table_exists($paymentTable)) { return false; } $db->begin_transaction(); try { $stmt = $db->prepare("SELECT `user_id`, `amount`, `currency`, `status` FROM `{$invoiceTable}` WHERE `invoice_id` = ? FOR UPDATE"); $stmt->bind_param('i', $invoiceId); $stmt->execute(); $row = $stmt->get_result()->fetch_assoc(); $stmt->close(); if (!is_array($row) || (string)$row['status'] === 'paid') { $db->commit(); return true; } $userId = (int)$row['user_id']; $amount = (float)$row['amount']; $currency = (string)$row['currency']; $meta = json_encode($metadata, JSON_UNESCAPED_SLASHES); $stmt = $db->prepare("UPDATE `{$invoiceTable}` SET `status` = 'paid', `paid_date` = NOW(), `payment_method` = ?, `payment_txid` = ?, `paypal_order_id` = ?, `paypal_capture_id` = ?, `payer_email` = ? WHERE `invoice_id` = ?"); $stmt->bind_param('sssssi', $provider, $captureId, $providerOrderId, $captureId, $payerEmail, $invoiceId); $stmt->execute(); $stmt->close(); $paid = 'paid'; $provisioning = 'paid'; $stmt = $db->prepare("UPDATE `{$orderTable}` SET `status` = ?, `payment_txid` = ?, `paid_ts` = NOW() WHERE `invoice_id` = ? AND `status` IN ('pending_payment', 'paid')"); $stmt->bind_param('ssi', $provisioning, $captureId, $invoiceId); $stmt->execute(); $stmt->close(); $stmt = $db->prepare("INSERT IGNORE INTO `{$paymentTable}` (`invoice_id`, `user_id`, `provider`, `provider_order_id`, `provider_capture_id`, `amount`, `currency`, `status`, `payer_email`, `created_at`, `metadata_json`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)"); $stmt->bind_param('iisssdssss', $invoiceId, $userId, $provider, $providerOrderId, $captureId, $amount, $currency, $paid, $payerEmail, $meta); $stmt->execute(); $stmt->close(); $db->commit(); website_log_activity('Website invoice paid #' . $invoiceId, $userId, 'invoice_paid'); return true; } catch (Throwable $e) { $db->rollback(); website_log('Payment mark-paid failed: ' . $e->getMessage()); return false; } }