filemtime($b); }); $toDelete = count($files) - $retention; for ($i = 0; $i < $toDelete; $i++) { @unlink($files[$i]); } } // --------------------------------------------------------------------------- // Helper: create a backup of the config file; returns backup filename or ''. // --------------------------------------------------------------------------- function billing_admin_create_backup(string $cfgPath, string $bakDir): string { @mkdir($bakDir, 0775, true); $bakName = $bakDir . '/config.inc.php.' . date('Ymd-His') . '.' . bin2hex(random_bytes(4)) . '.bak'; if (!copy($cfgPath, $bakName)) { return ''; } return $bakName; } // --------------------------------------------------------------------------- // Helper: run php -l on a file and return [ok, output]. // --------------------------------------------------------------------------- function billing_admin_lint(string $filePath): array { $phpExec = PHP_BINARY ?: null; if (!$phpExec) { return [true, 'PHP executable not found; skipping syntax check.']; } $cmd = escapeshellarg($phpExec) . ' -l ' . escapeshellarg($filePath); $out = []; $rc = 0; @exec($cmd . ' 2>&1', $out, $rc); return [$rc === 0, implode("\n", $out)]; } // --------------------------------------------------------------------------- // Helper: generate canonical config.inc.php content from an array of values. // DB settings are preserved from the existing file; only billing fields change. // --------------------------------------------------------------------------- function billing_admin_build_config(string $existingContent, array $vals): string { // Extract current DB settings from existing file content so we never lose them. $dbLines = []; foreach (['db_host', 'db_port', 'db_user', 'db_pass', 'db_name', 'table_prefix', 'db_type'] as $var) { if (preg_match('/^\s*\$' . preg_quote($var, '/') . '\s*=.*$/m', $existingContent, $m)) { $dbLines[$var] = rtrim($m[0]); } } $q = static function (string $v): string { return '"' . addslashes($v) . '"'; }; $mode = (strtolower($vals['paypal_mode'] ?? 'sandbox') === 'live') ? 'live' : 'sandbox'; $retention = max(1, min(10, (int)($vals['backup_retention'] ?? 5))); $baseUrl = rtrim(trim($vals['SITE_BASE_URL'] ?? ''), '/'); $bg = trim($vals['SITE_BACKGROUND'] ?? 'images/dark.jpg'); $dataDir = trim($vals['SITE_DATA_DIR'] ?? ''); $wh_path = '/' . ltrim(trim($vals['paypal_webhook_path'] ?? '/paypal/webhook.php'), '/'); // Sandbox credentials — never erase existing secret if field was left blank $sb_id = trim($vals['paypal_sandbox_client_id'] ?? ''); $sb_sec = trim($vals['paypal_sandbox_client_secret'] ?? ''); $sb_wh = trim($vals['paypal_sandbox_webhook_id'] ?? ''); // Live credentials — never erase existing secret if field was left blank $lv_id = trim($vals['paypal_live_client_id'] ?? ''); $lv_sec = trim($vals['paypal_live_client_secret'] ?? ''); $lv_wh = trim($vals['paypal_live_webhook_id'] ?? ''); $dbBlock = ''; foreach (['db_host', 'db_port', 'db_user', 'db_pass', 'db_name', 'table_prefix', 'db_type'] as $var) { if (isset($dbLines[$var])) { $dbBlock .= $dbLines[$var] . "\n"; } } $dataDirLine = ($dataDir !== '' && $dataDir !== 'auto') ? '$SITE_DATA_DIR = ' . $q($dataDir) . ';' : "\$SITE_DATA_DIR = realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data';"; return ' Edit Config.' . "\n" . '###############################################' . "\n" . $dbBlock . "\n" . '// Optional: base URL without trailing slash (e.g. https://gameservers.world).' . "\n" . '// Leave empty to use relative paths.' . "\n" . '$SITE_BASE_URL = ' . $q($baseUrl) . ';' . "\n" . '$SITE_BASE_URL = rtrim(trim((string)$SITE_BASE_URL), \'/\');' . "\n" . "\n" . '// Site-wide background image (relative to site root).' . "\n" . '$SITE_BACKGROUND = ' . $q($bg) . ';' . "\n" . '$SITE_BACKGROUND = trim((string)$SITE_BACKGROUND);' . "\n" . "\n" . '// Data directory for persisted payment webhook JSON files.' . "\n" . $dataDirLine . "\n" . "\n" . '// ---------------------------------------------------------------------------' . "\n" . '// PayPal configuration' . "\n" . '// ---------------------------------------------------------------------------' . "\n" . '$paypal_mode = ' . $q($mode) . '; // \'sandbox\' or \'live\'' . "\n" . "\n" . '// Sandbox credentials (PayPal Developer Dashboard → sandbox app)' . "\n" . '$paypal_sandbox_client_id = ' . $q($sb_id) . ';' . "\n" . '$paypal_sandbox_client_secret = ' . $q($sb_sec) . ';' . "\n" . '$paypal_sandbox_webhook_id = ' . $q($sb_wh) . ';' . "\n" . "\n" . '// Live credentials (leave blank until ready for production)' . "\n" . '$paypal_live_client_id = ' . $q($lv_id) . ';' . "\n" . '$paypal_live_client_secret = ' . $q($lv_sec) . ';' . "\n" . '$paypal_live_webhook_id = ' . $q($lv_wh) . ';' . "\n" . "\n" . '// Webhook path (relative to billing site root, must start with /)' . "\n" . '// Full public URL = $SITE_BASE_URL + $paypal_webhook_path' . "\n" . '$paypal_webhook_path = ' . $q($wh_path) . ';' . "\n" . "\n" . '// Admin config backup retention: how many backups to keep (1–10). Default 5.' . "\n" . '$SITE_CONFIG_BACKUP_RETENTION = ' . $retention . ';' . "\n" . '?>' . "\n"; } // --------------------------------------------------------------------------- // Read current values from config (already loaded by config_loader above). // --------------------------------------------------------------------------- $cfgVals = [ 'SITE_BASE_URL' => $SITE_BASE_URL ?? '', 'SITE_BACKGROUND' => $SITE_BACKGROUND ?? 'images/dark.jpg', 'SITE_DATA_DIR' => $SITE_DATA_DIR ?? '', 'paypal_mode' => $paypal_mode ?? 'sandbox', 'paypal_sandbox_client_id' => $paypal_sandbox_client_id ?? '', 'paypal_sandbox_client_secret' => $paypal_sandbox_client_secret ?? '', 'paypal_sandbox_webhook_id' => $paypal_sandbox_webhook_id ?? '', 'paypal_live_client_id' => $paypal_live_client_id ?? '', 'paypal_live_client_secret' => $paypal_live_client_secret ?? '', 'paypal_live_webhook_id' => $paypal_live_webhook_id ?? '', 'paypal_webhook_path' => $paypal_webhook_path ?? '/paypal/webhook.php', 'backup_retention' => $SITE_CONFIG_BACKUP_RETENTION ?? 5, ]; // Computed full webhook URL for display $computedWebhookUrl = function_exists('gsp_paypal_get_full_webhook_url') ? gsp_paypal_get_full_webhook_url() : rtrim($cfgVals['SITE_BASE_URL'], '/') . $cfgVals['paypal_webhook_path']; // Detect panel-mode (DB settings are managed by the panel) $panelMode = defined('BILLING_PANEL_CONFIG_PATH'); $panelCfgPath = $panelMode ? BILLING_PANEL_CONFIG_PATH : null; $status = ''; $statusType = 'info'; // 'success' | 'error' | 'info' // --------------------------------------------------------------------------- // POST: Save interactive form // --------------------------------------------------------------------------- if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_form') { $token = $_POST['csrf'] ?? ''; if (!hash_equals($csrf, (string)$token)) { $status = 'Invalid CSRF token.'; $statusType = 'error'; } elseif (!is_writable($cfgPath)) { $status = 'Config file is not writable: ' . h($cfgPath); $statusType = 'error'; } else { // Collect and validate form values $formVals = [ 'SITE_BASE_URL' => trim($_POST['SITE_BASE_URL'] ?? ''), 'SITE_BACKGROUND' => trim($_POST['SITE_BACKGROUND'] ?? 'images/dark.jpg'), 'SITE_DATA_DIR' => trim($_POST['SITE_DATA_DIR'] ?? ''), 'paypal_mode' => (strtolower(trim($_POST['paypal_mode'] ?? 'sandbox')) === 'live') ? 'live' : 'sandbox', 'paypal_sandbox_client_id' => trim($_POST['paypal_sandbox_client_id'] ?? ''), 'paypal_live_client_id' => trim($_POST['paypal_live_client_id'] ?? ''), 'paypal_sandbox_webhook_id' => trim($_POST['paypal_sandbox_webhook_id'] ?? ''), 'paypal_live_webhook_id' => trim($_POST['paypal_live_webhook_id'] ?? ''), 'paypal_webhook_path' => trim($_POST['paypal_webhook_path'] ?? '/paypal/webhook.php'), 'backup_retention' => (int)($_POST['backup_retention'] ?? 5), ]; // Client secrets: only update if a non-blank value was submitted (never erase existing). $sbSecPost = trim($_POST['paypal_sandbox_client_secret'] ?? ''); $formVals['paypal_sandbox_client_secret'] = ($sbSecPost !== '') ? $sbSecPost : ($cfgVals['paypal_sandbox_client_secret'] ?? ''); $lvSecPost = trim($_POST['paypal_live_client_secret'] ?? ''); $formVals['paypal_live_client_secret'] = ($lvSecPost !== '') ? $lvSecPost : ($cfgVals['paypal_live_client_secret'] ?? ''); // Validate $validationError = ''; if ($formVals['backup_retention'] < 1 || $formVals['backup_retention'] > 10) { $validationError = 'Backup retention must be a number between 1 and 10.'; } if ($validationError) { $status = $validationError; $statusType = 'error'; } else { $existingContent = (string)file_get_contents($cfgPath); $newContent = billing_admin_build_config($existingContent, $formVals); // Backup before write. // Note: the backup copy and subsequent file_put_contents are not covered by a // single atomic lock. This is acceptable for an admin-only operation where // concurrent writes are not expected. $bakName = billing_admin_create_backup($cfgPath, $bakDir); if (!$bakName) { $status = 'Failed to create backup. Aborting save.'; $statusType = 'error'; } else { if (file_put_contents($cfgPath, $newContent, LOCK_EX) === false) { $status = 'Failed to write config file.'; $statusType = 'error'; } else { // Syntax check [$lintOk, $lintOut] = billing_admin_lint($cfgPath); if (!$lintOk) { @copy($bakName, $cfgPath); // rollback $status = 'Syntax error in generated config; rolled back. Lint: ' . h($lintOut); $statusType = 'error'; } else { // Apply backup retention $retention = max(1, min(10, $formVals['backup_retention'])); billing_admin_apply_retention($bakDir, $retention); $cfgVals = $formVals; // update displayed values $computedWebhookUrl = rtrim($formVals['SITE_BASE_URL'], '/') . ('/' . ltrim($formVals['paypal_webhook_path'] ?? '/paypal/webhook.php', '/')); $status = 'Config saved successfully. Backup: ' . basename($bakName); $statusType = 'success'; } } } } } } // --------------------------------------------------------------------------- // POST: Save raw editor // --------------------------------------------------------------------------- if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_raw') { $token = $_POST['csrf'] ?? ''; if (!hash_equals($csrf, (string)$token)) { $status = 'Invalid CSRF token.'; $statusType = 'error'; } elseif (!is_writable($cfgPath)) { $status = 'Config file is not writable: ' . h($cfgPath); $statusType = 'error'; } else { $newRaw = $_POST['config_text'] ?? ''; if (strpos(trim($newRaw), ' Admin — Edit Config

Edit Site Config

⚠️

Site Settings

ℹ️ Panel-integrated mode. Database settings are managed by the panel and synced automatically from . They are shown below for reference only.
Managed by the panel config. Edit the panel's includes/config.inc.php to change.
Full base URL without trailing slash (e.g. https://gameservers.world). Leave empty to use relative paths. Used to compute the full public PayPal webhook URL.
Path to background image relative to the billing site root (e.g. images/dark.jpg).
Absolute path where payment webhook JSON files are stored. Leave empty to use the default: modules/billing/data/.

PayPal Configuration

Currently active PayPal mode:
Sandbox uses test credentials and the PayPal sandbox API — safe for development. Live processes real payments. Switch only after configuring live credentials.

Sandbox Credentials

Found in PayPal Developer Dashboard → sandbox app. Safe to expose in browser JS.
Server-side only — never sent to the browser. Leave blank to keep existing value.
Webhook ID from your PayPal sandbox app (for signature verification). Leave empty to skip verification in sandbox mode (OK for initial setup).

Live Credentials

From your PayPal live app. Leave blank until ready for production.
Server-side only. Leave blank to keep existing value.
Webhook ID from your PayPal live app (for signature verification).

Webhook Endpoint

PayPal requires a full public HTTPS URL to deliver webhook events. Set your Site Base URL above, then copy the computed URL below into your PayPal app's webhook configuration.
Path relative to the billing site root (must start with /). Default: /paypal/webhook.php
This is the URL PayPal will POST webhook events to. It must be publicly accessible over HTTPS before enabling live mode.

Backup Settings

Number of config backups to keep (1–10). The oldest backup beyond this limit is deleted after each save. Backups are stored in .
' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ''; } // Last webhook events + recent PayPal errors $diag_recent_events = []; $diag_recent_errors = []; $diag_errors_warning = ''; try { $port_int = intval($db_port ?? 3306) ?: 3306; $diag_db = @mysqli_connect($db_host ?? 'localhost', $db_user ?? '', $db_pass ?? '', $db_name ?? '', $port_int); if ($diag_db) { $pfx_diag = $table_prefix ?? 'gsp_'; mysqli_set_charset($diag_db, 'utf8mb4'); $res = @mysqli_query($diag_db, "SELECT paypal_event_id, event_type, processing_status, created_at FROM `{$pfx_diag}billing_paypal_webhook_events` ORDER BY id DESC LIMIT 5"); if ($res) { while ($row = mysqli_fetch_assoc($res)) { $diag_recent_events[] = $row; } } // Recent PayPal errors — use BillingRepository for safe table creation require_once __DIR__ . '/classes/BillingRepository.php'; $diag_repo = new BillingRepository($diag_db, $pfx_diag); if ($diag_repo->ensureBillingPaypalErrorsTable()) { $diag_recent_errors = $diag_repo->getRecentPaypalErrors(10); } else { $diag_errors_warning = 'Could not create billing_paypal_errors table. Check DB permissions.'; } mysqli_close($diag_db); } } catch (Throwable $e) { $diag_errors_warning = 'Diagnostics DB query failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'); } ?>

PayPal Diagnostics

Self-Check Results:
• Mode:
• Active Client ID:
• Active Client Secret:
• Active Webhook ID:
• Webhook file:
• Logs directory:
• Data directory:
• Config file:
Current mode
test live
Active Client ID
Active Client Secret
Active Webhook ID
Sandbox Client ID
Sandbox Client Secret
Sandbox Webhook ID
Live Client ID
Live Client Secret
Live Webhook ID
Webhook path
Full public webhook URL
Webhook file on disk

Recent Webhook Events

PayPal Event ID Type Status Received

No webhook events recorded yet. Events will appear here after PayPal delivers the first webhook to .

Recent PayPal Errors

No PayPal errors logged yet.

TimeContextError CodeMessage Debug IDOrder IDUser

Advanced: Raw Config Editor

⚠️ Warning: Manually editing the raw PHP file can break the billing website if you introduce a syntax error or remove required variables. A backup is created automatically before saving, and a syntax check runs after. The file is rolled back if a parse error is detected.

Backup directory:
backup(s) stored. Most recent:
No backups yet.