feat: abstract DB prefix, stepped checksum, restore security gate
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Auto Version Bump / Version Bump (push) Successful in 3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 6s
Generic: Project CI / Lint & Validate (pull_request) Successful in 32s
Generic: Project CI / Lint & Validate (push) Successful in 32s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 35s
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
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
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Auto Version Bump / Version Bump (push) Successful in 3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 6s
Generic: Project CI / Lint & Validate (pull_request) Successful in 32s
Generic: Project CI / Lint & Validate (push) Successful in 32s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 35s
Database prefix abstraction: - DatabaseDumper uses #__ placeholder instead of live prefix in all SQL output (DROP TABLE, CREATE TABLE, INSERT INTO) - SteppedBackupEngine::dumpSingleTable() same #__ replacement - DatabaseImporter replaces #__ with current site prefix on import - MokoRestore replaces #__ with user-specified prefix on import - Backups are now portable across sites with different prefixes Stepped backup checksum: - completeRecord() now computes and stores SHA-256 checksum MokoRestore security gate: - Writes .mokorestore-security.php with random 8-char code to site root - User must read code from filesystem and enter it in browser - Proves filesystem access before any restore actions are allowed - Security file auto-deleted after successful verification - All AJAX actions blocked until verification completes
This commit is contained in:
@@ -60,7 +60,9 @@ class DatabaseDumper
|
||||
$output[] = '-- Generated: ' . date('Y-m-d H:i:s');
|
||||
$output[] = '-- Server: ' . $db->getServerType();
|
||||
$output[] = '-- Database: ' . $db->getName();
|
||||
$output[] = '-- Prefix: ' . $prefix;
|
||||
$output[] = '-- Original Prefix: ' . $prefix;
|
||||
$output[] = '-- Abstract Prefix: #__';
|
||||
$output[] = '-- Note: Table names use #__ placeholder. Replace with your prefix on restore.';
|
||||
$output[] = '';
|
||||
$output[] = 'SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";';
|
||||
$output[] = 'SET time_zone = "+00:00";';
|
||||
@@ -90,7 +92,7 @@ class DatabaseDumper
|
||||
$this->tablesCount++;
|
||||
|
||||
$output[] = '-- --------------------------------------------------------';
|
||||
$output[] = '-- Table: ' . $table;
|
||||
$output[] = '-- Table: ' . $abstractName;
|
||||
|
||||
if ($skipData) {
|
||||
$output[] = '-- (data excluded)';
|
||||
@@ -112,8 +114,10 @@ class DatabaseDumper
|
||||
continue;
|
||||
}
|
||||
|
||||
$output[] = 'DROP TABLE IF EXISTS ' . $db->quoteName($table) . ';';
|
||||
$output[] = $createRow[1] . ';';
|
||||
// Replace live prefix with #__ in CREATE TABLE output
|
||||
$createSql = str_replace($table, $abstractName, $createRow[1]);
|
||||
$output[] = 'DROP TABLE IF EXISTS `' . $abstractName . '`;';
|
||||
$output[] = $createSql . ';';
|
||||
$output[] = '';
|
||||
}
|
||||
|
||||
@@ -160,7 +164,7 @@ class DatabaseDumper
|
||||
}
|
||||
|
||||
$columns = array_map([$db, 'quoteName'], array_keys($row));
|
||||
$output[] = 'INSERT INTO ' . $db->quoteName($table)
|
||||
$output[] = 'INSERT INTO `' . $abstractName . '`'
|
||||
. ' (' . implode(', ', $columns) . ')'
|
||||
. ' VALUES (' . implode(', ', $values) . ');';
|
||||
}
|
||||
|
||||
@@ -87,11 +87,8 @@ class DatabaseImporter
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace the prefix from the dump with the current site prefix.
|
||||
// The dump uses real table names (with the original prefix), but
|
||||
// if restoring to a site with a different prefix we need to handle it.
|
||||
// Our DatabaseDumper uses real names, so no replacement needed
|
||||
// for same-site restores.
|
||||
// Replace abstract #__ prefix with the current site's prefix
|
||||
$statement = str_replace('#__', $prefix, $statement);
|
||||
|
||||
try {
|
||||
$db->setQuery($statement);
|
||||
|
||||
@@ -109,6 +109,52 @@ if (empty($_SESSION['restore_token'])) {
|
||||
|
||||
$token = $_SESSION['restore_token'];
|
||||
|
||||
// ── Security Verification ───────────────────────────────────────────
|
||||
// Write a security file to the web root with a random code.
|
||||
// The user must read the code from the file and enter it in the browser
|
||||
// to prove they have filesystem access before any restore actions are allowed.
|
||||
$securityFile = RESTORE_DIR . '/.mokorestore-security.php';
|
||||
$securityCode = $_SESSION['security_code'] ?? '';
|
||||
|
||||
if (empty($securityCode)) {
|
||||
$securityCode = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8));
|
||||
$_SESSION['security_code'] = $securityCode;
|
||||
$_SESSION['security_verified'] = false;
|
||||
|
||||
// Write security file with the code
|
||||
$securityContent = "<?php die('MokoRestore Security Code: " . $securityCode . "'); ?>\n"
|
||||
. "MokoRestore Security Verification\n"
|
||||
. "==================================\n"
|
||||
. "Code: " . $securityCode . "\n"
|
||||
. "Enter this code in the MokoRestore browser interface to proceed.\n"
|
||||
. "This file will be deleted automatically after verification.\n";
|
||||
file_put_contents($securityFile, $securityContent);
|
||||
}
|
||||
|
||||
// Handle security code verification via POST
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'verify_security') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$inputCode = strtoupper(trim($_POST['security_code'] ?? ''));
|
||||
|
||||
if ($inputCode === $securityCode) {
|
||||
$_SESSION['security_verified'] = true;
|
||||
|
||||
// Delete the security file
|
||||
if (is_file($securityFile)) {
|
||||
@unlink($securityFile);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Security verified']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Incorrect security code. Check the file: .mokorestore-security.php']);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// Block all other actions until security is verified
|
||||
$securityVerified = !empty($_SESSION['security_verified']);
|
||||
|
||||
// ── AJAX Handler ────────────────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
@@ -118,6 +164,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$securityVerified) {
|
||||
echo json_encode(['success' => false, 'message' => 'Security verification required. Enter the code from .mokorestore-security.php']);
|
||||
exit;
|
||||
}
|
||||
|
||||
@set_time_limit(0);
|
||||
@ini_set('max_execution_time', '0');
|
||||
@ini_set('memory_limit', '512M');
|
||||
@@ -348,7 +399,12 @@ function actionDatabase(array $data): array
|
||||
$pdo->exec("SET time_zone = '+00:00'");
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
|
||||
|
||||
$sql = file_get_contents($sqlFile);
|
||||
$sql = file_get_contents($sqlFile);
|
||||
$prefix = getValidatedPrefix($data);
|
||||
|
||||
// Replace abstract #__ prefix with the user's target prefix
|
||||
$sql = str_replace('#__', $prefix, $sql);
|
||||
|
||||
$parts = explode(";\n", $sql);
|
||||
$statements = 0;
|
||||
$errors = 0;
|
||||
@@ -981,8 +1037,33 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
<div class="mr-step" data-step="7"><span class="mr-num">7</span>Complete</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 0: Security Verification -->
|
||||
<div class="mr-panel <?php echo $securityVerified ? '' : 'visible'; ?>" id="panel0">
|
||||
<h2>Security Verification</h2>
|
||||
<p class="mr-desc">To prevent unauthorized access, enter the security code from the file <code>.mokorestore-security.php</code> in your site root.</p>
|
||||
<div style="border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#f8fafc">
|
||||
<div style="font-weight:600;font-size:0.9rem;color:#334155;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
|
||||
<span style="font-size:1.1rem">🔒</span> How to find the code
|
||||
</div>
|
||||
<ol style="margin:0;padding-left:1.25rem;color:#475569;font-size:0.9rem;line-height:1.6">
|
||||
<li>Connect to your server via FTP, SSH, or file manager</li>
|
||||
<li>Open <code>.mokorestore-security.php</code> in the site root directory</li>
|
||||
<li>Copy the 8-character code and enter it below</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="mr-field">
|
||||
<label>Security Code</label>
|
||||
<input type="text" id="securityCode" placeholder="e.g. A1B2C3D4" maxlength="8" style="text-transform:uppercase;letter-spacing:0.2em;font-family:monospace;font-size:1.1rem;text-align:center">
|
||||
</div>
|
||||
<div class="mr-status" id="securityStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<span></span>
|
||||
<button class="mr-btn mr-btn-primary" id="btnVerify" onclick="verifySecurity()">Verify & Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Pre-flight Checks -->
|
||||
<div class="mr-panel visible" id="panel1">
|
||||
<div class="mr-panel <?php echo $securityVerified ? 'visible' : ''; ?>" id="panel1">
|
||||
<h2>Pre-Installation Checks</h2>
|
||||
<p class="mr-desc">Verify your server meets the requirements for Joomla and MokoRestore.</p>
|
||||
<ul class="mr-checks" id="checkList"></ul>
|
||||
@@ -1223,6 +1304,35 @@ function setBtnLoading(btn, loading) {
|
||||
}
|
||||
|
||||
// Step 1
|
||||
async function verifySecurity() {
|
||||
const btn = document.getElementById('btnVerify');
|
||||
setBtnLoading(btn, true);
|
||||
const code = document.getElementById('securityCode').value.trim();
|
||||
|
||||
if (!code) {
|
||||
setStatus('securityStatus', 'Please enter the security code', 'error');
|
||||
setBtnLoading(btn, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append('action', 'verify_security');
|
||||
form.append('security_code', code);
|
||||
form.append('token', TOKEN);
|
||||
|
||||
const resp = await fetch('', { method: 'POST', body: form });
|
||||
const r = await resp.json();
|
||||
setBtnLoading(btn, false);
|
||||
|
||||
if (r.success) {
|
||||
setStatus('securityStatus', 'Verified!', 'success');
|
||||
document.getElementById('panel0').classList.remove('visible');
|
||||
document.getElementById('panel1').classList.add('visible');
|
||||
} else {
|
||||
setStatus('securityStatus', r.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function runPreflight() {
|
||||
const btn = document.getElementById('btnCheck');
|
||||
setBtnLoading(btn, true);
|
||||
|
||||
@@ -434,12 +434,14 @@ class SteppedBackupEngine
|
||||
}
|
||||
|
||||
$totalSize = is_file($session->archivePath) ? filesize($session->archivePath) : 0;
|
||||
$checksum = is_file($session->archivePath) ? hash_file('sha256', $session->archivePath) : '';
|
||||
|
||||
$update = (object) [
|
||||
'id' => $session->recordId,
|
||||
'status' => 'complete',
|
||||
'backupend' => date('Y-m-d H:i:s'),
|
||||
'total_size' => $totalSize,
|
||||
'checksum' => $checksum,
|
||||
'log' => $logContent,
|
||||
];
|
||||
|
||||
@@ -529,13 +531,16 @@ class SteppedBackupEngine
|
||||
*/
|
||||
private function dumpSingleTable(object $db, string $table): string
|
||||
{
|
||||
$prefix = $db->getPrefix();
|
||||
$abstractName = '#__' . substr($table, strlen($prefix));
|
||||
|
||||
$output = [];
|
||||
$output[] = '-- --------------------------------------------------------';
|
||||
$output[] = '-- Table: ' . $table;
|
||||
$output[] = '-- Table: ' . $abstractName;
|
||||
$output[] = '-- --------------------------------------------------------';
|
||||
$output[] = '';
|
||||
|
||||
// CREATE TABLE
|
||||
// CREATE TABLE — replace live prefix with #__
|
||||
$db->setQuery('SHOW CREATE TABLE ' . $db->quoteName($table));
|
||||
$createRow = $db->loadRow();
|
||||
|
||||
@@ -543,8 +548,9 @@ class SteppedBackupEngine
|
||||
return '';
|
||||
}
|
||||
|
||||
$output[] = 'DROP TABLE IF EXISTS ' . $db->quoteName($table) . ';';
|
||||
$output[] = $createRow[1] . ';';
|
||||
$createSql = str_replace($table, $abstractName, $createRow[1]);
|
||||
$output[] = 'DROP TABLE IF EXISTS `' . $abstractName . '`;';
|
||||
$output[] = $createSql . ';';
|
||||
$output[] = '';
|
||||
|
||||
// Data in chunks
|
||||
@@ -580,7 +586,7 @@ class SteppedBackupEngine
|
||||
}
|
||||
|
||||
$columns = array_map([$db, 'quoteName'], array_keys($row));
|
||||
$output[] = 'INSERT INTO ' . $db->quoteName($table)
|
||||
$output[] = 'INSERT INTO `' . $abstractName . '`'
|
||||
. ' (' . implode(', ', $columns) . ')'
|
||||
. ' VALUES (' . implode(', ', $values) . ');';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user