From a3dbd1f89a1bea10507f397c112e5d41f34ae124 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 25 Jun 2026 09:50:44 -0500 Subject: [PATCH] fix(mokorestore): add Joomla detection warning, multi-zip selector, and standalone backup scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Preflight now detects existing Joomla installation (configuration.php / Version.php) and shows a yellow warning — does not block, but alerts the user - Standalone mode: backup archive check scans for all ZIPs instead of hardcoded name - Multi-zip selector integrated into extract step with radio buttons - Selected backup file passed through to extract action - Added warn-style CSS class (yellow) for preflight warnings --- .../src/Engine/MokoRestore.php | 193 ++++++++++++------ 1 file changed, 134 insertions(+), 59 deletions(-) diff --git a/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php index dfb6ffc..9bc481a 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php +++ b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php @@ -165,7 +165,38 @@ SCANNER; $php ); - /* Modify the pre-checks to use getSelectedBackupFile() */ + /* Replace the backup archive check with one that scans for ZIPs + (must run BEFORE the blanket file_exists replacement below) */ + $php = str_replace( + <<<'ORIG' + $checks[] = [ + 'label' => 'Backup Archive', + 'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found', + 'ok' => file_exists(BACKUP_FILE), + 'hint' => 'site-backup.zip must be in the same directory as restore.php', + ]; +ORIG, + <<<'REPL' + $availableBackups = scanForBackups(); + $backupCount = count($availableBackups); + $selectedFile = getSelectedBackupFile(); + if ($selectedFile && file_exists($selectedFile)) { + $archiveValue = basename($selectedFile) . ' (' . number_format(filesize($selectedFile) / 1048576, 2) . ' MB)'; + } elseif ($backupCount > 0) { + $archiveValue = $backupCount . ' ZIP file(s) found'; + } else { + $archiveValue = 'No ZIP files found'; + } + $checks[] = [ + 'label' => 'Backup Archive', + 'value' => $archiveValue, + 'ok' => $backupCount > 0, + 'hint' => 'Place one or more backup ZIP files in the same directory as restore.php', + ]; +REPL + ); + + /* Modify remaining pre-checks to use getSelectedBackupFile() */ $php = str_replace( "file_exists(BACKUP_FILE)", "(getSelectedBackupFile() !== '' || file_exists(BACKUP_FILE))", @@ -174,65 +205,83 @@ SCANNER; $html = self::generateFrontend(); - /* Add backup file selector to the frontend before the extract step */ + /* Inject backup file selector into the extract step (panel2) */ $selectorHtml = <<<'SELECTOR' - - - + if (backups.length === 0) { + var alert = document.createElement('div'); + alert.style.cssText = 'padding:12px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:#dc2626;'; + alert.textContent = 'No ZIP files found in this directory. Upload a backup archive first.'; + list.appendChild(alert); + } else if (backups.length === 1) { + hiddenInput.value = backups[0].name; + var found = document.createElement('div'); + found.style.cssText = 'padding:12px;background:#dcfce7;border:1px solid #bbf7d0;border-radius:6px;color:#16a34a;'; + var strong = document.createElement('strong'); + strong.textContent = backups[0].name; + found.appendChild(document.createTextNode('Found: ')); + found.appendChild(strong); + found.appendChild(document.createTextNode(' (' + (backups[0].size / 1048576).toFixed(1) + ' MB)')); + list.appendChild(found); + } else { + var hint = document.createElement('div'); + hint.style.cssText = 'padding:8px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;color:#1d4ed8;margin-bottom:8px;font-size:0.9em;'; + hint.textContent = 'Multiple backup archives found \u2014 select which one to restore:'; + list.appendChild(hint); + backups.forEach(function(b, i) { + var label = document.createElement('label'); + label.style.cssText = 'display:flex;align-items:center;padding:10px 12px;margin:4px 0;border:1px solid #e2e8f0;border-radius:6px;cursor:pointer;transition:background 0.15s;'; + label.onmouseover = function() { this.style.background = '#f8fafc'; }; + label.onmouseout = function() { this.style.background = ''; }; + var radio = document.createElement('input'); + radio.type = 'radio'; + radio.name = 'backup_choice'; + radio.value = b.name; + radio.style.marginRight = '10px'; + if (i === 0) { radio.checked = true; hiddenInput.value = b.name; } + radio.addEventListener('change', function() { hiddenInput.value = this.value; }); + label.appendChild(radio); + var info = document.createElement('div'); + var nameStrong = document.createElement('strong'); + nameStrong.textContent = b.name; + info.appendChild(nameStrong); + var meta = document.createElement('div'); + meta.style.cssText = 'font-size:0.85em;color:#64748b;margin-top:2px;'; + meta.textContent = (b.size / 1048576).toFixed(1) + ' MB \u2014 ' + b.date; + info.appendChild(meta); + label.appendChild(info); + list.appendChild(label); + }); + } + })(); + SELECTOR; - /* Insert the selector before the extract step in the HTML */ + /* Insert the selector into the extract panel */ $html = str_replace( - '', - $selectorHtml . "\n", + '

Extract site-backup.zip into the current directory.

', + '

Select a backup archive and extract it into the current directory.

' . "\n" . $selectorHtml, + $html + ); + + /* Pass selected backup file to the extract action */ + $html = str_replace( + "const r = await post('extract', pw ? { archive_password: pw } : {});", + "var extraParams = {};\n" . + " if (pw) extraParams.archive_password = pw;\n" . + " var sel = document.getElementById('mr-backup-file');\n" . + " if (sel && sel.value) extraParams.backup_file = sel.value;\n" . + " const r = await post('extract', extraParams);", $html ); @@ -462,15 +511,31 @@ function actionPreflight(): array 'hint' => 'Informational', ]; + $joomlaExists = file_exists(RESTORE_DIR . '/configuration.php') + || file_exists(RESTORE_DIR . '/libraries/src/Version.php'); + $checks[] = [ + 'label' => 'Existing Installation', + 'value' => $joomlaExists ? 'Joomla detected' : 'Clean directory', + 'ok' => true, + 'warn' => $joomlaExists, + 'hint' => $joomlaExists + ? 'WARNING: A Joomla installation already exists in this directory. Restoring will overwrite it.' + : 'No existing installation found — safe to proceed', + ]; + $allOk = true; + $warnings = []; foreach ($checks as $c) { if (!$c['ok']) { $allOk = false; } + if (!empty($c['warn'])) { + $warnings[] = $c['hint']; + } } - return ['success' => $allOk, 'checks' => $checks]; + return ['success' => $allOk, 'checks' => $checks, 'warnings' => $warnings]; } function actionExtract(array $data): array @@ -1425,6 +1490,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N .mr-checks li:last-child{border-bottom:none} .mr-check-icon{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0} .mr-check-ok{background:#dcfce7;color:#16a34a} +.mr-check-warn{background:#fef9c3;color:#a16207} .mr-check-fail{background:#fef2f2;color:#dc2626} .mr-check-info{background:#e0f2fe;color:#0284c7} .mr-check-label{flex:1;font-weight:500} @@ -1877,8 +1943,10 @@ async function runPreflight() { r.checks.forEach(function(c) { const li = document.createElement('li'); const icon = document.createElement('span'); - icon.className = 'mr-check-icon ' + (c.ok ? 'mr-check-ok' : 'mr-check-fail'); - icon.textContent = c.ok ? '\u2713' : '\u2717'; + var iconClass = c.ok ? 'mr-check-ok' : 'mr-check-fail'; + if (c.warn) iconClass = 'mr-check-warn'; + icon.className = 'mr-check-icon ' + iconClass; + icon.textContent = c.warn ? '\u26a0' : (c.ok ? '\u2713' : '\u2717'); const label = document.createElement('span'); label.className = 'mr-check-label'; @@ -1891,9 +1959,16 @@ async function runPreflight() { li.appendChild(icon); li.appendChild(label); li.appendChild(val); + if (c.warn && c.hint) { + var hint = document.createElement('div'); + hint.style.cssText = 'font-size:0.85em;color:#a16207;margin-top:4px;padding:4px 8px;background:#fef9c3;border-radius:4px;'; + hint.textContent = c.hint; + li.appendChild(hint); + } list.appendChild(li); - log(' ' + (c.ok ? 'OK' : 'FAIL') + ': ' + c.label + ' = ' + c.value); + var logPrefix = c.warn ? 'WARN' : (c.ok ? 'OK' : 'FAIL'); + log(' ' + logPrefix + ': ' + c.label + ' = ' + c.value); }); setBtnLoading(btn, false);