+
MokoJoomBackup Restore
+
Standalone site restoration tool
+
+
DELETE this file (restore.php) immediately after restoration is complete!
+
+
+
Step 1: Pre-flight Checks
+
+
+
+
+
+
Step 2: Extract Files
+
+
+
+
+
Step 3: Database Restore
+
+
+
+
+
+
+
+
+
+
Step 4: Update Configuration
+
+
+
+
+
+
+
Step 5: Cleanup
+
+
+
+
Ready. Click "Run Checks" to begin.
+
+
+
+
+
+RESTORE_PHP;
+ }
+}
diff --git a/src/packages/com_mokobackup/src/Engine/RestoreEngine.php b/src/packages/com_mokobackup/src/Engine/RestoreEngine.php
new file mode 100644
index 00000000..ec3baaa5
--- /dev/null
+++ b/src/packages/com_mokobackup/src/Engine/RestoreEngine.php
@@ -0,0 +1,203 @@
+
+ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ *
+ * Restore engine — extracts a backup archive and reimports the database.
+ *
+ * Steps:
+ * 1. Extract ZIP to a temp staging directory
+ * 2. Preserve current configuration.php (DB credentials, paths)
+ * 3. Restore files from staging to Joomla root
+ * 4. Import database.sql (if present in archive)
+ * 5. Restore preserved configuration.php
+ * 6. Clean up staging directory
+ */
+
+namespace Joomla\Component\MokoBackup\Administrator\Engine;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+
+class RestoreEngine
+{
+ private array $log = [];
+ private string $stagingDir;
+
+ /**
+ * Run a full restore from a backup record.
+ *
+ * @param int $recordId Backup record ID to restore from
+ * @param bool $restoreFiles Whether to restore files
+ * @param bool $restoreDb Whether to restore the database
+ * @param bool $preserveConfig Keep current configuration.php
+ *
+ * @return array{success: bool, message: string}
+ */
+ public function restore(int $recordId, bool $restoreFiles = true, bool $restoreDb = true, bool $preserveConfig = true): array
+ {
+ $db = Factory::getDbo();
+
+ // Load backup record
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from($db->quoteName('#__mokobackup_records'))
+ ->where($db->quoteName('id') . ' = ' . $recordId);
+ $db->setQuery($query);
+ $record = $db->loadObject();
+
+ if (!$record) {
+ return ['success' => false, 'message' => 'Backup record not found: ' . $recordId];
+ }
+
+ if ($record->status !== 'complete') {
+ return ['success' => false, 'message' => 'Cannot restore from incomplete backup (status: ' . $record->status . ')'];
+ }
+
+ $archivePath = $record->absolute_path;
+
+ if (!is_file($archivePath) || !is_readable($archivePath)) {
+ return ['success' => false, 'message' => 'Backup archive not found: ' . $archivePath];
+ }
+
+ // Create staging directory
+ $this->stagingDir = JPATH_ROOT . '/tmp/mokobackup-restore-' . $record->tag;
+
+ if (is_dir($this->stagingDir)) {
+ $this->recursiveDelete($this->stagingDir);
+ }
+
+ mkdir($this->stagingDir, 0755, true);
+
+ try {
+ // Step 1: Extract archive to staging
+ $this->log('Extracting archive: ' . basename($archivePath));
+ $this->extractArchive($archivePath);
+ $this->log('Extraction complete');
+
+ // Step 2: Preserve configuration.php
+ $configBackup = '';
+
+ if ($preserveConfig && is_file(JPATH_ROOT . '/configuration.php')) {
+ $configBackup = file_get_contents(JPATH_ROOT . '/configuration.php');
+ $this->log('Current configuration.php preserved');
+ }
+
+ // Step 3: Restore files
+ if ($restoreFiles) {
+ $this->log('Restoring files...');
+ $restorer = new FileRestorer($this->stagingDir, JPATH_ROOT);
+ $fileCount = $restorer->restore();
+ $this->log('Files restored: ' . $fileCount);
+ }
+
+ // Step 4: Import database
+ if ($restoreDb) {
+ $sqlFile = $this->stagingDir . '/database.sql';
+
+ if (is_file($sqlFile)) {
+ $this->log('Importing database...');
+ $importer = new DatabaseImporter();
+ $tableCount = $importer->import($sqlFile);
+ $this->log('Database imported: ' . $tableCount . ' statements executed');
+ } else {
+ $this->log('No database.sql found in archive — skipping database restore');
+ }
+ }
+
+ // Step 5: Restore preserved configuration.php
+ if ($preserveConfig && !empty($configBackup)) {
+ file_put_contents(JPATH_ROOT . '/configuration.php', $configBackup);
+ $this->log('Configuration.php restored to pre-restore state');
+ }
+
+ // Step 6: Clean up staging
+ $this->recursiveDelete($this->stagingDir);
+ $this->log('Staging directory cleaned up');
+
+ $this->log('Restore complete');
+
+ return [
+ 'success' => true,
+ 'message' => 'Restore complete from: ' . basename($archivePath),
+ 'log' => implode("\n", $this->log),
+ ];
+ } catch (\Throwable $e) {
+ $this->log('FATAL: ' . $e->getMessage());
+
+ // Restore config even on failure
+ if ($preserveConfig && !empty($configBackup)) {
+ file_put_contents(JPATH_ROOT . '/configuration.php', $configBackup);
+ $this->log('Configuration.php restored after failure');
+ }
+
+ // Clean up staging on failure
+ if (is_dir($this->stagingDir)) {
+ $this->recursiveDelete($this->stagingDir);
+ }
+
+ return [
+ 'success' => false,
+ 'message' => 'Restore failed: ' . $e->getMessage(),
+ 'log' => implode("\n", $this->log),
+ ];
+ }
+ }
+
+ /**
+ * Extract a ZIP archive to the staging directory.
+ */
+ private function extractArchive(string $archivePath): void
+ {
+ $zip = new \ZipArchive();
+ $result = $zip->open($archivePath);
+
+ if ($result !== true) {
+ throw new \RuntimeException('Cannot open archive (error code: ' . $result . ')');
+ }
+
+ if (!$zip->extractTo($this->stagingDir)) {
+ $zip->close();
+
+ throw new \RuntimeException('Failed to extract archive to staging directory');
+ }
+
+ $this->log('Extracted ' . $zip->numFiles . ' entries');
+ $zip->close();
+ }
+
+ /**
+ * Recursively delete a directory and all its contents.
+ */
+ private function recursiveDelete(string $dir): void
+ {
+ if (!is_dir($dir)) {
+ return;
+ }
+
+ $items = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($items as $item) {
+ if ($item->isDir()) {
+ @rmdir($item->getPathname());
+ } else {
+ @unlink($item->getPathname());
+ }
+ }
+
+ @rmdir($dir);
+ }
+
+ private function log(string $message): void
+ {
+ $this->log[] = '[' . date('H:i:s') . '] ' . $message;
+ }
+}
diff --git a/src/packages/com_mokobackup/src/View/Backups/HtmlView.php b/src/packages/com_mokobackup/src/View/Backups/HtmlView.php
index 088d5c1e..1412e108 100644
--- a/src/packages/com_mokobackup/src/View/Backups/HtmlView.php
+++ b/src/packages/com_mokobackup/src/View/Backups/HtmlView.php
@@ -41,6 +41,7 @@ class HtmlView extends BaseHtmlView
{
ToolbarHelper::title(Text::_('COM_MOKOBACKUP_BACKUPS_TITLE'), 'database');
ToolbarHelper::custom('backups.start', 'download', '', 'COM_MOKOBACKUP_TOOLBAR_BACKUP_NOW', false);
+ ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOBACKUP_TOOLBAR_RESTORE', true);
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'backups.delete');
ToolbarHelper::preferences('com_mokobackup');
}