diff --git a/modules/billing/admin.php b/modules/billing/admin.php
index 42b0bf1b..b2a0241b 100644
--- a/modules/billing/admin.php
+++ b/modules/billing/admin.php
@@ -3,32 +3,9 @@
require_once(__DIR__ . '/includes/admin_auth.php');
require_once(__DIR__ . '/includes/config_loader.php');
-// Ensure site variables are defined regardless of which config was loaded.
-// The panel config (loaded first by config_loader) does not define these billing-specific
-// variables. Try loading them from the billing config.inc.php if not already set.
-if (!isset($SITE_BASE_URL) || !isset($SITE_DATA_DIR)) {
- $billingLocalCfg = __DIR__ . '/includes/config.inc.php';
- if (is_readable($billingLocalCfg) && defined('BILLING_CONFIG_PATH') && BILLING_CONFIG_PATH !== $billingLocalCfg) {
- // Panel config was loaded; read billing config vars without re-running DB setup.
- // Use a temporary scope to avoid overwriting DB credentials.
- $__billing_cfg_vars = (static function() use ($billingLocalCfg) {
- $SITE_BASE_URL = '';
- $SITE_DATA_DIR = '';
- @include $billingLocalCfg;
- return ['base' => $SITE_BASE_URL ?? '', 'data' => $SITE_DATA_DIR ?? ''];
- })();
- if (!isset($SITE_BASE_URL)) $SITE_BASE_URL = $__billing_cfg_vars['base'];
- if (!isset($SITE_DATA_DIR)) $SITE_DATA_DIR = $__billing_cfg_vars['data'];
- unset($__billing_cfg_vars, $billingLocalCfg);
- }
-}
-// Final safe defaults if still not set.
-if (!isset($SITE_BASE_URL)) {
- $SITE_BASE_URL = '';
-}
-if (!isset($SITE_DATA_DIR)) {
- $SITE_DATA_DIR = realpath(__DIR__ . '/data') ?: (__DIR__ . '/data');
-}
+// 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');
diff --git a/modules/billing/admin_config.php b/modules/billing/admin_config.php
index 8cf3aa15..515ef13c 100644
--- a/modules/billing/admin_config.php
+++ b/modules/billing/admin_config.php
@@ -1,5 +1,20 @@
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) . '"';
+ };
+
+ $sandbox = (bool)$vals['paypal_sandbox'];
+ $retention = max(1, min(10, (int)($vals['backup_retention'] ?? 5)));
+ $baseUrl = trim($vals['SITE_BASE_URL'] ?? '');
+ $bg = trim($vals['SITE_BACKGROUND'] ?? 'images/dark.jpg');
+ $dataDir = trim($vals['SITE_DATA_DIR'] ?? '');
+
+ $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 used by admin pages to build absolute image previews.' . "\n"
+ . '// Leave empty to prefer relative paths (local folder).' . "\n"
+ . '$SITE_BASE_URL = ' . $q($baseUrl) . ';' . "\n"
+ . "\n"
+ . '// Normalize: ensure either empty or ends without trailing slash' . "\n"
+ . '$SITE_BASE_URL = trim((string)$SITE_BASE_URL);' . "\n"
+ . "\n"
+ . '// Site-wide background image (relative to site root).' . "\n"
+ . '$SITE_BACKGROUND = ' . $q($bg) . ';' . "\n"
+ . '// Normalize' . "\n"
+ . '$SITE_BACKGROUND = trim((string)$SITE_BACKGROUND);' . "\n"
+ . "\n"
+ . '// Data directory for persisted payment webhook JSON files (relative to repo root)' . "\n"
+ . $dataDirLine . "\n"
+ . "\n"
+ . '// PayPal configuration — set credentials here, never in API files' . "\n"
+ . '$paypal_sandbox = ' . ($sandbox ? 'true' : 'false') . '; // Set to false for live payments' . "\n"
+ . '$paypal_client_id = ' . $q($vals['paypal_client_id'] ?? '') . '; // Your PayPal Client ID' . "\n"
+ . '$paypal_client_secret = ' . $q($vals['paypal_client_secret'] ?? '') . '; // Your PayPal Client Secret' . "\n"
+ . '$paypal_webhook_id = ' . $q($vals['paypal_webhook_id'] ?? '') . '; // Your PayPal Webhook ID' . "\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' => isset($SITE_DATA_DIR) ? $SITE_DATA_DIR : '',
+ 'paypal_sandbox' => $paypal_sandbox ?? true,
+ 'paypal_client_id' => $paypal_client_id ?? '',
+ 'paypal_client_secret' => $paypal_client_secret ?? '',
+ 'paypal_webhook_id' => $paypal_webhook_id ?? '',
+ 'backup_retention' => $SITE_CONFIG_BACKUP_RETENTION ?? 5,
+];
+
+// Detect panel-mode (DB settings are managed by the panel)
+$panelMode = defined('BILLING_PANEL_CONFIG_PATH');
+$panelCfgPath = $panelMode ? BILLING_PANEL_CONFIG_PATH : null;
$status = '';
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- $token = $_POST['csrf'] ?? '';
- if (!hash_equals($csrf, (string)$token)) {
- $status = 'Invalid CSRF token.';
- } else {
- if (!is_writable($cfgPath)) {
- $status = 'Config file not writable: ' . h($cfgPath);
- } else {
- // Backup
- $bakDir = dirname($cfgPath) . '/backups';
- @mkdir($bakDir, 0775, true);
- $bakName = $bakDir . '/config.inc.php.' . date('Ymd-His') . '.' . bin2hex(random_bytes(4)) . '.bak';
- if (!copy($cfgPath, $bakName)) {
- $status = 'Failed to create backup. Aborting.';
- } else {
- $new = $_POST['config_text'] ?? '';
- // Basic safety: ensure the file still starts with &1', $out, $rc);
- $lintOutput = is_array($out) ? implode("\n", $out) : (string)$out;
- if ($rc !== 0) {
- $lintOk = false;
- }
- } else {
- $lintOutput = 'PHP executable not found for linting; skipping post-save syntax check.';
- }
+$statusType = 'info'; // 'success' | 'error' | 'info'
- if (!$lintOk) {
- // rollback
- @copy($bakName, $cfgPath);
- $status = 'Syntax error detected in saved config. Changes rolled back. Lint output: ' . h($lintOutput);
- } else {
- $status = 'Config saved successfully. Backup: ' . basename($bakName) . (strlen($lintOutput) ? ' (lint: '.h($lintOutput).')' : '');
- // reload values
- require_once($cfgPath);
- }
- }
+// ---------------------------------------------------------------------------
+// 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_sandbox' => (($_POST['paypal_sandbox'] ?? 'true') === 'true'),
+ 'paypal_client_id' => trim($_POST['paypal_client_id'] ?? ''),
+ 'paypal_client_secret' => trim($_POST['paypal_client_secret'] ?? ''),
+ 'paypal_webhook_id' => trim($_POST['paypal_webhook_id'] ?? ''),
+ 'backup_retention' => (int)($_POST['backup_retention'] ?? 5),
+ ];
+
+ // 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
+ $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), '
@@ -86,20 +317,232 @@ if (is_readable($cfgPath)) {
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.
+
+
+
+
+
+
+
+
+
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.
+
+
+
-
Backups are stored in
diff --git a/modules/billing/includes/config.example.php b/modules/billing/includes/config.example.php
index a675b925..dbee9b4f 100644
--- a/modules/billing/includes/config.example.php
+++ b/modules/billing/includes/config.example.php
@@ -41,3 +41,7 @@ $paypal_sandbox = true; // Set to false for live payments
$paypal_client_id = ''; // Your PayPal Client ID
$paypal_client_secret = ''; // Your PayPal Client Secret
$paypal_webhook_id = ''; // Your PayPal Webhook ID (for webhook signature verification)
+
+# --- Admin config backup retention ---
+# Number of config backups to keep (1–10). Oldest backups beyond this limit are deleted.
+$SITE_CONFIG_BACKUP_RETENTION = 5;
diff --git a/modules/billing/includes/config.inc.php b/modules/billing/includes/config.inc.php
index b36a465d..b389e452 100644
--- a/modules/billing/includes/config.inc.php
+++ b/modules/billing/includes/config.inc.php
@@ -36,4 +36,7 @@ $paypal_sandbox = true; // Set to false for live payments
$paypal_client_id = ''; // Your PayPal Client ID
$paypal_client_secret = ''; // Your PayPal Client Secret
$paypal_webhook_id = ''; // Your PayPal Webhook ID (for webhook signature verification)
+
+// Admin config backup retention: how many backups to keep (1–10). Default 5.
+$SITE_CONFIG_BACKUP_RETENTION = 5;
?>
diff --git a/modules/billing/includes/config_loader.php b/modules/billing/includes/config_loader.php
index 3ee9f533..3f0699dc 100644
--- a/modules/billing/includes/config_loader.php
+++ b/modules/billing/includes/config_loader.php
@@ -2,73 +2,235 @@
/**
* Billing config loader
*
- * Priority order (panel-first):
- * 1. /includes/config.inc.php (active panel config — always wins when present)
- * 2. modules/billing/includes/config.inc.php (local billing config — standalone fallback only)
+ * Load order:
+ * 1. modules/billing/includes/config.inc.php — always loaded first; contains billing-specific
+ * settings (PayPal credentials, SITE_BASE_URL, SITE_BACKGROUND, SITE_DATA_DIR, backup
+ * retention, and default DB settings for standalone installs).
+ * 2. /includes/config.inc.php — read via regex (no side-effects) when the
+ * billing module is installed inside a GSP panel tree. DB variables extracted from the
+ * panel config override the billing config DB variables in memory so that the module
+ * always connects to the active panel database.
*
- * The panel config is preferred so that every billing page, migration, and schema
- * check automatically uses the database from the active installation. This prevents
- * a testing install from accidentally writing to a production database when the local
- * billing config.inc.php still contains hard-coded production credentials.
+ * Panel-child detection:
+ * Walk up from modules/billing/includes/ looking for the pattern
+ * /includes/config.inc.php that contains the GSP panel DB variables
+ * ($db_host, $db_user, $db_name, $table_prefix). Stop after six levels.
*
- * Standalone deployments (billing module deployed without the panel) should place
- * their own config.inc.php in modules/billing/includes/ as a fallback.
+ * Config sync:
+ * If the panel config DB variables differ from the billing config file on disk, the loader
+ * updates only the DB variable lines in billing/includes/config.inc.php so that subsequent
+ * page loads (and standalone tools) always see current credentials. The sync only runs when
+ * the file is writable. If it cannot write, a non-fatal admin-visible warning is set in
+ * $billing_config_warning.
+ *
+ * Standalone installs:
+ * When no panel config is found the billing config is used as-is; standalone mode is fully
+ * supported.
*/
if (defined('BILLING_CONFIG_LOADED')) {
return;
}
-$attempted = [];
-
-// Prefer the panel config so the billing module always uses the active installation's DB.
-$panelConfig = null;
-$projectRoot = realpath(__DIR__ . '/../../..');
-if ($projectRoot !== false) {
- $panelConfig = $projectRoot . '/includes/config.inc.php';
-} else {
- $panelConfig = __DIR__ . '/../../../includes/config.inc.php';
-}
-
-if ($panelConfig && is_readable($panelConfig)) {
- $attempted[] = $panelConfig;
- require_once $panelConfig;
- if (!defined('BILLING_CONFIG_PATH')) {
- define('BILLING_CONFIG_PATH', $panelConfig);
+// ---------------------------------------------------------------------------
+// Helper: extract DB variable values from a PHP config file without including it.
+// Uses regex on the raw file text so there are no side-effects.
+// ---------------------------------------------------------------------------
+if (!function_exists('_billing_extract_db_vars_from_file')) {
+ function _billing_extract_db_vars_from_file(string $path): array
+ {
+ $content = @file_get_contents($path);
+ if ($content === false) {
+ return [];
+ }
+ $result = [];
+ foreach (['db_host', 'db_port', 'db_user', 'db_pass', 'db_name', 'table_prefix', 'db_type'] as $var) {
+ // Match: $varname = "value"; or $varname="value"; (single or double quotes, no escaped quotes)
+ // Note: credentials containing escaped quotes or special chars are not supported by this
+ // regex-based extractor — use var_export() to write values and keep creds simple.
+ if (preg_match('/^\s*\$' . preg_quote($var, '/') . '\s*=\s*"([^"]*)"/m', $content, $m) ||
+ preg_match('/^\s*\$' . preg_quote($var, '/') . "\s*=\s*'([^']*)'/m", $content, $m)) {
+ $result[$var] = $m[1];
+ }
+ }
+ return $result;
}
- define('BILLING_CONFIG_LOADED', true);
- return;
}
-$attempted[] = $panelConfig;
+// ---------------------------------------------------------------------------
+// Helper: update DB variable lines in the billing config file without touching
+// any other settings. Returns true when the file was updated, false otherwise.
+// ---------------------------------------------------------------------------
+if (!function_exists('_billing_sync_db_vars_to_file')) {
+ function _billing_sync_db_vars_to_file(string $filePath, array $panelVars): bool
+ {
+ if (!is_writable($filePath)) {
+ return false;
+ }
+ $content = file_get_contents($filePath);
+ if ($content === false) {
+ return false;
+ }
+ $changed = false;
+ foreach (['db_host', 'db_port', 'db_user', 'db_pass', 'db_name', 'table_prefix', 'db_type'] as $var) {
+ if (!array_key_exists($var, $panelVars)) {
+ continue;
+ }
+ $newVal = $panelVars[$var];
+ // Match any existing assignment for this var (double or single quotes, no escaped quotes)
+ // Use var_export() to produce the replacement value so special characters are handled
+ // correctly (var_export produces a valid PHP string literal).
+ $pattern = '/^(\s*\$' . preg_quote($var, '/') . '\s*=\s*)["\'][^"\']*["\'](.*)$/m';
+ $exportedVal = var_export($newVal, true); // produces 'value' with proper escaping
+ $newLine = '${1}' . str_replace('\\', '\\\\', $exportedVal) . '${2}';
+ $updated = preg_replace($pattern, $newLine, $content, 1, $count);
+ if ($count > 0 && $updated !== $content) {
+ $content = (string)$updated;
+ $changed = true;
+ }
+ }
+ if (!$changed) {
+ return false;
+ }
+ return file_put_contents($filePath, $content, LOCK_EX) !== false;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Helper: locate the panel config by walking up ancestor directories.
+// Returns an absolute path when found, or null.
+// ---------------------------------------------------------------------------
+if (!function_exists('_billing_find_panel_config')) {
+ function _billing_find_panel_config(string $startDir): ?string
+ {
+ $dir = realpath($startDir);
+ if ($dir === false) {
+ return null;
+ }
+ // Walk up at most 6 levels (covers: includes/ → billing/ → modules/ → panel_root/)
+ for ($i = 0; $i < 6; $i++) {
+ $candidate = $dir . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'config.inc.php';
+ if (is_readable($candidate)) {
+ // Confirm it is a GSP/panel config by looking for $db_host and $table_prefix
+ $content = @file_get_contents($candidate);
+ if ($content !== false &&
+ strpos($content, '$db_host') !== false &&
+ strpos($content, '$table_prefix') !== false) {
+ return $candidate;
+ }
+ }
+ $parent = dirname($dir);
+ if ($parent === $dir) {
+ break; // reached filesystem root
+ }
+ $dir = $parent;
+ }
+ return null;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Step 1: Load the billing config (always — it holds billing-specific settings).
+// ---------------------------------------------------------------------------
+$billing_config_warning = null; // surfaced to admin pages when non-null
-// Fallback: local billing config (useful for standalone deployments where no panel is present).
$localConfig = __DIR__ . '/config.inc.php';
-if (is_readable($localConfig)) {
- $attempted[] = $localConfig;
- require_once $localConfig;
- if (!defined('BILLING_CONFIG_PATH')) {
- define('BILLING_CONFIG_PATH', $localConfig);
+if (!is_readable($localConfig)) {
+ // No billing config found — render an informative error for the admin.
+ $message = "GSP Billing module cannot find modules/billing/includes/config.inc.php.\n";
+ $message .= "Expected: " . $localConfig . "\n";
+ $message .= "\nCreate the file from the example (config.example.php) and fill in your settings.\n";
+ if (!headers_sent()) {
+ header('Content-Type: text/plain; charset=UTF-8', true, 500);
}
- define('BILLING_CONFIG_LOADED', true);
- return;
+ echo $message;
+ exit(1);
}
-$attempted[] = $localConfig;
+require_once $localConfig;
-$message = "GSP Billing module cannot find config.inc.php.\n";
-$message .= "Looked in:\n";
-foreach ((array)$attempted as $path) {
- if (!$path) {
- continue;
+if (!defined('BILLING_CONFIG_PATH')) {
+ define('BILLING_CONFIG_PATH', $localConfig);
+}
+
+// ---------------------------------------------------------------------------
+// Step 2: Child-of-panel detection.
+// ---------------------------------------------------------------------------
+$_billing_panel_config = _billing_find_panel_config(dirname(__DIR__, 2));
+
+if ($_billing_panel_config !== null) {
+ // Found a panel config — extract its DB variables (no side-effects).
+ $panelDbVars = _billing_extract_db_vars_from_file($_billing_panel_config);
+
+ if (!empty($panelDbVars)) {
+ // Override DB settings in the current scope with panel values.
+ foreach ($panelDbVars as $_bk => $_bv) {
+ $$_bk = $_bv;
+ }
+ unset($_bk, $_bv);
+
+ // Record which panel config was found (admin pages may display this).
+ if (!defined('BILLING_PANEL_CONFIG_PATH')) {
+ define('BILLING_PANEL_CONFIG_PATH', $_billing_panel_config);
+ }
+
+ // -------------------------------------------------------------------
+ // Step 3: Config sync — keep billing config.inc.php DB vars in sync.
+ // Only rewrite the file when the on-disk values actually differ so that
+ // we never touch the file on normal page loads where nothing changed.
+ // -------------------------------------------------------------------
+ $diskVars = _billing_extract_db_vars_from_file($localConfig);
+ $needsSync = false;
+ foreach ($panelDbVars as $k => $v) {
+ if (!array_key_exists($k, $diskVars) || $diskVars[$k] !== $v) {
+ $needsSync = true;
+ break;
+ }
+ }
+
+ if ($needsSync) {
+ if (!_billing_sync_db_vars_to_file($localConfig, $panelDbVars)) {
+ // Non-fatal: show admin warning; runtime DB vars are already overridden above.
+ $billing_config_warning =
+ 'Panel DB settings differ from billing/includes/config.inc.php but the file '
+ . 'is not writable. The billing module will use the panel DB settings for this '
+ . 'request, but consider updating file permissions or manually editing config.inc.php '
+ . 'to match the panel config at: ' . $_billing_panel_config;
+ }
+ }
}
- $message .= " - " . $path . "\n";
}
-$message .= "\nCopy your panel's includes/config.inc.php into modules/billing/includes/config.inc.php ";
-$message .= "or ensure the panel config is readable so the billing pages can load database settings.\n";
-if (!headers_sent()) {
- header('Content-Type: text/plain; charset=UTF-8', true, 500);
+unset($_billing_panel_config, $panelDbVars, $diskVars, $needsSync, $k, $v);
+
+// ---------------------------------------------------------------------------
+// Step 4: Apply safe defaults for billing-specific variables that may be absent
+// in older config files (never overwrite values already set by the config).
+// ---------------------------------------------------------------------------
+if (!isset($paypal_sandbox)) {
+ $paypal_sandbox = true;
}
-echo $message;
-exit(1);
+if (!isset($paypal_client_id)) {
+ $paypal_client_id = '';
+}
+if (!isset($paypal_client_secret)) {
+ $paypal_client_secret = '';
+}
+if (!isset($paypal_webhook_id)) {
+ $paypal_webhook_id = '';
+}
+if (!isset($SITE_BASE_URL)) {
+ $SITE_BASE_URL = '';
+}
+if (!isset($SITE_BACKGROUND)) {
+ $SITE_BACKGROUND = 'images/dark.jpg';
+}
+if (!isset($SITE_DATA_DIR)) {
+ $SITE_DATA_DIR = realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data';
+}
+if (!isset($SITE_CONFIG_BACKUP_RETENTION) || !is_int($SITE_CONFIG_BACKUP_RETENTION)) {
+ $SITE_CONFIG_BACKUP_RETENTION = 5;
+}
+$SITE_CONFIG_BACKUP_RETENTION = max(1, min(10, (int)$SITE_CONFIG_BACKUP_RETENTION));
+
+define('BILLING_CONFIG_LOADED', true);
diff --git a/modules/billing/timestamp.txt b/modules/billing/timestamp.txt
index 52fc9e18..722017ae 100644
--- a/modules/billing/timestamp.txt
+++ b/modules/billing/timestamp.txt
@@ -1 +1 @@
-Last Updated at 3:18pm on 2026-05-06
+Last Updated at 3:52pm on 2026-05-06