feat: MokoRestore post-restore resets + per-table conflict resolution (#131, #132)
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Build & Release / Promote to RC (pull_request) Failing after 13s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 53s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m0s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Build & Release / Promote to RC (pull_request) Failing after 13s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 53s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m0s
#131: Post-restore actions step in MokoRestore wizard: - Reset all passwords to temporary "changeme" with requireReset flag - Reset article hit counters to zero - Clear content versions (#__history) - Clear sessions (#__session) - Clear cache tables and filesystem cache - Auto-detect sanitized password hashes and prompt for reset #132: Per-table conflict resolution during database import: - New "Tables" step shows all tables from database.sql - Per-table dropdown: Replace / Skip / Merge / Data Only - Preset buttons: All Replace, All Skip, Everything except users - Skip mode skips all statements for that table - Merge mode uses INSERT IGNORE instead of INSERT INTO - Data Only skips DROP/CREATE, inserts data only Wizard now has 9 steps: Pre-check → Extract → Tables → Database → Config → Admin → Post-Restore → Provisioning → Complete Closes #131, closes #132
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- MokoRestore: post-restore reset options — passwords, hits, versions, sessions, cache (#131)
|
||||
- MokoRestore: per-table conflict resolution — replace, skip, merge, data-only per table (#132)
|
||||
- MokoRestore: preset buttons — "All Replace", "All Skip", "Everything except users"
|
||||
- MokoRestore: auto-detect sanitized passwords and prompt for reset
|
||||
- Data sanitization: passwords, emails, sessions in backup profile settings (#129)
|
||||
|
||||
## [01.38.05] --- 2026-06-23
|
||||
|
||||
## [01.38.05] --- 2026-06-23
|
||||
|
||||
@@ -376,16 +376,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
function handleAction(string $action, array $data): array
|
||||
{
|
||||
return match ($action) {
|
||||
'preflight' => actionPreflight(),
|
||||
'extract' => actionExtract($data),
|
||||
'testdb' => actionTestDb($data),
|
||||
'database' => actionDatabase($data),
|
||||
'config' => actionConfig($data),
|
||||
'listAdmins' => actionListAdmins($data),
|
||||
'resetAdmin' => actionResetAdmin($data),
|
||||
'provision' => actionProvision($data),
|
||||
'cleanup' => actionCleanup(),
|
||||
default => ['success' => false, 'message' => 'Unknown action: ' . $action],
|
||||
'preflight' => actionPreflight(),
|
||||
'extract' => actionExtract($data),
|
||||
'scanTables' => actionScanTables(),
|
||||
'testdb' => actionTestDb($data),
|
||||
'database' => actionDatabase($data),
|
||||
'config' => actionConfig($data),
|
||||
'listAdmins' => actionListAdmins($data),
|
||||
'resetAdmin' => actionResetAdmin($data),
|
||||
'postRestore' => actionPostRestore($data),
|
||||
'detectSanitized' => detectSanitizedPasswords($data),
|
||||
'provision' => actionProvision($data),
|
||||
'cleanup' => actionCleanup(),
|
||||
default => ['success' => false, 'message' => 'Unknown action: ' . $action],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -551,6 +554,65 @@ function actionExtract(array $data): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse database.sql and extract the list of table names.
|
||||
* Returns table names using the abstract #__ prefix so the UI
|
||||
* can display them before the user's target prefix is known.
|
||||
*/
|
||||
function actionScanTables(): array
|
||||
{
|
||||
$sqlFile = RESTORE_DIR . '/database.sql';
|
||||
|
||||
if (!is_file($sqlFile)) {
|
||||
return ['success' => true, 'tables' => [], 'message' => 'No database.sql found'];
|
||||
}
|
||||
|
||||
$sql = file_get_contents($sqlFile);
|
||||
$tables = [];
|
||||
|
||||
// Match DROP TABLE IF EXISTS `#__tablename` or CREATE TABLE ... `#__tablename`
|
||||
if (preg_match_all('/(?:DROP\s+TABLE\s+IF\s+EXISTS|CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?)\s+`([^`]+)`/i', $sql, $matches)) {
|
||||
foreach ($matches[1] as $name) {
|
||||
if (!in_array($name, $tables, true)) {
|
||||
$tables[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically for easier scanning
|
||||
sort($tables, SORT_STRING);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'tables' => $tables,
|
||||
'count' => count($tables),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which table a SQL statement belongs to.
|
||||
* Returns the table name (with the prefix already applied) or empty string.
|
||||
*/
|
||||
function getStatementTable(string $stmt): string
|
||||
{
|
||||
// DROP TABLE IF EXISTS `prefix_tablename`
|
||||
if (preg_match('/^DROP\s+TABLE\s+IF\s+EXISTS\s+`([^`]+)`/i', $stmt, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
|
||||
// CREATE TABLE `prefix_tablename` or CREATE TABLE IF NOT EXISTS `prefix_tablename`
|
||||
if (preg_match('/^CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`([^`]+)`/i', $stmt, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
|
||||
// INSERT INTO `prefix_tablename`
|
||||
if (preg_match('/^INSERT\s+INTO\s+`([^`]+)`/i', $stmt, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function actionTestDb(array $data): array
|
||||
{
|
||||
$host = $data['db_host'] ?? 'localhost';
|
||||
@@ -608,10 +670,27 @@ function actionDatabase(array $data): array
|
||||
// Replace abstract #__ prefix with the user's target prefix
|
||||
$sql = str_replace('#__', $prefix, $sql);
|
||||
|
||||
// Decode per-table conflict resolution selections
|
||||
// Keys are abstract table names (#__xxx), values are: replace|skip|merge|dataonly
|
||||
$tableResolutions = [];
|
||||
|
||||
if (!empty($data['table_resolutions'])) {
|
||||
$decoded = json_decode($data['table_resolutions'], true);
|
||||
|
||||
if (is_array($decoded)) {
|
||||
// Remap from abstract #__ names to the real prefix
|
||||
foreach ($decoded as $abstractName => $mode) {
|
||||
$realName = str_replace('#__', $prefix, $abstractName);
|
||||
$tableResolutions[$realName] = $mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$parts = explode(";\n", $sql);
|
||||
$statements = 0;
|
||||
$errors = 0;
|
||||
$errorList = [];
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$part = trim($part);
|
||||
@@ -620,6 +699,39 @@ function actionDatabase(array $data): array
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine which table this statement belongs to
|
||||
$table = getStatementTable($part);
|
||||
$mode = $tableResolutions[$table] ?? 'replace';
|
||||
|
||||
// Apply conflict resolution per table
|
||||
if ($mode === 'skip') {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$isDrop = (bool) preg_match('/^DROP\s+TABLE/i', $part);
|
||||
$isCreate = (bool) preg_match('/^CREATE\s+TABLE/i', $part);
|
||||
$isInsert = (bool) preg_match('/^INSERT\s+INTO/i', $part);
|
||||
|
||||
if ($mode === 'merge') {
|
||||
// Skip DROP and CREATE; convert INSERT INTO to INSERT IGNORE INTO
|
||||
if ($isDrop || $isCreate) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isInsert) {
|
||||
$part = preg_replace('/^INSERT\s+INTO/i', 'INSERT IGNORE INTO', $part);
|
||||
}
|
||||
} elseif ($mode === 'dataonly') {
|
||||
// Skip DROP and CREATE; execute INSERT as-is
|
||||
if ($isDrop || $isCreate) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// mode === 'replace' => execute everything as-is (default)
|
||||
|
||||
try {
|
||||
$pdo->exec($part);
|
||||
$statements++;
|
||||
@@ -634,11 +746,22 @@ function actionDatabase(array $data): array
|
||||
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
|
||||
|
||||
$msg = "Executed {$statements} statements";
|
||||
|
||||
if ($skipped > 0) {
|
||||
$msg .= " ({$skipped} skipped)";
|
||||
}
|
||||
|
||||
if ($errors > 0) {
|
||||
$msg .= " ({$errors} warnings)";
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => ($statements > 0 || $errors === 0),
|
||||
'message' => "Executed {$statements} statements" . ($errors ? " ({$errors} warnings)" : ''),
|
||||
'message' => $msg,
|
||||
'statements' => $statements,
|
||||
'errors' => $errors,
|
||||
'skipped' => $skipped,
|
||||
'errorList' => $errorList,
|
||||
];
|
||||
}
|
||||
@@ -989,6 +1112,127 @@ function actionResetAdmin(array $data): array
|
||||
return ['success' => true, 'message' => 'Admin password updated successfully'];
|
||||
}
|
||||
|
||||
function actionPostRestore(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
$prefix = getValidatedPrefix($data);
|
||||
$tasks = json_decode($data['tasks'] ?? '[]', true) ?: [];
|
||||
$results = [];
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
try {
|
||||
switch ($task) {
|
||||
case 'reset_passwords':
|
||||
// Set all user passwords to a known temporary hash ("changeme"),
|
||||
// clear activation tokens, and force password reset on next login.
|
||||
$tempHash = password_hash('changeme', PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE {$prefix}users SET password = ?, activation = '', requireReset = 1"
|
||||
);
|
||||
$stmt->execute([$tempHash]);
|
||||
$affected = $stmt->rowCount();
|
||||
$results[] = "All {$affected} user password(s) reset to temporary password (changeme) with forced reset";
|
||||
break;
|
||||
|
||||
case 'reset_hits':
|
||||
$pdo->exec("UPDATE {$prefix}content SET hits = 0");
|
||||
$results[] = 'Content hits reset to 0';
|
||||
break;
|
||||
|
||||
case 'clear_versions':
|
||||
try {
|
||||
$pdo->exec("TRUNCATE TABLE {$prefix}history");
|
||||
$results[] = 'Content version history cleared';
|
||||
} catch (PDOException $e) {
|
||||
$results[] = 'Version history: table not found (skipped)';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'clear_sessions':
|
||||
$pdo->exec("TRUNCATE TABLE {$prefix}session");
|
||||
$results[] = 'Sessions cleared';
|
||||
break;
|
||||
|
||||
case 'clear_cache':
|
||||
// Clear Joomla cache tables
|
||||
foreach (['cache', 'cache_extension'] as $tbl) {
|
||||
try {
|
||||
$pdo->exec("TRUNCATE TABLE {$prefix}{$tbl}");
|
||||
} catch (PDOException $e) {
|
||||
// Table may not exist
|
||||
}
|
||||
}
|
||||
|
||||
// Delete files in cache/ directory
|
||||
$cacheDir = RESTORE_DIR . '/cache';
|
||||
$cacheCount = 0;
|
||||
|
||||
if (is_dir($cacheDir)) {
|
||||
$it = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($cacheDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($it as $item) {
|
||||
if ($item->isFile()) {
|
||||
@unlink($item->getPathname());
|
||||
$cacheCount++;
|
||||
} elseif ($item->isDir()) {
|
||||
@rmdir($item->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also clear administrator/cache/
|
||||
$adminCacheDir = RESTORE_DIR . '/administrator/cache';
|
||||
|
||||
if (is_dir($adminCacheDir)) {
|
||||
$it = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($adminCacheDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($it as $item) {
|
||||
if ($item->isFile()) {
|
||||
@unlink($item->getPathname());
|
||||
$cacheCount++;
|
||||
} elseif ($item->isDir()) {
|
||||
@rmdir($item->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results[] = "Cache tables cleared, {$cacheCount} cache file(s) removed";
|
||||
break;
|
||||
|
||||
default:
|
||||
$results[] = "Unknown task: {$task}";
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$results[] = "Error ({$task}): " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'results' => $results, 'message' => count($results) . ' post-restore task(s) completed'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the database contains sanitized sentinel password hashes.
|
||||
* Returns true if any user has the MokoSuiteBackup sanitized placeholder hash.
|
||||
*/
|
||||
function detectSanitizedPasswords(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
$prefix = getValidatedPrefix($data);
|
||||
$sentinel = '$2y$10$SANITIZED.MOKOSUITEBACKUP.INVALID.HASH.DO.NOT.USE.000000';
|
||||
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM {$prefix}users WHERE password = ?");
|
||||
$stmt->execute([$sentinel]);
|
||||
$count = (int) $stmt->fetchColumn();
|
||||
|
||||
return ['success' => true, 'detected' => $count > 0, 'count' => $count];
|
||||
}
|
||||
|
||||
function actionProvision(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
@@ -1233,11 +1477,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
<div class="mr-steps" id="stepBar">
|
||||
<div class="mr-step active" data-step="1"><span class="mr-num">1</span>Checks</div>
|
||||
<div class="mr-step" data-step="2"><span class="mr-num">2</span>Extract</div>
|
||||
<div class="mr-step" data-step="3"><span class="mr-num">3</span>Database</div>
|
||||
<div class="mr-step" data-step="4"><span class="mr-num">4</span>Configuration</div>
|
||||
<div class="mr-step" data-step="5"><span class="mr-num">5</span>Admin</div>
|
||||
<div class="mr-step" data-step="6"><span class="mr-num">6</span>Provisioning</div>
|
||||
<div class="mr-step" data-step="7"><span class="mr-num">7</span>Complete</div>
|
||||
<div class="mr-step" data-step="3"><span class="mr-num">3</span>Tables</div>
|
||||
<div class="mr-step" data-step="4"><span class="mr-num">4</span>Database</div>
|
||||
<div class="mr-step" data-step="5"><span class="mr-num">5</span>Configuration</div>
|
||||
<div class="mr-step" data-step="6"><span class="mr-num">6</span>Admin</div>
|
||||
<div class="mr-step" data-step="7"><span class="mr-num">7</span>Post-Restore</div>
|
||||
<div class="mr-step" data-step="8"><span class="mr-num">8</span>Provisioning</div>
|
||||
<div class="mr-step" data-step="9"><span class="mr-num">9</span>Complete</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 0: Security Verification -->
|
||||
@@ -1292,8 +1538,36 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Database -->
|
||||
<!-- Step 3: Table Conflict Resolution -->
|
||||
<div class="mr-panel" id="panel3">
|
||||
<h2>Table Conflict Resolution</h2>
|
||||
<p class="mr-desc">Choose how each table should be handled during database import. This lets you protect specific tables (e.g. users) from being overwritten.</p>
|
||||
<div style="margin-bottom:1rem;display:flex;gap:0.5rem;flex-wrap:wrap">
|
||||
<button class="mr-btn mr-btn-outline" style="font-size:0.8rem;padding:0.4rem 0.8rem" onclick="setAllTableMode('replace')">All Replace</button>
|
||||
<button class="mr-btn mr-btn-outline" style="font-size:0.8rem;padding:0.4rem 0.8rem" onclick="setAllTableMode('skip')">All Skip</button>
|
||||
<button class="mr-btn mr-btn-outline" style="font-size:0.8rem;padding:0.4rem 0.8rem" onclick="setAllTableMode('merge')">All Merge</button>
|
||||
<button class="mr-btn mr-btn-outline" style="font-size:0.8rem;padding:0.4rem 0.8rem" onclick="presetExceptUsers()">Everything except users</button>
|
||||
</div>
|
||||
<div class="mr-alert mr-alert-info" style="font-size:0.85rem">
|
||||
<strong>Modes:</strong>
|
||||
<strong>Replace</strong> = drop + recreate + insert (default).
|
||||
<strong>Skip</strong> = ignore entirely.
|
||||
<strong>Merge</strong> = keep existing table, INSERT IGNORE new rows.
|
||||
<strong>Data Only</strong> = keep schema, INSERT data as-is (assumes matching structure).
|
||||
</div>
|
||||
<div id="tableResolutionList" style="max-height:400px;overflow-y:auto;border:1px solid #e2e8f0;border-radius:8px;margin-top:1rem">
|
||||
<div style="padding:1rem;color:#94a3b8;text-align:center">Scanning tables...</div>
|
||||
</div>
|
||||
<input type="hidden" id="tableResolutions" value="{}">
|
||||
<div class="mr-status" id="tableScanStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(2)">Back</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnTablesContinue" onclick="goStep(4)">Continue to Database</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Database -->
|
||||
<div class="mr-panel" id="panel4">
|
||||
<h2>Database Configuration</h2>
|
||||
<p class="mr-desc">Enter the database credentials for this server. The SQL dump will be imported.</p>
|
||||
<div class="mr-row">
|
||||
@@ -1312,13 +1586,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
<div class="mr-progress"><div class="mr-progress-bar" id="dbProgress" style="width:0%"></div></div>
|
||||
<div class="mr-status" id="dbStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(2)">Back</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(3)">Back</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnImport" onclick="runDatabase()">Import Database</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Site Configuration -->
|
||||
<div class="mr-panel" id="panel4">
|
||||
<!-- Step 5: Site Configuration -->
|
||||
<div class="mr-panel" id="panel5">
|
||||
<h2>Site Configuration</h2>
|
||||
<p class="mr-desc">Configure your site settings. Credentials were removed from the backup for security — enter the correct values for this server.</p>
|
||||
|
||||
@@ -1361,13 +1635,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
</div>
|
||||
<div class="mr-status" id="configStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(3)">Back</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(4)">Back</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnConfig" onclick="runConfig()">Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Admin Password Reset -->
|
||||
<div class="mr-panel" id="panel5">
|
||||
<!-- Step 6: Admin Password Reset -->
|
||||
<div class="mr-panel" id="panel6">
|
||||
<h2>Super Admin Password</h2>
|
||||
<p class="mr-desc">Reset the password for a super administrator account. This is optional but recommended after restoring to a new server.</p>
|
||||
<div class="mr-field">
|
||||
@@ -1380,16 +1654,40 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
</div>
|
||||
<div class="mr-status" id="adminStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(4)">Back</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(5)">Back</button>
|
||||
<div>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(6)">Skip</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(7)">Skip</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnResetAdmin" onclick="runResetAdmin()">Reset Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 6: Client Provisioning -->
|
||||
<div class="mr-panel" id="panel6">
|
||||
<!-- Step 7: Post-Restore Actions -->
|
||||
<div class="mr-panel" id="panel7">
|
||||
<h2>Post-Restore Actions</h2>
|
||||
<p class="mr-desc">Optional reset tasks to clean up the restored database. These are especially useful when restoring a sanitized backup.</p>
|
||||
<div class="mr-alert mr-alert-warn" id="postRestoreSanitizedWarn" style="display:none">
|
||||
<strong>Sanitized passwords detected!</strong> This backup contains placeholder password hashes that will prevent all users from logging in. The "Reset all user passwords" option below is strongly recommended.
|
||||
</div>
|
||||
<ul class="mr-provision-list" id="postRestoreList">
|
||||
<li><input type="checkbox" class="post-restore-task" id="prResetPasswords" value="reset_passwords"><span>Reset all user passwords</span><span class="mr-provision-desc">Set to "changeme" and force reset on next login</span></li>
|
||||
<li><input type="checkbox" class="post-restore-task" value="reset_hits"><span>Reset content hits</span><span class="mr-provision-desc">Set all article hit counters to 0</span></li>
|
||||
<li><input type="checkbox" class="post-restore-task" value="clear_versions"><span>Clear version history</span><span class="mr-provision-desc">Truncate the content version history table</span></li>
|
||||
<li><input type="checkbox" class="post-restore-task" value="clear_sessions" checked><span>Clear sessions</span><span class="mr-provision-desc">Remove all active user sessions</span></li>
|
||||
<li><input type="checkbox" class="post-restore-task" value="clear_cache" checked><span>Clear cache</span><span class="mr-provision-desc">Truncate cache tables and delete cache files</span></li>
|
||||
</ul>
|
||||
<div class="mr-status" id="postRestoreStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(6)">Back</button>
|
||||
<div>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(8)">Skip</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnPostRestore" onclick="runPostRestore()">Run Selected Tasks</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 8: Client Provisioning -->
|
||||
<div class="mr-panel" id="panel8">
|
||||
<h2>Client Provisioning</h2>
|
||||
<p class="mr-desc">Optional cleanup tasks for deploying this backup as a new client site. Check the tasks you want to run.</p>
|
||||
<ul class="mr-provision-list">
|
||||
@@ -1404,16 +1702,16 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
</ul>
|
||||
<div class="mr-status" id="provisionStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(5)">Back</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(7)">Back</button>
|
||||
<div>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(7)">Skip</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(9)">Skip</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnProvision" onclick="runProvision()">Run Selected Tasks</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 7: Complete -->
|
||||
<div class="mr-panel" id="panel7">
|
||||
<!-- Step 9: Complete -->
|
||||
<div class="mr-panel" id="panel9">
|
||||
<h2>Installation Complete</h2>
|
||||
<p class="mr-desc">Your Joomla site has been restored and configured.</p>
|
||||
<div class="mr-alert mr-alert-success">
|
||||
@@ -1484,7 +1782,9 @@ function goStep(n) {
|
||||
else if (sn < n) s.classList.add('done');
|
||||
});
|
||||
|
||||
if (n === 5) loadAdmins();
|
||||
if (n === 3) scanTables();
|
||||
if (n === 6) loadAdmins();
|
||||
if (n === 7) checkSanitizedPasswords();
|
||||
}
|
||||
|
||||
function setStatus(id, msg, type) {
|
||||
@@ -1621,7 +1921,111 @@ async function runExtract() {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3
|
||||
// Step 3: Table Conflict Resolution
|
||||
let tableList = [];
|
||||
|
||||
async function scanTables() {
|
||||
const container = document.getElementById('tableResolutionList');
|
||||
|
||||
// Only scan once
|
||||
if (tableList.length > 0) return;
|
||||
|
||||
log('Scanning database.sql for table names...');
|
||||
const r = await post('scanTables');
|
||||
|
||||
if (!r.success || !r.tables || r.tables.length === 0) {
|
||||
container.innerHTML = '<div style="padding:1rem;color:#94a3b8;text-align:center">No tables found in database.sql (or file not present). You can skip this step.</div>';
|
||||
setStatus('tableScanStatus', r.tables ? 'No tables found' : (r.message || 'Scan failed'), r.success ? '' : 'error');
|
||||
log(r.message || 'No tables found');
|
||||
return;
|
||||
}
|
||||
|
||||
tableList = r.tables;
|
||||
log('Found ' + r.count + ' tables');
|
||||
setStatus('tableScanStatus', 'Found ' + r.count + ' tables', 'success');
|
||||
|
||||
renderTableList();
|
||||
}
|
||||
|
||||
function renderTableList() {
|
||||
const container = document.getElementById('tableResolutionList');
|
||||
container.innerHTML = '';
|
||||
|
||||
var resolutions = {};
|
||||
|
||||
tableList.forEach(function(name) {
|
||||
var row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:0.5rem 0.75rem;border-bottom:1px solid #f1f5f9;font-size:0.85rem;';
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.style.cssText = 'font-family:monospace;color:#334155;word-break:break-all;flex:1;margin-right:0.75rem;';
|
||||
label.textContent = name;
|
||||
|
||||
var sel = document.createElement('select');
|
||||
sel.dataset.table = name;
|
||||
sel.className = 'table-mode-select';
|
||||
sel.style.cssText = 'padding:0.3rem 0.5rem;border:1px solid #d1d5db;border-radius:4px;font-size:0.8rem;min-width:120px;background:#fff;';
|
||||
|
||||
var modes = [
|
||||
['replace', 'Replace'],
|
||||
['skip', 'Skip'],
|
||||
['merge', 'Merge'],
|
||||
['dataonly', 'Data Only']
|
||||
];
|
||||
|
||||
modes.forEach(function(m) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = m[0];
|
||||
opt.textContent = m[1];
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
|
||||
sel.addEventListener('change', updateTableResolutions);
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(sel);
|
||||
container.appendChild(row);
|
||||
|
||||
resolutions[name] = 'replace';
|
||||
});
|
||||
|
||||
document.getElementById('tableResolutions').value = JSON.stringify(resolutions);
|
||||
}
|
||||
|
||||
function updateTableResolutions() {
|
||||
var resolutions = {};
|
||||
document.querySelectorAll('.table-mode-select').forEach(function(sel) {
|
||||
resolutions[sel.dataset.table] = sel.value;
|
||||
});
|
||||
document.getElementById('tableResolutions').value = JSON.stringify(resolutions);
|
||||
}
|
||||
|
||||
function setAllTableMode(mode) {
|
||||
document.querySelectorAll('.table-mode-select').forEach(function(sel) {
|
||||
sel.value = mode;
|
||||
});
|
||||
updateTableResolutions();
|
||||
log('Set all tables to: ' + mode);
|
||||
}
|
||||
|
||||
function presetExceptUsers() {
|
||||
var userTables = ['#__users', '#__user_usergroup_map', '#__user_profiles'];
|
||||
|
||||
document.querySelectorAll('.table-mode-select').forEach(function(sel) {
|
||||
var tableName = sel.dataset.table;
|
||||
|
||||
if (userTables.indexOf(tableName) !== -1) {
|
||||
sel.value = 'skip';
|
||||
} else {
|
||||
sel.value = 'replace';
|
||||
}
|
||||
});
|
||||
|
||||
updateTableResolutions();
|
||||
log('Preset: Replace all except user tables (skipped)');
|
||||
}
|
||||
|
||||
// Step 4
|
||||
function getDbParams() {
|
||||
return {
|
||||
db_host: document.getElementById('dbHost').value,
|
||||
@@ -1647,7 +2051,12 @@ async function runDatabase() {
|
||||
log('Importing database...');
|
||||
|
||||
dbConfig = getDbParams();
|
||||
const r = await post('database', dbConfig);
|
||||
// Include table conflict resolution selections
|
||||
var tableRes = document.getElementById('tableResolutions');
|
||||
var dbParams = Object.assign({}, dbConfig, {
|
||||
table_resolutions: tableRes ? tableRes.value : '{}'
|
||||
});
|
||||
const r = await post('database', dbParams);
|
||||
|
||||
document.getElementById('dbProgress').style.width = '100%';
|
||||
setBtnLoading(btn, false);
|
||||
@@ -1655,17 +2064,20 @@ async function runDatabase() {
|
||||
if (r.success) {
|
||||
setStatus('dbStatus', r.message, 'success');
|
||||
log(r.message);
|
||||
if (r.skipped && r.skipped > 0) {
|
||||
log(' Skipped ' + r.skipped + ' statements due to conflict resolution');
|
||||
}
|
||||
if (r.errorList && r.errorList.length > 0) {
|
||||
r.errorList.forEach(function(e) { log(' Warning: ' + e); });
|
||||
}
|
||||
setTimeout(function() { goStep(4); }, 500);
|
||||
setTimeout(function() { goStep(5); }, 500);
|
||||
} else {
|
||||
setStatus('dbStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4
|
||||
// Step 5
|
||||
async function runConfig() {
|
||||
const btn = document.getElementById('btnConfig');
|
||||
setBtnLoading(btn, true);
|
||||
@@ -1686,14 +2098,14 @@ async function runConfig() {
|
||||
if (r.success) {
|
||||
setStatus('configStatus', r.message, 'success');
|
||||
log(r.message);
|
||||
setTimeout(function() { goStep(5); }, 500);
|
||||
setTimeout(function() { goStep(6); }, 500);
|
||||
} else {
|
||||
setStatus('configStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5
|
||||
// Step 6
|
||||
async function loadAdmins() {
|
||||
const sel = document.getElementById('adminSelect');
|
||||
while (sel.firstChild) sel.removeChild(sel.firstChild);
|
||||
@@ -1738,20 +2150,65 @@ async function runResetAdmin() {
|
||||
if (r.success) {
|
||||
setStatus('adminStatus', r.message, 'success');
|
||||
log(r.message);
|
||||
setTimeout(function() { goStep(6); }, 500);
|
||||
setTimeout(function() { goStep(7); }, 500);
|
||||
} else {
|
||||
setStatus('adminStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6
|
||||
// Step 7: Post-Restore
|
||||
async function checkSanitizedPasswords() {
|
||||
log('Checking for sanitized password hashes...');
|
||||
|
||||
try {
|
||||
const r = await post('detectSanitized', dbConfig);
|
||||
|
||||
if (r.success && r.detected) {
|
||||
document.getElementById('postRestoreSanitizedWarn').style.display = '';
|
||||
document.getElementById('prResetPasswords').checked = true;
|
||||
log('WARNING: ' + r.count + ' user(s) have sanitized placeholder passwords');
|
||||
} else {
|
||||
document.getElementById('postRestoreSanitizedWarn').style.display = 'none';
|
||||
log('No sanitized passwords detected');
|
||||
}
|
||||
} catch (e) {
|
||||
log('Could not check for sanitized passwords: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function runPostRestore() {
|
||||
const btn = document.getElementById('btnPostRestore');
|
||||
const tasks = [];
|
||||
document.querySelectorAll('.post-restore-task:checked').forEach(function(cb) { tasks.push(cb.value); });
|
||||
|
||||
if (tasks.length === 0) { goStep(8); return; }
|
||||
|
||||
setBtnLoading(btn, true);
|
||||
log('Running ' + tasks.length + ' post-restore task(s)...');
|
||||
|
||||
const params = Object.assign({}, dbConfig, { tasks: JSON.stringify(tasks) });
|
||||
const r = await post('postRestore', params);
|
||||
|
||||
setBtnLoading(btn, false);
|
||||
|
||||
if (r.success) {
|
||||
setStatus('postRestoreStatus', r.message, 'success');
|
||||
r.results.forEach(function(msg) { log(' ' + msg); });
|
||||
setTimeout(function() { goStep(8); }, 500);
|
||||
} else {
|
||||
setStatus('postRestoreStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8
|
||||
async function runProvision() {
|
||||
const btn = document.getElementById('btnProvision');
|
||||
const tasks = [];
|
||||
document.querySelectorAll('.prov-task:checked').forEach(function(cb) { tasks.push(cb.value); });
|
||||
|
||||
if (tasks.length === 0) { goStep(7); return; }
|
||||
if (tasks.length === 0) { goStep(9); return; }
|
||||
|
||||
setBtnLoading(btn, true);
|
||||
log('Running ' + tasks.length + ' provisioning tasks...');
|
||||
@@ -1764,14 +2221,14 @@ async function runProvision() {
|
||||
if (r.success) {
|
||||
setStatus('provisionStatus', r.message, 'success');
|
||||
r.results.forEach(function(msg) { log(' ' + msg); });
|
||||
setTimeout(function() { goStep(7); }, 500);
|
||||
setTimeout(function() { goStep(9); }, 500);
|
||||
} else {
|
||||
setStatus('provisionStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 7
|
||||
// Step 9
|
||||
async function runCleanup() {
|
||||
log('Cleaning up restore files...');
|
||||
const r = await post('cleanup');
|
||||
|
||||
Reference in New Issue
Block a user