- config.inc.php: new sandbox/live credential structure with paypal_mode, separate sandbox/live client_id, client_secret, webhook_id, and webhook_path - config.example.php: updated to match new structure - config_loader.php: adds defaults and backward compat mapping from old $paypal_sandbox/$paypal_client_id variables; adds gsp_paypal_* helper functions - PayPalGateway.php: fromConfig() uses gsp_paypal_* helpers with fallback - cart.php: uses gsp_paypal_get_client_id()/gsp_paypal_is_sandbox() helpers - webhook.php: updated to use gsp_paypal_* helpers for credentials/API base - paypal/webhook.php: new full-featured webhook receiver with signature verification, idempotency log, event processing, provisioning trigger - admin_config.php: expanded to separate sandbox/live fields, computed webhook URL, diagnostics panel showing credential status and recent webhook events - module.php: bumped to v3.3/db_version 3, adds billing_paypal_webhook_events table Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/f974e469-8562-41df-ba37-bc340f5a154c Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
747 lines
38 KiB
PHP
747 lines
38 KiB
PHP
<?php
|
||
/**
|
||
* Admin Config Editor
|
||
*
|
||
* Provides two ways to edit modules/billing/includes/config.inc.php:
|
||
* A. Interactive form (top) — fields for each billing-specific setting.
|
||
* B. Raw PHP editor (bottom) — direct file content textarea (advanced).
|
||
*
|
||
* Both methods create a timestamped backup before saving and apply the
|
||
* $SITE_CONFIG_BACKUP_RETENTION limit (default 5) after writing.
|
||
* A post-save php -l syntax check rolls back the file on parse errors.
|
||
*
|
||
* Database settings (db_host, db_port, db_user, db_pass, db_name, table_prefix)
|
||
* are shown as read-only when the module is installed inside a GSP panel tree.
|
||
* They are managed via the panel and synced automatically by config_loader.php.
|
||
*/
|
||
|
||
require_once(__DIR__ . '/includes/admin_auth.php');
|
||
require_once(__DIR__ . '/includes/config_loader.php');
|
||
include(__DIR__ . '/includes/top.php');
|
||
include(__DIR__ . '/includes/menu.php');
|
||
|
||
if (session_status() === PHP_SESSION_NONE) {
|
||
session_name('opengamepanel_web');
|
||
session_start();
|
||
}
|
||
if (empty($_SESSION['admin_csrf'])) {
|
||
$_SESSION['admin_csrf'] = bin2hex(random_bytes(16));
|
||
}
|
||
$csrf = $_SESSION['admin_csrf'];
|
||
|
||
$cfgPath = __DIR__ . '/includes/config.inc.php';
|
||
$bakDir = dirname($cfgPath) . '/backups';
|
||
|
||
function h(string $s): string
|
||
{
|
||
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Helper: apply backup retention — delete oldest .bak files beyond the limit.
|
||
// Only touches files with names matching *.bak inside the expected backup dir.
|
||
// ---------------------------------------------------------------------------
|
||
function billing_admin_apply_retention(string $dir, int $retention): void
|
||
{
|
||
$retention = max(1, min(10, $retention));
|
||
if (!is_dir($dir)) {
|
||
return;
|
||
}
|
||
$files = glob($dir . '/*.bak');
|
||
if (!is_array($files) || count($files) <= $retention) {
|
||
return;
|
||
}
|
||
// Sort oldest first (by file modification time)
|
||
usort($files, static function (string $a, string $b): int {
|
||
return filemtime($a) <=> 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 '<?php' . "\n"
|
||
. '###############################################' . "\n"
|
||
. '# Website Database Configuration' . "\n"
|
||
. '# This file contains the database connection' . "\n"
|
||
. '# settings for the billing website.' . "\n"
|
||
. '#' . "\n"
|
||
. '# Managed via Admin > 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), '<?php') !== 0) {
|
||
$status = 'Config must start with <?php';
|
||
$statusType = 'error';
|
||
} else {
|
||
// Backup then write (admin-only operation; 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, $newRaw, LOCK_EX) === false) {
|
||
$status = 'Failed to write config file.';
|
||
$statusType = 'error';
|
||
} else {
|
||
[$lintOk, $lintOut] = billing_admin_lint($cfgPath);
|
||
if (!$lintOk) {
|
||
@copy($bakName, $cfgPath); // rollback
|
||
$status = 'Syntax error detected; changes rolled back. Lint: ' . h($lintOut);
|
||
$statusType = 'error';
|
||
} else {
|
||
// Apply backup retention from config
|
||
$retentionNow = max(1, min(10, (int)($SITE_CONFIG_BACKUP_RETENTION ?? 5)));
|
||
billing_admin_apply_retention($bakDir, $retentionNow);
|
||
|
||
$status = 'Config saved successfully. Backup: ' . basename($bakName);
|
||
$statusType = 'success';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Always read current raw content from disk for the raw editor
|
||
$currentText = '';
|
||
if (is_readable($cfgPath)) {
|
||
$currentText = file_get_contents($cfgPath);
|
||
}
|
||
|
||
// List current backups for display
|
||
$bakFiles = is_dir($bakDir) ? (array)glob($bakDir . '/*.bak') : [];
|
||
rsort($bakFiles); // newest first
|
||
|
||
?>
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Admin — Edit Config</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link rel="stylesheet" href="css/header.css">
|
||
<style>
|
||
.cfg-section { background:#fff; border:1px solid #ddd; border-radius:6px; padding:20px 24px; margin-bottom:28px; }
|
||
.cfg-section h2 { margin-top:0; color:#333; border-bottom:2px solid #eee; padding-bottom:8px; }
|
||
.field-group { margin-bottom:18px; }
|
||
.field-group label { display:block; font-weight:600; color:#333; margin-bottom:4px; }
|
||
.field-help { font-size:0.85em; color:#666; margin-bottom:6px; }
|
||
.field-group input[type=text],
|
||
.field-group input[type=password],
|
||
.field-group input[type=number],
|
||
.field-group select { width:100%; max-width:520px; padding:8px 10px; border:1px solid #ccc;
|
||
border-radius:4px; font-size:1em; box-sizing:border-box; }
|
||
.field-group .pw-wrap { display:flex; gap:6px; align-items:center; max-width:520px; }
|
||
.field-group .pw-wrap input { flex:1; }
|
||
.btn-show { padding:8px 14px; font-size:0.9em; border:1px solid #aaa; border-radius:4px;
|
||
background:#f5f5f5; cursor:pointer; white-space:nowrap; }
|
||
.status-box { padding:12px 16px; border-radius:4px; margin-bottom:18px; font-weight:600; }
|
||
.status-success { background:#d4edda; color:#155724; border:1px solid #c3e6cb; }
|
||
.status-error { background:#f8d7da; color:#721c24; border:1px solid #f5c6cb; }
|
||
.status-info { background:#d1ecf1; color:#0c5460; border:1px solid #bee5eb; }
|
||
.panel-badge { background:#e8f4fd; border:1px solid #9ec8f0; color:#1a5276; padding:10px 14px;
|
||
border-radius:4px; margin-bottom:18px; font-size:0.9em; }
|
||
.readonly-field { background:#f4f4f4; color:#555; cursor:not-allowed; }
|
||
.warn-box { background:#fff3cd; border:1px solid #ffc107; color:#856404; padding:10px 14px;
|
||
border-radius:4px; margin-bottom:14px; font-size:0.9em; }
|
||
.save-row { margin:14px 0; }
|
||
.save-row button { padding:10px 24px; font-size:1em; font-weight:600; }
|
||
.bak-list { font-size:0.85em; color:#555; margin-top:4px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container-wide panel">
|
||
<h1>Edit Site Config</h1>
|
||
|
||
<?php if ($status): ?>
|
||
<div class="status-box status-<?php echo h($statusType); ?>"><?php echo h($status); ?></div>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!empty($billing_config_warning)): ?>
|
||
<div class="warn-box">⚠️ <?php echo h($billing_config_warning); ?></div>
|
||
<?php endif; ?>
|
||
|
||
<!-- ===================================================================
|
||
SECTION A: Interactive form
|
||
==================================================================== -->
|
||
<div class="cfg-section">
|
||
<h2>Site Settings</h2>
|
||
|
||
<?php if ($panelMode): ?>
|
||
<div class="panel-badge">
|
||
ℹ️ <strong>Panel-integrated mode.</strong>
|
||
Database settings are managed by the panel and synced automatically from
|
||
<code><?php echo h($panelCfgPath); ?></code>.
|
||
They are shown below for reference only.
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<form method="post" action="">
|
||
<input type="hidden" name="csrf" value="<?php echo h($csrf); ?>">
|
||
<input type="hidden" name="action" value="save_form">
|
||
|
||
<!-- DB read-only info (panel mode) -->
|
||
<?php if ($panelMode): ?>
|
||
<div class="field-group">
|
||
<label>Database Host</label>
|
||
<div class="field-help">Managed by the panel config. Edit the panel's <code>includes/config.inc.php</code> to change.</div>
|
||
<input type="text" class="readonly-field" value="<?php echo h((string)($db_host ?? '')); ?>" readonly>
|
||
</div>
|
||
<div class="field-group">
|
||
<label>Database Name</label>
|
||
<input type="text" class="readonly-field" value="<?php echo h((string)($db_name ?? '')); ?>" readonly>
|
||
</div>
|
||
<div class="field-group">
|
||
<label>Table Prefix</label>
|
||
<input type="text" class="readonly-field" value="<?php echo h((string)($table_prefix ?? '')); ?>" readonly>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Site Base URL -->
|
||
<div class="field-group">
|
||
<label for="cfg_base_url">Site Base URL</label>
|
||
<div class="field-help">
|
||
Full base URL <strong>without trailing slash</strong> (e.g. <code>https://gameservers.world</code>).
|
||
Leave empty to use relative paths. Used to compute the full public PayPal webhook URL.
|
||
</div>
|
||
<input type="text" id="cfg_base_url" name="SITE_BASE_URL"
|
||
value="<?php echo h((string)$cfgVals['SITE_BASE_URL']); ?>"
|
||
placeholder="https://example.com">
|
||
</div>
|
||
|
||
<!-- Site Background -->
|
||
<div class="field-group">
|
||
<label for="cfg_bg">Site Background Image</label>
|
||
<div class="field-help">
|
||
Path to background image relative to the billing site root (e.g. <code>images/dark.jpg</code>).
|
||
</div>
|
||
<input type="text" id="cfg_bg" name="SITE_BACKGROUND"
|
||
value="<?php echo h((string)$cfgVals['SITE_BACKGROUND']); ?>"
|
||
placeholder="images/dark.jpg">
|
||
</div>
|
||
|
||
<!-- Data Directory -->
|
||
<div class="field-group">
|
||
<label for="cfg_datadir">Site Data Directory</label>
|
||
<div class="field-help">
|
||
Absolute path where payment webhook JSON files are stored.
|
||
Leave empty to use the default: <code>modules/billing/data/</code>.
|
||
</div>
|
||
<input type="text" id="cfg_datadir" name="SITE_DATA_DIR"
|
||
value="<?php echo h((string)$cfgVals['SITE_DATA_DIR']); ?>"
|
||
placeholder="(default: billing/data/)">
|
||
</div>
|
||
|
||
<hr style="border:none;border-top:1px solid #eee;margin:24px 0;">
|
||
<h3 style="margin-top:0;color:#333;">PayPal Configuration</h3>
|
||
|
||
<?php
|
||
$isSandboxMode = ($cfgVals['paypal_mode'] ?? 'sandbox') !== 'live';
|
||
$modeLabel = $isSandboxMode ? '🟡 Sandbox (test mode)' : '🟢 Live (real payments)';
|
||
$modeBadgeClass = $isSandboxMode ? 'status-info' : 'status-success';
|
||
?>
|
||
<div class="status-box <?php echo h($modeBadgeClass); ?>" style="margin-bottom:14px;font-size:0.95em;">
|
||
Currently active PayPal mode: <strong><?php echo h($modeLabel); ?></strong>
|
||
</div>
|
||
|
||
<!-- PayPal Mode -->
|
||
<div class="field-group">
|
||
<label for="cfg_mode">PayPal Mode</label>
|
||
<div class="field-help">
|
||
<strong>Sandbox</strong> uses test credentials and the PayPal sandbox API — safe for development.
|
||
<strong>Live</strong> processes real payments. Switch only after configuring live credentials.
|
||
</div>
|
||
<select id="cfg_mode" name="paypal_mode">
|
||
<option value="sandbox" <?php echo $isSandboxMode ? 'selected' : ''; ?>>Sandbox (test mode)</option>
|
||
<option value="live" <?php echo !$isSandboxMode ? 'selected' : ''; ?>>Live (real payments)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Sandbox credentials -->
|
||
<h4 style="color:#555;margin:20px 0 8px;">Sandbox Credentials</h4>
|
||
<div class="field-group">
|
||
<label for="cfg_sb_id">Sandbox Client ID</label>
|
||
<div class="field-help">Found in PayPal Developer Dashboard → sandbox app. Safe to expose in browser JS.</div>
|
||
<input type="text" id="cfg_sb_id" name="paypal_sandbox_client_id"
|
||
value="<?php echo h((string)$cfgVals['paypal_sandbox_client_id']); ?>"
|
||
placeholder="AfvY_... or sandbox client ID">
|
||
</div>
|
||
<div class="field-group">
|
||
<label for="cfg_sb_sec">Sandbox Client Secret</label>
|
||
<div class="field-help"><strong>Server-side only</strong> — never sent to the browser. Leave blank to keep existing value.</div>
|
||
<div class="pw-wrap">
|
||
<input type="password" id="cfg_sb_sec" name="paypal_sandbox_client_secret"
|
||
placeholder="<?php echo $cfgVals['paypal_sandbox_client_secret'] !== '' ? '(set — leave blank to keep)' : '(not set)'; ?>"
|
||
autocomplete="new-password">
|
||
<button type="button" class="btn-show"
|
||
onclick="var f=document.getElementById('cfg_sb_sec');f.type=f.type==='password'?'text':'password';this.textContent=f.type==='password'?'Show':'Hide';">Show</button>
|
||
</div>
|
||
</div>
|
||
<div class="field-group">
|
||
<label for="cfg_sb_wh">Sandbox Webhook ID</label>
|
||
<div class="field-help">
|
||
Webhook ID from your PayPal sandbox app (for signature verification).
|
||
Leave empty to skip verification in sandbox mode (OK for initial setup).
|
||
</div>
|
||
<input type="text" id="cfg_sb_wh" name="paypal_sandbox_webhook_id"
|
||
value="<?php echo h((string)$cfgVals['paypal_sandbox_webhook_id']); ?>"
|
||
placeholder="Sandbox Webhook ID">
|
||
</div>
|
||
|
||
<!-- Live credentials -->
|
||
<h4 style="color:#555;margin:20px 0 8px;">Live Credentials</h4>
|
||
<div class="field-group">
|
||
<label for="cfg_lv_id">Live Client ID</label>
|
||
<div class="field-help">From your PayPal live app. Leave blank until ready for production.</div>
|
||
<input type="text" id="cfg_lv_id" name="paypal_live_client_id"
|
||
value="<?php echo h((string)$cfgVals['paypal_live_client_id']); ?>"
|
||
placeholder="Live Client ID">
|
||
</div>
|
||
<div class="field-group">
|
||
<label for="cfg_lv_sec">Live Client Secret</label>
|
||
<div class="field-help"><strong>Server-side only.</strong> Leave blank to keep existing value.</div>
|
||
<div class="pw-wrap">
|
||
<input type="password" id="cfg_lv_sec" name="paypal_live_client_secret"
|
||
placeholder="<?php echo $cfgVals['paypal_live_client_secret'] !== '' ? '(set — leave blank to keep)' : '(not set)'; ?>"
|
||
autocomplete="new-password">
|
||
<button type="button" class="btn-show"
|
||
onclick="var f=document.getElementById('cfg_lv_sec');f.type=f.type==='password'?'text':'password';this.textContent=f.type==='password'?'Show':'Hide';">Show</button>
|
||
</div>
|
||
</div>
|
||
<div class="field-group">
|
||
<label for="cfg_lv_wh">Live Webhook ID</label>
|
||
<div class="field-help">Webhook ID from your PayPal live app (for signature verification).</div>
|
||
<input type="text" id="cfg_lv_wh" name="paypal_live_webhook_id"
|
||
value="<?php echo h((string)$cfgVals['paypal_live_webhook_id']); ?>"
|
||
placeholder="Live Webhook ID">
|
||
</div>
|
||
|
||
<!-- Webhook path + computed URL -->
|
||
<h4 style="color:#555;margin:20px 0 8px;">Webhook Endpoint</h4>
|
||
<div class="field-help" style="margin-bottom:10px;">
|
||
PayPal requires a <strong>full public HTTPS URL</strong> to deliver webhook events.
|
||
Set your Site Base URL above, then copy the computed URL below into your PayPal app's webhook configuration.
|
||
</div>
|
||
<div class="field-group">
|
||
<label for="cfg_wh_path">Webhook Path</label>
|
||
<div class="field-help">Path relative to the billing site root (must start with <code>/</code>). Default: <code>/paypal/webhook.php</code></div>
|
||
<input type="text" id="cfg_wh_path" name="paypal_webhook_path"
|
||
value="<?php echo h((string)$cfgVals['paypal_webhook_path']); ?>"
|
||
placeholder="/paypal/webhook.php"
|
||
oninput="updateWebhookUrl()">
|
||
</div>
|
||
<div class="field-group">
|
||
<label>Computed Full Webhook URL <small style="font-weight:normal;color:#888;">(read-only — paste this into PayPal)</small></label>
|
||
<div class="field-help">
|
||
This is the URL PayPal will POST webhook events to.
|
||
It must be publicly accessible over HTTPS before enabling live mode.
|
||
</div>
|
||
<input type="text" id="computed_webhook_url"
|
||
class="readonly-field"
|
||
value="<?php echo h($computedWebhookUrl); ?>"
|
||
readonly
|
||
style="font-family:monospace;color:#333;background:#f0f4ff;">
|
||
<button type="button" class="btn-show" style="margin-top:4px;"
|
||
onclick="navigator.clipboard.writeText(document.getElementById('computed_webhook_url').value).then(function(){this.textContent='Copied!';setTimeout(function(){document.querySelector('[onclick*=computed_webhook_url]').textContent='Copy';},2000);}.bind(this));">Copy</button>
|
||
</div>
|
||
<script>
|
||
function updateWebhookUrl() {
|
||
var base = document.getElementById('cfg_base_url');
|
||
var path = document.getElementById('cfg_wh_path');
|
||
var out = document.getElementById('computed_webhook_url');
|
||
if (!base || !path || !out) return;
|
||
var b = base.value.replace(/\/+$/, '');
|
||
var p = path.value.replace(/^([^\/])/, '/$1');
|
||
out.value = b + p;
|
||
}
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
var base = document.getElementById('cfg_base_url');
|
||
if (base) base.addEventListener('input', updateWebhookUrl);
|
||
});
|
||
</script>
|
||
|
||
<hr style="border:none;border-top:1px solid #eee;margin:24px 0;">
|
||
<h3 style="margin-top:0;color:#333;">Backup Settings</h3>
|
||
|
||
<!-- Backup Retention -->
|
||
<div class="field-group">
|
||
<label for="cfg_retention">Config Backup Retention</label>
|
||
<div class="field-help">
|
||
Number of config backups to keep (1–10). The oldest backup beyond this limit is
|
||
deleted after each save. Backups are stored in
|
||
<code><?php echo h($bakDir); ?></code>.
|
||
</div>
|
||
<input type="number" id="cfg_retention" name="backup_retention"
|
||
value="<?php echo (int)$cfgVals['backup_retention']; ?>"
|
||
min="1" max="10" style="max-width:100px;">
|
||
</div>
|
||
|
||
<div class="save-row">
|
||
<button type="submit">💾 Save Settings</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- ===================================================================
|
||
SECTION B: PayPal Diagnostics
|
||
==================================================================== -->
|
||
<?php
|
||
// Gather diagnostics data
|
||
$diag_mode = $cfgVals['paypal_mode'] ?? 'sandbox';
|
||
$diag_is_sandbox = $diag_mode !== 'live';
|
||
$diag_sb_id_set = $cfgVals['paypal_sandbox_client_id'] !== '';
|
||
$diag_sb_sec_set = $cfgVals['paypal_sandbox_client_secret'] !== '';
|
||
$diag_sb_wh_set = $cfgVals['paypal_sandbox_webhook_id'] !== '';
|
||
$diag_lv_id_set = $cfgVals['paypal_live_client_id'] !== '';
|
||
$diag_lv_sec_set = $cfgVals['paypal_live_client_secret'] !== '';
|
||
$diag_lv_wh_set = $cfgVals['paypal_live_webhook_id'] !== '';
|
||
$diag_wh_path = $cfgVals['paypal_webhook_path'] ?? '/paypal/webhook.php';
|
||
$diag_wh_full_url = $computedWebhookUrl;
|
||
$diag_wh_file = __DIR__ . ltrim($diag_wh_path, '/');
|
||
$diag_wh_exists = file_exists($diag_wh_file);
|
||
|
||
// Active mode credential check
|
||
$diag_active_id_set = $diag_is_sandbox ? $diag_sb_id_set : $diag_lv_id_set;
|
||
$diag_active_sec_set = $diag_is_sandbox ? $diag_sb_sec_set : $diag_lv_sec_set;
|
||
$diag_active_wh_set = $diag_is_sandbox ? $diag_sb_wh_set : $diag_lv_wh_set;
|
||
|
||
function diag_badge(bool $ok, string $yes = 'Yes', string $no = 'No'): string {
|
||
$cls = $ok ? 'background:#d4edda;color:#155724;border:1px solid #c3e6cb;' : 'background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;';
|
||
$label = $ok ? $yes : $no;
|
||
return '<span style="' . $cls . 'padding:2px 8px;border-radius:3px;font-size:0.85em;font-weight:600;">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span>';
|
||
}
|
||
|
||
// Last webhook events
|
||
$diag_recent_events = [];
|
||
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_';
|
||
$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;
|
||
}
|
||
}
|
||
mysqli_close($diag_db);
|
||
}
|
||
} catch (Throwable $e) {
|
||
// non-fatal
|
||
}
|
||
?>
|
||
<div class="cfg-section">
|
||
<h2>PayPal Diagnostics</h2>
|
||
<table style="border-collapse:collapse;width:100%;font-size:0.9em;">
|
||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;width:260px;">Current mode</td><td style="padding:6px 8px;"><strong><?php echo h($diag_mode); ?></strong></td></tr>
|
||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Active Client ID configured</td><td style="padding:6px 8px;"><?php echo diag_badge($diag_active_id_set); ?></td></tr>
|
||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Active Client Secret configured</td><td style="padding:6px 8px;"><?php echo diag_badge($diag_active_sec_set); ?></td></tr>
|
||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Active Webhook ID configured</td><td style="padding:6px 8px;"><?php echo diag_badge($diag_active_wh_set, 'Yes', 'No (signature verification skipped)'); ?></td></tr>
|
||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Sandbox credentials</td><td style="padding:6px 8px;">ID: <?php echo diag_badge($diag_sb_id_set); ?> Secret: <?php echo diag_badge($diag_sb_sec_set); ?> Webhook ID: <?php echo diag_badge($diag_sb_wh_set); ?></td></tr>
|
||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Live credentials</td><td style="padding:6px 8px;">ID: <?php echo diag_badge($diag_lv_id_set); ?> Secret: <?php echo diag_badge($diag_lv_sec_set); ?> Webhook ID: <?php echo diag_badge($diag_lv_wh_set); ?></td></tr>
|
||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Webhook path</td><td style="padding:6px 8px;"><code><?php echo h($diag_wh_path); ?></code></td></tr>
|
||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Full public webhook URL</td><td style="padding:6px 8px;"><code style="word-break:break-all;"><?php echo h($diag_wh_full_url ?: '(Site Base URL not set)'); ?></code></td></tr>
|
||
<tr><td style="padding:6px 8px;color:#555;">Webhook file exists on disk</td><td style="padding:6px 8px;"><?php echo diag_badge($diag_wh_exists, 'Yes — ' . h($diag_wh_file), 'No — ' . h($diag_wh_file) . ' not found'); ?></td></tr>
|
||
</table>
|
||
|
||
<?php if (!empty($diag_recent_events)): ?>
|
||
<h4 style="margin-top:18px;color:#555;">Recent Webhook Events</h4>
|
||
<table style="border-collapse:collapse;width:100%;font-size:0.85em;">
|
||
<thead><tr style="background:#f8f9fa;">
|
||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">PayPal Event ID</th>
|
||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">Type</th>
|
||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">Status</th>
|
||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">Received</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<?php foreach ($diag_recent_events as $ev): ?>
|
||
<tr style="border-bottom:1px solid #eee;">
|
||
<td style="padding:5px 8px;font-family:monospace;font-size:0.85em;"><?php echo h($ev['paypal_event_id'] ?: '—'); ?></td>
|
||
<td style="padding:5px 8px;"><?php echo h($ev['event_type']); ?></td>
|
||
<td style="padding:5px 8px;"><?php echo diag_badge($ev['processing_status'] === 'processed', $ev['processing_status'], $ev['processing_status']); ?></td>
|
||
<td style="padding:5px 8px;"><?php echo h($ev['created_at']); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php elseif (empty($diag_recent_events)): ?>
|
||
<p style="color:#888;font-size:0.9em;margin-top:12px;">No webhook events recorded yet. Events will appear here after PayPal delivers the first webhook to <code><?php echo h($diag_wh_full_url ?: $diag_wh_path); ?></code>.</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- ===================================================================
|
||
SECTION C: Raw PHP editor
|
||
==================================================================== -->
|
||
<div class="cfg-section">
|
||
<h2>Advanced: Raw Config Editor</h2>
|
||
<div class="warn-box">
|
||
⚠️ <strong>Warning:</strong> 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.
|
||
</div>
|
||
|
||
<form method="post" action="">
|
||
<input type="hidden" name="csrf" value="<?php echo h($csrf); ?>">
|
||
<input type="hidden" name="action" value="save_raw">
|
||
<div class="save-row"><button type="submit">💾 Save Raw Config</button></div>
|
||
<textarea name="config_text" rows="28"
|
||
style="width:100%;font-family:monospace;font-size:0.9em;border:1px solid #ccc;border-radius:4px;padding:10px;box-sizing:border-box;"
|
||
><?php echo h((string)$currentText); ?></textarea>
|
||
<div class="save-row"><button type="submit">💾 Save Raw Config</button></div>
|
||
</form>
|
||
|
||
<p style="margin-top:16px;">
|
||
<strong>Backup directory:</strong> <code><?php echo h($bakDir); ?></code>
|
||
<?php if ($bakFiles): ?>
|
||
<br><span class="bak-list">
|
||
<?php echo count($bakFiles); ?> backup(s) stored.
|
||
Most recent: <code><?php echo h(basename($bakFiles[0])); ?></code>
|
||
</span>
|
||
<?php else: ?>
|
||
<br><span class="bak-list">No backups yet.</span>
|
||
<?php endif; ?>
|
||
</p>
|
||
</div>
|
||
|
||
</div>
|
||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||
</body>
|
||
</html>
|