refactor: store config as .bak, rebuild on restore
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 18s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled

Instead of storing a sanitized configuration.php in the archive,
save it as configuration.php.bak with credentials stripped. No
configuration.php exists in the archive — it's rebuilt from the
.bak template + user-provided credentials during restore.

Backup side:
- configuration.php stored as configuration.php.bak (sanitized)
- No configuration.php in the archive (prevents accidental use)

MokoRestore side:
- Reads .bak as base template (preserves non-sensitive settings:
  debug, cache, SEF, editor, timezone, etc.)
- Replaces all sanitized fields with user input
- Clears proxy/Redis/TLS placeholders to empty strings
- Deletes .bak after successful rebuild
- Falls back to configuration.php for legacy backups

FileRestorer:
- Added configuration.php.bak to skip list
This commit is contained in:
Jonathan Miller
2026-06-14 15:30:14 -05:00
parent f43664dbef
commit 3a6354e648
4 changed files with 46 additions and 28 deletions
@@ -166,13 +166,13 @@ class BackupEngine
continue;
}
// Sanitize configuration.php — replace credentials with
// placeholders so backups are safe to store or transfer.
// MokoRestore prompts for these values during restore.
// Store configuration.php as .bak with credentials stripped.
// The restore process rebuilds a fresh configuration.php
// from user input + non-sensitive values from the .bak.
if ($relativePath === 'configuration.php') {
$sanitized = self::sanitizeConfiguration($fullPath);
$archiver->addFromString($relativePath, $sanitized);
$this->log('configuration.php sanitized (credentials replaced with placeholders)');
$archiver->addFromString('configuration.php.bak', $sanitized);
$this->log('configuration.php saved as .bak (credentials stripped)');
} else {
$archiver->addFile($fullPath, $relativePath);
}
@@ -22,10 +22,11 @@ class FileRestorer
/**
* Files that should never be overwritten during restore.
* configuration.php is handled separately by the RestoreEngine.
* configuration.php is rebuilt from .bak + user input by RestoreEngine.
*/
private const SKIP_FILES = [
'configuration.php',
'configuration.php.bak',
'.htaccess',
'web.config',
];
@@ -258,10 +258,14 @@ function actionExtract(array $data): array
$count = $zip->numFiles;
$zip->close();
// Try to read existing configuration.php for pre-filling.
// Sanitized backups contain [SANITIZED:field] placeholders — skip those.
// Pre-fill from configuration.php.bak (sanitized backup) or
// configuration.php (legacy/unsanitized backup). Skip [SANITIZED:] values.
$existingConfig = [];
$configFile = RESTORE_DIR . '/configuration.php';
$configFile = RESTORE_DIR . '/configuration.php.bak';
if (!is_file($configFile)) {
$configFile = RESTORE_DIR . '/configuration.php';
}
if (is_file($configFile)) {
$content = file_get_contents($configFile);
@@ -278,7 +282,6 @@ function actionExtract(array $data): array
foreach ($fieldMap as $phpField => $configKey) {
if (preg_match('/\$' . preg_quote($phpField, '/') . '\s*=\s*\'([^\']*)\'/', $content, $m)) {
// Skip sanitized placeholder values
if (strpos($m[1], '[SANITIZED:') === false) {
$existingConfig[$configKey] = $m[1];
}
@@ -396,12 +399,18 @@ function actionConfig(array $data): array
$tmpPath = RESTORE_DIR . '/tmp';
$logPath = RESTORE_DIR . '/administrator/logs';
$configFile = RESTORE_DIR . '/configuration.php';
$configPath = RESTORE_DIR . '/configuration.php';
$bakPath = RESTORE_DIR . '/configuration.php.bak';
if (is_file($configFile)) {
// Update existing configuration.php
$config = file_get_contents($configFile);
// Use .bak as the base template (preserves non-sensitive settings like
// debug, cache, SEF, editor, etc.). Fall back to existing config
// for legacy/unsanitized backups, or build from scratch if neither exists.
$basePath = is_file($bakPath) ? $bakPath : (is_file($configPath) ? $configPath : null);
if ($basePath !== null) {
$config = file_get_contents($basePath);
// Replace all credential and server-specific fields with user input
$replacements = [
'/\$host\s*=\s*\'[^\']*\'/' => "\$host = '{$host}'",
'/\$db\s*=\s*\'[^\']*\'/' => "\$db = '{$dbName}'",
@@ -418,24 +427,32 @@ function actionConfig(array $data): array
$replacements['/\$live_site\s*=\s*\'[^\']*\'/'] = "\$live_site = '{$livesite}'";
}
// Replace SMTP credentials (only if provided — leave existing values if blank)
if ($smtpHost !== '') {
$replacements['/\$smtphost\s*=\s*\'[^\']*\'/'] = "\$smtphost = '{$smtpHost}'";
}
if ($smtpUser !== '') {
$replacements['/\$smtpuser\s*=\s*\'[^\']*\'/'] = "\$smtpuser = '" . addcslashes($smtpUser, "'\\") . "'";
}
if ($smtpPass !== '') {
$replacements['/\$smtppass\s*=\s*\'[^\']*\'/'] = "\$smtppass = '" . addcslashes($smtpPass, "'\\") . "'";
}
// SMTP — always replace (clears sanitized placeholders even if blank)
$replacements['/\$smtphost\s*=\s*\'[^\']*\'/'] = "\$smtphost = '" . addcslashes($smtpHost, "'\\") . "'";
$replacements['/\$smtpuser\s*=\s*\'[^\']*\'/'] = "\$smtpuser = '" . addcslashes($smtpUser, "'\\") . "'";
$replacements['/\$smtppass\s*=\s*\'[^\']*\'/'] = "\$smtppass = '" . addcslashes($smtpPass, "'\\") . "'";
// Clear remaining sanitized placeholders (proxy, Redis, DB TLS)
$replacements['/\$proxy_user\s*=\s*\'[^\']*\'/'] = "\$proxy_user = ''";
$replacements['/\$proxy_pass\s*=\s*\'[^\']*\'/'] = "\$proxy_pass = ''";
$replacements['/\$redis_server_auth\s*=\s*\'[^\']*\'/'] = "\$redis_server_auth = ''";
$replacements['/\$session_redis_server_auth\s*=\s*\'[^\']*\'/'] = "\$session_redis_server_auth = ''";
$replacements['/\$dbsslkey\s*=\s*\'[^\']*\'/'] = "\$dbsslkey = ''";
$replacements['/\$dbsslcert\s*=\s*\'[^\']*\'/'] = "\$dbsslcert = ''";
$replacements['/\$dbsslca\s*=\s*\'[^\']*\'/'] = "\$dbsslca = ''";
foreach ($replacements as $pattern => $replacement) {
$config = preg_replace($pattern, $replacement, $config);
}
file_put_contents($configFile, $config);
file_put_contents($configPath, $config);
return ['success' => true, 'message' => 'configuration.php updated with new settings and fresh secret'];
// Remove .bak after successful rebuild
if (is_file($bakPath)) {
@unlink($bakPath);
}
return ['success' => true, 'message' => 'Joomla configuration rebuilt with fresh credentials and secret'];
}
// Create new configuration.php from scratch
@@ -281,10 +281,10 @@ class SteppedBackupEngine
continue;
}
// Sanitize Joomla config — replace credentials with placeholders
// Store config as .bak with credentials stripped — restore rebuilds it
if (basename($relativePath) === 'configuration.php' && dirname($relativePath) === '.') {
$sanitized = BackupEngine::sanitizeConfiguration($fullPath);
$zip->addFromString($relativePath, $sanitized);
$zip->addFromString('configuration.php.bak', $sanitized);
} else {
$zip->addFile($fullPath, $relativePath);
}