From be52bc048faf042c11dd6556d99b099f158c29f9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 6 Jun 2026 12:48:42 -0500 Subject: [PATCH] feat: Joomla-styled standalone installer with provisioning wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete rewrite of MokoRestore restore.php generator: - 7-step wizard UI matching Joomla installer look and feel - Pre-flight checks (PHP, extensions, disk space, backup detection) - Archive extraction with password support and config pre-fill - Database connection test and SQL import with error reporting - Create or update configuration.php with fresh Joomla secret - Super admin dropdown with bcrypt password reset - Client provisioning: reset hits, clear sessions/cache/keys/tokens/logs - Zero innerHTML — all DOM built with safe createElement/textContent - Fully self-contained single PHP file, no external dependencies Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com_mokobackup/src/Engine/MokoRestore.php | 1366 +++++++++++++---- 1 file changed, 1078 insertions(+), 288 deletions(-) diff --git a/source/packages/com_mokobackup/src/Engine/MokoRestore.php b/source/packages/com_mokobackup/src/Engine/MokoRestore.php index 38c2c0a..5321485 100644 --- a/source/packages/com_mokobackup/src/Engine/MokoRestore.php +++ b/source/packages/com_mokobackup/src/Engine/MokoRestore.php @@ -7,17 +7,18 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE * - * Standalone restore script generator. + * Standalone restore/installer script generator. * * When "Include MokoRestore" is enabled on a profile, the backup archive * is wrapped: * * outer.zip - * ├── restore.php ← Standalone restore script (no Joomla needed) + * ├── restore.php ← Standalone installer (no Joomla needed) * └── site-backup.zip ← The actual site backup * * Upload outer.zip to a blank server, extract, open restore.php in a - * browser, and it handles everything — self-contained site restoration. + * browser, and it handles everything — self-contained site restoration + * with a Joomla-styled wizard interface. */ namespace Joomla\Component\MokoBackup\Administrator\Engine; @@ -56,39 +57,50 @@ class MokoRestore /** * Generate the standalone restore.php script. * - * This is a self-contained PHP file that: - * 1. Provides a web UI for configuration (DB credentials, etc.) + * This is a self-contained PHP file with a Joomla-styled wizard UI that: + * 1. Runs pre-installation checks (PHP, extensions, permissions) * 2. Extracts site-backup.zip to the current directory - * 3. Imports database.sql using the provided credentials - * 4. Updates configuration.php with new settings - * 5. Cleans up after itself + * 3. Imports database.sql using provided credentials + * 4. Creates or updates configuration.php + * 5. Resets super admin password (optional) + * 6. Runs client provisioning tasks (optional) + * 7. Cleans up restore artifacts */ private static function generateRestoreScript(): string { - return <<<'RESTORE_PHP' + $php = self::generateBackend(); + $html = self::generateFrontend(); + + return $php . $html; + } + + /** + * Generate the PHP backend portion of the restore script. + */ + private static function generateBackend(): string + { + return <<<'PHP_BACKEND' false, 'message' => 'Invalid token']); + if (!isset($_POST['token']) || !hash_equals($token, $_POST['token'])) { + echo json_encode(['success' => false, 'message' => 'Invalid security token. Reload the page.']); exit; } - $action = $_POST['action']; + @set_time_limit(0); + @ini_set('max_execution_time', '0'); + @ini_set('memory_limit', '512M'); + @ignore_user_abort(true); try { - switch ($action) { - case 'preflight': - // Override PHP limits for restore - @set_time_limit(0); - @ini_set('max_execution_time', '0'); - @ini_set('memory_limit', '512M'); - @ignore_user_abort(true); - - $checks = []; - $checks['php_version'] = ['value' => PHP_VERSION, 'ok' => version_compare(PHP_VERSION, '8.1', '>=')]; - $checks['zip_ext'] = ['value' => extension_loaded('zip') ? 'Yes' : 'No', 'ok' => extension_loaded('zip')]; - $checks['pdo_mysql'] = ['value' => extension_loaded('pdo_mysql') ? 'Yes' : 'No', 'ok' => extension_loaded('pdo_mysql')]; - $checks['mbstring'] = ['value' => extension_loaded('mbstring') ? 'Yes' : 'No', 'ok' => extension_loaded('mbstring')]; - $checks['backup_exists'] = ['value' => file_exists(BACKUP_FILE) ? 'Yes' : 'No', 'ok' => file_exists(BACKUP_FILE)]; - $checks['writable'] = ['value' => is_writable(RESTORE_DIR) ? 'Yes' : 'No', 'ok' => is_writable(RESTORE_DIR)]; - $checks['max_execution_time'] = ['value' => ini_get('max_execution_time') ?: 'unlimited', 'ok' => true]; - $checks['memory_limit'] = ['value' => ini_get('memory_limit'), 'ok' => true]; - - if (file_exists(BACKUP_FILE)) { - $checks['backup_size'] = ['value' => number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB', 'ok' => true]; - } - - $allOk = true; - foreach ($checks as $c) { - if (!$c['ok']) $allOk = false; - } - - echo json_encode(['success' => $allOk, 'checks' => $checks]); - break; - - case 'extract': - $zip = new ZipArchive(); - - if ($zip->open(BACKUP_FILE) !== true) { - throw new RuntimeException('Cannot open backup archive'); - } - - // Set decryption password if provided - $archivePassword = $_POST['archive_password'] ?? ''; - if (!empty($archivePassword)) { - $zip->setPassword($archivePassword); - } - - if (!$zip->extractTo(RESTORE_DIR)) { - $zip->close(); - throw new RuntimeException( - 'Extraction failed. ' . (!empty($archivePassword) ? 'Check the decryption password.' : 'The archive may be encrypted.') - ); - } - - $count = $zip->numFiles; - $zip->close(); - - echo json_encode(['success' => true, 'message' => "Extracted {$count} files"]); - break; - - case 'database': - $host = $_POST['db_host'] ?? 'localhost'; - $name = $_POST['db_name'] ?? ''; - $user = $_POST['db_user'] ?? ''; - $pass = $_POST['db_pass'] ?? ''; - $prefix = $_POST['db_prefix'] ?? 'moko_'; - - if (empty($name) || empty($user)) { - throw new RuntimeException('Database name and user are required'); - } - - $pdo = new PDO( - "mysql:host={$host};dbname={$name};charset=utf8mb4", - $user, - $pass, - [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] - ); - - $sqlFile = RESTORE_DIR . '/database.sql'; - - if (!file_exists($sqlFile)) { - echo json_encode(['success' => true, 'message' => 'No database.sql found — skipped']); - break; - } - - $sql = file_get_contents($sqlFile); - $statements = 0; - $errors = 0; - - // Split on semicolons (simple splitter for our dump format) - $parts = explode(";\n", $sql); - - foreach ($parts as $part) { - $part = trim($part); - - if (empty($part) || strpos($part, '--') === 0) { - continue; - } - - try { - $pdo->exec($part); - $statements++; - } catch (PDOException $e) { - $errors++; - } - } - - echo json_encode([ - 'success' => true, - 'message' => "Executed {$statements} statements" . ($errors ? " ({$errors} warnings)" : ''), - ]); - break; - - case 'config': - $host = $_POST['db_host'] ?? 'localhost'; - $name = $_POST['db_name'] ?? ''; - $user = $_POST['db_user'] ?? ''; - $pass = $_POST['db_pass'] ?? ''; - $prefix = $_POST['db_prefix'] ?? 'moko_'; - $sitename = $_POST['sitename'] ?? 'Restored Site'; - $livesite = $_POST['live_site'] ?? ''; - $tmpPath = $_POST['tmp_path'] ?? RESTORE_DIR . '/tmp'; - $logPath = $_POST['log_path'] ?? RESTORE_DIR . '/administrator/logs'; - - $configFile = RESTORE_DIR . '/configuration.php'; - - if (file_exists($configFile)) { - // Update existing configuration.php - $config = file_get_contents($configFile); - $replacements = [ - '/\$host\s*=\s*\'[^\']*\'/' => "\$host = '{$host}'", - '/\$db\s*=\s*\'[^\']*\'/' => "\$db = '{$name}'", - '/\$user\s*=\s*\'[^\']*\'/' => "\$user = '{$user}'", - '/\$password\s*=\s*\'[^\']*\'/' => "\$password = '{$pass}'", - '/\$dbprefix\s*=\s*\'[^\']*\'/' => "\$dbprefix = '{$prefix}'", - '/\$tmp_path\s*=\s*\'[^\']*\'/' => "\$tmp_path = '{$tmpPath}'", - '/\$log_path\s*=\s*\'[^\']*\'/' => "\$log_path = '{$logPath}'", - ]; - - if (!empty($livesite)) { - $replacements['/\$live_site\s*=\s*\'[^\']*\'/'] = "\$live_site = '{$livesite}'"; - } - - foreach ($replacements as $pattern => $replacement) { - $config = preg_replace($pattern, $replacement, $config); - } - - file_put_contents($configFile, $config); - } - - echo json_encode(['success' => true, 'message' => 'Configuration updated']); - break; - - case 'cleanup': - // Remove restore artifacts - @unlink(RESTORE_DIR . '/database.sql'); - @unlink(BACKUP_FILE); - // Don't delete restore.php here — user does it manually - - echo json_encode(['success' => true, 'message' => 'Cleanup complete. DELETE restore.php manually!']); - break; - - default: - echo json_encode(['success' => false, 'message' => 'Unknown action']); - } + $result = handleAction($_POST['action'], $_POST); + echo json_encode($result); } catch (Throwable $e) { echo json_encode(['success' => false, 'message' => $e->getMessage()]); } @@ -277,164 +133,1098 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { exit; } -// ── HTML UI ──────────────────────────────────────────────────────── +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], + }; +} + +function actionPreflight(): array +{ + $checks = []; + + $checks[] = [ + 'label' => 'PHP Version', + 'value' => PHP_VERSION, + 'ok' => version_compare(PHP_VERSION, '8.1', '>='), + 'hint' => 'Joomla 4/5 requires PHP 8.1+', + ]; + + $checks[] = [ + 'label' => 'ZipArchive Extension', + 'value' => extension_loaded('zip') ? 'Available' : 'Missing', + 'ok' => extension_loaded('zip'), + 'hint' => 'Required to extract backup archives', + ]; + + $checks[] = [ + 'label' => 'PDO MySQL', + 'value' => extension_loaded('pdo_mysql') ? 'Available' : 'Missing', + 'ok' => extension_loaded('pdo_mysql'), + 'hint' => 'Required for database import', + ]; + + $checks[] = [ + 'label' => 'Multibyte String', + 'value' => extension_loaded('mbstring') ? 'Available' : 'Missing', + 'ok' => extension_loaded('mbstring'), + 'hint' => 'Required by Joomla for UTF-8 handling', + ]; + + $checks[] = [ + 'label' => 'JSON Extension', + 'value' => extension_loaded('json') ? 'Available' : 'Missing', + 'ok' => extension_loaded('json'), + 'hint' => 'Required by Joomla', + ]; + + $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', + ]; + + $checks[] = [ + 'label' => 'Directory Writable', + 'value' => is_writable(RESTORE_DIR) ? 'Yes' : 'No', + 'ok' => is_writable(RESTORE_DIR), + 'hint' => 'The restore directory must be writable', + ]; + + $freeSpace = @disk_free_space(RESTORE_DIR); + $freeGB = $freeSpace ? round($freeSpace / 1073741824, 1) : 0; + + $checks[] = [ + 'label' => 'Free Disk Space', + 'value' => $freeGB . ' GB', + 'ok' => $freeGB >= 0.5, + 'hint' => 'At least 500 MB free space recommended', + ]; + + $checks[] = [ + 'label' => 'Memory Limit', + 'value' => ini_get('memory_limit') ?: 'Unknown', + 'ok' => true, + 'hint' => 'Informational', + ]; + + $allOk = true; + + foreach ($checks as $c) { + if (!$c['ok']) { + $allOk = false; + } + } + + return ['success' => $allOk, 'checks' => $checks]; +} + +function actionExtract(array $data): array +{ + if (!file_exists(BACKUP_FILE)) { + throw new RuntimeException('Backup file not found: site-backup.zip'); + } + + $zip = new ZipArchive(); + + if ($zip->open(BACKUP_FILE) !== true) { + throw new RuntimeException('Cannot open backup archive'); + } + + $password = trim($data['archive_password'] ?? ''); + + if ($password !== '') { + $zip->setPassword($password); + } + + if (!$zip->extractTo(RESTORE_DIR)) { + $zip->close(); + throw new RuntimeException( + 'Extraction failed. ' . ($password !== '' ? 'Check the decryption password.' : 'The archive may be encrypted — provide a password.') + ); + } + + $count = $zip->numFiles; + $zip->close(); + + // Try to read existing configuration.php for pre-filling + $existingConfig = []; + $configFile = RESTORE_DIR . '/configuration.php'; + + if (is_file($configFile)) { + $content = file_get_contents($configFile); + + if (preg_match('/\$host\s*=\s*\'([^\']*)\'/', $content, $m)) { + $existingConfig['db_host'] = $m[1]; + } + + if (preg_match('/\$db\s*=\s*\'([^\']*)\'/', $content, $m)) { + $existingConfig['db_name'] = $m[1]; + } + + if (preg_match('/\$user\s*=\s*\'([^\']*)\'/', $content, $m)) { + $existingConfig['db_user'] = $m[1]; + } + + if (preg_match('/\$dbprefix\s*=\s*\'([^\']*)\'/', $content, $m)) { + $existingConfig['db_prefix'] = $m[1]; + } + + if (preg_match('/\$sitename\s*=\s*\'([^\']*)\'/', $content, $m)) { + $existingConfig['sitename'] = $m[1]; + } + } + + return [ + 'success' => true, + 'message' => "Extracted {$count} files", + 'config' => $existingConfig, + 'has_db' => is_file(RESTORE_DIR . '/database.sql'), + ]; +} + +function actionTestDb(array $data): array +{ + $host = $data['db_host'] ?? 'localhost'; + $name = $data['db_name'] ?? ''; + $user = $data['db_user'] ?? ''; + $pass = $data['db_pass'] ?? ''; + + if (empty($name) || empty($user)) { + throw new RuntimeException('Database name and user are required'); + } + + $pdo = new PDO( + "mysql:host={$host};dbname={$name};charset=utf8mb4", + $user, + $pass, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5] + ); + + $version = $pdo->query('SELECT VERSION()')->fetchColumn(); + + return ['success' => true, 'message' => 'Connected — MySQL ' . $version]; +} + +function actionDatabase(array $data): array +{ + $host = $data['db_host'] ?? 'localhost'; + $name = $data['db_name'] ?? ''; + $user = $data['db_user'] ?? ''; + $pass = $data['db_pass'] ?? ''; + + if (empty($name) || empty($user)) { + throw new RuntimeException('Database name and user are required'); + } + + $sqlFile = RESTORE_DIR . '/database.sql'; + + if (!is_file($sqlFile)) { + return ['success' => true, 'message' => 'No database.sql found — skipped', 'statements' => 0, 'errors' => 0]; + } + + $pdo = new PDO( + "mysql:host={$host};dbname={$name};charset=utf8mb4", + $user, + $pass, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + + $pdo->exec('SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"'); + $pdo->exec("SET time_zone = '+00:00'"); + $pdo->exec('SET FOREIGN_KEY_CHECKS = 0'); + + $sql = file_get_contents($sqlFile); + $parts = explode(";\n", $sql); + $statements = 0; + $errors = 0; + $errorList = []; + + foreach ($parts as $part) { + $part = trim($part); + + if ($part === '' || str_starts_with($part, '--') || str_starts_with($part, 'SET ')) { + continue; + } + + try { + $pdo->exec($part); + $statements++; + } catch (PDOException $e) { + $errors++; + + if (count($errorList) < 5) { + $errorList[] = substr($e->getMessage(), 0, 120); + } + } + } + + $pdo->exec('SET FOREIGN_KEY_CHECKS = 1'); + + return [ + 'success' => true, + 'message' => "Executed {$statements} statements" . ($errors ? " ({$errors} warnings)" : ''), + 'statements' => $statements, + 'errors' => $errors, + 'errorList' => $errorList, + ]; +} + +function actionConfig(array $data): array +{ + $host = $data['db_host'] ?? 'localhost'; + $dbName = $data['db_name'] ?? ''; + $dbUser = $data['db_user'] ?? ''; + $dbPass = $data['db_pass'] ?? ''; + $prefix = $data['db_prefix'] ?? 'moko_'; + $sitename = $data['sitename'] ?? 'Joomla Site'; + $livesite = $data['live_site'] ?? ''; + $tmpPath = RESTORE_DIR . '/tmp'; + $logPath = RESTORE_DIR . '/administrator/logs'; + + $configFile = RESTORE_DIR . '/configuration.php'; + + if (is_file($configFile)) { + // Update existing configuration.php + $config = file_get_contents($configFile); + + $replacements = [ + '/\$host\s*=\s*\'[^\']*\'/' => "\$host = '{$host}'", + '/\$db\s*=\s*\'[^\']*\'/' => "\$db = '{$dbName}'", + '/\$user\s*=\s*\'[^\']*\'/' => "\$user = '{$dbUser}'", + '/\$password\s*=\s*\'[^\']*\'/' => "\$password = '" . addcslashes($dbPass, "'\\") . "'", + '/\$dbprefix\s*=\s*\'[^\']*\'/' => "\$dbprefix = '{$prefix}'", + '/\$tmp_path\s*=\s*\'[^\']*\'/' => "\$tmp_path = '{$tmpPath}'", + '/\$log_path\s*=\s*\'[^\']*\'/' => "\$log_path = '{$logPath}'", + '/\$sitename\s*=\s*\'[^\']*\'/' => "\$sitename = '" . addcslashes($sitename, "'\\") . "'", + '/\$secret\s*=\s*\'[^\']*\'/' => "\$secret = '" . bin2hex(random_bytes(16)) . "'", + ]; + + if ($livesite !== '') { + $replacements['/\$live_site\s*=\s*\'[^\']*\'/'] = "\$live_site = '{$livesite}'"; + } + + foreach ($replacements as $pattern => $replacement) { + $config = preg_replace($pattern, $replacement, $config); + } + + file_put_contents($configFile, $config); + + return ['success' => true, 'message' => 'configuration.php updated with new settings and fresh secret']; + } + + // Create new configuration.php from scratch + $secret = bin2hex(random_bytes(16)); + $newConfig = << true, 'message' => 'configuration.php created from scratch with fresh secret']; +} + +function actionListAdmins(array $data): array +{ + $pdo = getDbConnection($data); + $prefix = $data['db_prefix'] ?? 'moko_'; + + // Find super admin users (group 8 = Super Users in Joomla) + $stmt = $pdo->prepare( + "SELECT u.id, u.name, u.username, u.email + FROM {$prefix}users u + INNER JOIN {$prefix}user_usergroup_map m ON m.user_id = u.id + WHERE m.group_id = 8 + ORDER BY u.id ASC" + ); + $stmt->execute(); + $admins = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($admins)) { + return ['success' => true, 'admins' => [], 'message' => 'No super admin users found']; + } + + return ['success' => true, 'admins' => $admins]; +} + +function actionResetAdmin(array $data): array +{ + $pdo = getDbConnection($data); + $prefix = $data['db_prefix'] ?? 'moko_'; + $userId = (int) ($data['admin_id'] ?? 0); + $password = $data['new_password'] ?? ''; + + if ($userId < 1 || strlen($password) < 8) { + throw new RuntimeException('Select an admin and enter a password (8+ characters)'); + } + + $hash = password_hash($password, PASSWORD_DEFAULT); + + $stmt = $pdo->prepare("UPDATE {$prefix}users SET password = ?, requireReset = 0 WHERE id = ?"); + $stmt->execute([$hash, $userId]); + + if ($stmt->rowCount() === 0) { + throw new RuntimeException('User not found or password unchanged'); + } + + return ['success' => true, 'message' => 'Admin password updated successfully']; +} + +function actionProvision(array $data): array +{ + $pdo = getDbConnection($data); + $prefix = $data['db_prefix'] ?? 'moko_'; + $tasks = json_decode($data['tasks'] ?? '[]', true) ?: []; + $results = []; + + foreach ($tasks as $task) { + try { + switch ($task) { + case 'reset_hits': + $pdo->exec("UPDATE {$prefix}content SET hits = 0"); + $results[] = 'Content hits reset to 0'; + 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 + } + } + $results[] = 'Cache tables cleared'; + break; + + case 'clear_update_keys': + $pdo->exec("UPDATE {$prefix}update_sites SET extra_query = ''"); + $results[] = 'Update site download keys cleared'; + break; + + case 'clear_updates': + $pdo->exec("DELETE FROM {$prefix}updates"); + $results[] = 'Pending updates cleared'; + break; + + case 'clear_api_tokens': + try { + $pdo->exec("TRUNCATE TABLE {$prefix}user_keys"); + $results[] = 'API tokens cleared'; + } catch (PDOException $e) { + $results[] = 'API tokens: table not found (skipped)'; + } + break; + + case 'clear_action_logs': + try { + $pdo->exec("TRUNCATE TABLE {$prefix}action_logs"); + $results[] = 'Action logs cleared'; + } catch (PDOException $e) { + $results[] = 'Action logs: table not found (skipped)'; + } + break; + + case 'clear_mail_queue': + try { + $pdo->exec("TRUNCATE TABLE {$prefix}mail_queue"); + $results[] = 'Mail queue cleared'; + } catch (PDOException $e) { + $results[] = 'Mail queue: table not found (skipped)'; + } + break; + + default: + $results[] = "Unknown task: {$task}"; + } + } catch (Throwable $e) { + $results[] = "Error ({$task}): " . $e->getMessage(); + } + } + + return ['success' => true, 'results' => $results, 'message' => count($results) . ' provisioning tasks completed']; +} + +function actionCleanup(): array +{ + $removed = []; + + foreach (['database.sql', 'site-backup.zip'] as $file) { + $path = RESTORE_DIR . '/' . $file; + + if (is_file($path) && @unlink($path)) { + $removed[] = $file; + } + } + + return [ + 'success' => true, + 'message' => 'Removed: ' . (empty($removed) ? '(none)' : implode(', ', $removed)) + . '. IMPORTANT: Delete restore.php manually!', + ]; +} + +function getDbConnection(array $data): PDO +{ + $host = $data['db_host'] ?? 'localhost'; + $name = $data['db_name'] ?? ''; + $user = $data['db_user'] ?? ''; + $pass = $data['db_pass'] ?? ''; + + return new PDO( + "mysql:host={$host};dbname={$name};charset=utf8mb4", + $user, + $pass, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); +} + +// ── Render HTML UI ────────────────────────────────────────────────── ?> +PHP_BACKEND; + } + + /** + * Generate the HTML/CSS/JS frontend of the restore script. + */ + private static function generateFrontend(): string + { + return <<<'HTML_FRONTEND' - - - MokoJoomBackup — Site Restore - + + +MokoRestore — Site Installer + -
-

MokoJoomBackup Restore

-

Standalone site restoration tool

+
+

MokoRestore

+

Standalone Site Installer — MokoJoomBackup

+
-
DELETE this file (restore.php) immediately after restoration is complete!
- -
-

Step 1: Pre-flight Checks

-
- +
+
+ Security: Delete restore.php immediately after installation is complete.
-
-

Step 2: Extract Files

- - - + +
+
1Checks
+
2Extract
+
3Database
+
4Configuration
+
5Admin
+
6Provisioning
+
7Complete
-
-

Step 3: Database Restore

- - - - - - + +
+

Pre-Installation Checks

+

Verify your server meets the requirements for Joomla and MokoRestore.

+
    +
    + + +
    -
    -

    Step 4: Update Configuration

    - - - + +
    +

    Extract Backup

    +

    Extract site-backup.zip into the current directory.

    +
    + + +
    +
    +
    +
    + + +
    -
    -

    Step 5: Cleanup

    - + +
    +

    Database Configuration

    +

    Enter the database credentials for this server. The SQL dump will be imported.

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    -
    Ready. Click "Run Checks" to begin.
    + +
    +

    Site Configuration

    +

    Update or create configuration.php with the correct settings for this server.

    +
    +
    + + +
    Leave blank to auto-detect. Set this if using a reverse proxy or custom domain.
    +
    +
    + A new Joomla secret will be generated automatically for security. +
    +
    +
    + + +
    +
    + + +
    +

    Super Admin Password

    +

    Reset the password for a super administrator account. This is optional but recommended after restoring to a new server.

    +
    + + +
    +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + + +
    +

    Client Provisioning

    +

    Optional cleanup tasks for deploying this backup as a new client site. Check the tasks you want to run.

    +
      +
    • Reset content hitsSet all article hit counters to 0
    • +
    • Clear sessionsRemove all active user sessions
    • +
    • Clear cacheTruncate Joomla cache tables
    • +
    • Clear download keysRemove update site extra_query keys
    • +
    • Clear pending updatesRemove cached update records
    • +
    • Clear API tokensRemove all personal access tokens
    • +
    • Clear action logsRemove admin action log history
    • +
    • Clear mail queueRemove pending outbound emails
    • +
    +
    +
    + +
    + + +
    +
    +
    + + +
    +

    Installation Complete

    +

    Your Joomla site has been restored and configured.

    +
    + Success! The site restoration is complete. +
    +
    + Important: Delete restore.php and site-backup.zip from your server immediately for security. +
    +
    + + Open Joomla Admin + View Site +
    +
    +
    + + +
    +
    + +
    +
    + + -RESTORE_PHP; +HTML_FRONTEND; } }