From ed589ef65b96e7f10a5007ab6a0fa6e09e00cfd2 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 9 Oct 2025 12:28:38 -0400 Subject: [PATCH] Update Backup / Restore, now streams backup and restore to disk instead of memory causing memory to run out, sets timeout limit to unlimited, checks backup file contents for anything bad, use php instead shell exec for import of db, added .htaccess for apache to prevent php execution in /uploads/ directory as this is intended for file download only --- admin/post/backup.php | 349 +++++++++++++++++++++++++------------- setup/index.php | 283 ++++++++++++++++++++----------- setup/setup_functions.php | 347 +++++++++++++++++++++++++++++++++++++ uploads/.htaccess | 11 ++ 4 files changed, 780 insertions(+), 210 deletions(-) create mode 100644 setup/setup_functions.php create mode 100644 uploads/.htaccess diff --git a/admin/post/backup.php b/admin/post/backup.php index eac494cd..6abe4c67 100644 --- a/admin/post/backup.php +++ b/admin/post/backup.php @@ -2,185 +2,304 @@ /* * ITFlow - GET/POST request handler for DB / master key backup + * Rewritten with streaming SQL dump, component checksums, safer zipping, and better headers. */ defined('FROM_POST_HANDLER') || die("Direct file access is not allowed"); require_once "../includes/app_version.php"; -if (isset($_GET['download_backup'])) { - - validateCSRFToken($_GET['csrf_token']); - - $timestamp = date('YmdHis'); - $baseName = "itflow_$timestamp"; +// --- Optional performance levers for big backups --- +@set_time_limit(0); +if (function_exists('ini_set')) { + @ini_set('memory_limit', '1024M'); +} - // === 0. Scoped cleanup === - $cleanupFiles = []; +/** + * Write a line to a file handle with newline. + */ +function fwrite_ln($fh, string $s): void { + fwrite($fh, $s); + fwrite($fh, PHP_EOL); +} - $registerTempFileForCleanup = function ($file) use (&$cleanupFiles) { - $cleanupFiles[] = $file; - }; - - register_shutdown_function(function () use (&$cleanupFiles) { - foreach ($cleanupFiles as $file) { - if (is_file($file)) { - @unlink($file); - } - } - }); - - // === 1. Local helper function: zipFolder - $zipFolder = function ($folderPath, $zipFilePath) { - $zip = new ZipArchive(); - if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) { - error_log("Failed to open zip file: $zipFilePath"); - http_response_code(500); - exit("Internal Server Error: Cannot open zip archive."); - } - - $folderPath = realpath($folderPath); - if (!$folderPath) { - error_log("Invalid folder path: $folderPath"); - http_response_code(500); - exit("Internal Server Error: Invalid folder path."); - } - - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($folderPath), - RecursiveIteratorIterator::LEAVES_ONLY - ); - - foreach ($files as $file) { - if (!$file->isDir()) { - $filePath = $file->getRealPath(); - $relativePath = substr($filePath, strlen($folderPath) + 1); - $zip->addFile($filePath, $relativePath); - } - } - - $zip->close(); - }; - - // === 2. Create all temp files - $sqlFile = tempnam(sys_get_temp_dir(), $baseName . "_sql_"); - $uploadsZip = tempnam(sys_get_temp_dir(), $baseName . "_uploads_"); - $versionFile = tempnam(sys_get_temp_dir(), $baseName . "_version_"); - $finalZip = tempnam(sys_get_temp_dir(), $baseName . "_backup_"); - - foreach ([$sqlFile, $uploadsZip, $versionFile, $finalZip] as $f) { - $registerTempFileForCleanup($f); - chmod($f, 0600); +/** + * Stream a SQL dump of schema and data into $sqlFile. + * - Tables first (DROP + CREATE + INSERTs) + * - Views (DROP VIEW + CREATE VIEW) + * - Triggers (DROP TRIGGER + CREATE TRIGGER) + * + * NOTE: Routines/events are not dumped here. Add if needed. + */ +function dump_database_streaming(mysqli $mysqli, string $sqlFile): void { + $fh = fopen($sqlFile, 'wb'); + if (!$fh) { + http_response_code(500); + exit("Cannot open dump file"); } - // === 3. Generate SQL Dump - $sqlContent = "-- UTF-8 + Foreign Key Safe Dump\n"; - $sqlContent .= "SET NAMES 'utf8mb4';\n"; - $sqlContent .= "SET foreign_key_checks = 0;\n\n"; + // Preamble + fwrite_ln($fh, "-- UTF-8 + Foreign Key Safe Dump"); + fwrite_ln($fh, "SET NAMES 'utf8mb4';"); + fwrite_ln($fh, "SET FOREIGN_KEY_CHECKS = 0;"); + fwrite_ln($fh, "SET UNIQUE_CHECKS = 0;"); + fwrite_ln($fh, "SET AUTOCOMMIT = 0;"); + fwrite_ln($fh, ""); + // Gather tables and views $tables = []; - $res = $mysqli->query("SHOW TABLES"); + $views = []; + + $res = $mysqli->query("SHOW FULL TABLES"); if (!$res) { - error_log("MySQL Error: " . $mysqli->error); + fclose($fh); + error_log("MySQL Error (SHOW FULL TABLES): " . $mysqli->error); + http_response_code(500); exit("Error retrieving tables."); } + while ($row = $res->fetch_array(MYSQLI_NUM)) { + $name = $row[0]; + $type = strtoupper($row[1] ?? ''); + if ($type === 'VIEW') { + $views[] = $name; + } else { + $tables[] = $name; + } + } + $res->close(); - while ($row = $res->fetch_row()) { - $tables[] = $row[0]; + // --- TABLES: structure and data --- + foreach ($tables as $table) { + $createRes = $mysqli->query("SHOW CREATE TABLE `{$mysqli->real_escape_string($table)}`"); + if (!$createRes) { + error_log("MySQL Error (SHOW CREATE TABLE $table): " . $mysqli->error); + // continue to next table + continue; + } + $createRow = $createRes->fetch_assoc(); + $createSQL = array_values($createRow)[1] ?? ''; + $createRes->close(); + + fwrite_ln($fh, "-- ----------------------------"); + fwrite_ln($fh, "-- Table structure for `{$table}`"); + fwrite_ln($fh, "-- ----------------------------"); + fwrite_ln($fh, "DROP TABLE IF EXISTS `{$table}`;"); + fwrite_ln($fh, $createSQL . ";"); + fwrite_ln($fh, ""); + + // Dump data in a streaming fashion + $dataRes = $mysqli->query("SELECT * FROM `{$mysqli->real_escape_string($table)}`", MYSQLI_USE_RESULT); + if ($dataRes) { + $wroteHeader = false; + while ($row = $dataRes->fetch_assoc()) { + if (!$wroteHeader) { + fwrite_ln($fh, "-- Dumping data for table `{$table}`"); + $wroteHeader = true; + } + $cols = array_map(fn($c) => '`' . $mysqli->real_escape_string($c) . '`', array_keys($row)); + $vals = array_map( + function ($v) use ($mysqli) { + return is_null($v) ? "NULL" : "'" . $mysqli->real_escape_string($v) . "'"; + }, + array_values($row) + ); + fwrite_ln($fh, "INSERT INTO `{$table}` (" . implode(", ", $cols) . ") VALUES (" . implode(", ", $vals) . ");"); + } + $dataRes->close(); + if ($wroteHeader) fwrite_ln($fh, ""); + } } - foreach ($tables as $table) { - $createRes = $mysqli->query("SHOW CREATE TABLE `$table`"); - if (!$createRes) { - error_log("MySQL Error: " . $mysqli->error); + // --- VIEWS --- + foreach ($views as $view) { + $escView = $mysqli->real_escape_string($view); + $cRes = $mysqli->query("SHOW CREATE VIEW `{$escView}`"); + if ($cRes) { + $row = $cRes->fetch_assoc(); + $createView = $row['Create View'] ?? ''; + $cRes->close(); + + fwrite_ln($fh, "-- ----------------------------"); + fwrite_ln($fh, "-- View structure for `{$view}`"); + fwrite_ln($fh, "-- ----------------------------"); + fwrite_ln($fh, "DROP VIEW IF EXISTS `{$view}`;"); + // Ensure statement ends with semicolon + if (!str_ends_with($createView, ';')) $createView .= ';'; + fwrite_ln($fh, $createView); + fwrite_ln($fh, ""); + } + } + + // --- TRIGGERS --- + $tRes = $mysqli->query("SHOW TRIGGERS"); + if ($tRes) { + while ($t = $tRes->fetch_assoc()) { + $triggerName = $t['Trigger']; + $escTrig = $mysqli->real_escape_string($triggerName); + $crt = $mysqli->query("SHOW CREATE TRIGGER `{$escTrig}`"); + if ($crt) { + $row = $crt->fetch_assoc(); + $createTrig = $row['SQL Original Statement'] ?? ($row['Create Trigger'] ?? ''); + $crt->close(); + + fwrite_ln($fh, "-- ----------------------------"); + fwrite_ln($fh, "-- Trigger for `{$triggerName}`"); + fwrite_ln($fh, "-- ----------------------------"); + fwrite_ln($fh, "DROP TRIGGER IF EXISTS `{$triggerName}`;"); + if (!str_ends_with($createTrig, ';')) $createTrig .= ';'; + fwrite_ln($fh, $createTrig); + fwrite_ln($fh, ""); + } + } + $tRes->close(); + } + + // Postamble + fwrite_ln($fh, "SET FOREIGN_KEY_CHECKS = 1;"); + fwrite_ln($fh, "SET UNIQUE_CHECKS = 1;"); + fwrite_ln($fh, "COMMIT;"); + + fclose($fh); +} + +/** + * Zip a folder to $zipFilePath, skipping symlinks and dot-entries. + */ +function zipFolderStrict(string $folderPath, string $zipFilePath): void { + $zip = new ZipArchive(); + if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) { + error_log("Failed to open zip file: $zipFilePath"); + http_response_code(500); + exit("Internal Server Error: Cannot open zip archive."); + } + + $folderReal = realpath($folderPath); + if (!$folderReal || !is_dir($folderReal)) { + // Create an empty archive if uploads folder doesn't exist yet + $zip->close(); + return; + } + + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($folderReal, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($files as $file) { + /** @var SplFileInfo $file */ + if ($file->isDir()) continue; + if ($file->isLink()) continue; // skip symlinks + $filePath = $file->getRealPath(); + if ($filePath === false) continue; + + // ensure path is inside the folder boundary + if (strpos($filePath, $folderReal . DIRECTORY_SEPARATOR) !== 0 && $filePath !== $folderReal) { continue; } - $createRow = $createRes->fetch_assoc(); - $createSQL = array_values($createRow)[1]; - - $sqlContent .= "\n-- ----------------------------\n"; - $sqlContent .= "-- Table structure for `$table`\n"; - $sqlContent .= "-- ----------------------------\n"; - $sqlContent .= "DROP TABLE IF EXISTS `$table`;\n"; - $sqlContent .= $createSQL . ";\n\n"; - - $dataRes = $mysqli->query("SELECT * FROM `$table`"); - if ($dataRes && $dataRes->num_rows > 0) { - $sqlContent .= "-- Dumping data for table `$table`\n"; - while ($row = $dataRes->fetch_assoc()) { - $columns = array_map(fn($col) => '`' . $mysqli->real_escape_string($col) . '`', array_keys($row)); - $values = array_map(function ($val) use ($mysqli) { - return is_null($val) ? "NULL" : "'" . $mysqli->real_escape_string($val) . "'"; - }, array_values($row)); - $sqlContent .= "INSERT INTO `$table` (" . implode(", ", $columns) . ") VALUES (" . implode(", ", $values) . ");\n"; - } - $sqlContent .= "\n"; - } + $relativePath = substr($filePath, strlen($folderReal) + 1); + $zip->addFile($filePath, $relativePath); } - $sqlContent .= "SET foreign_key_checks = 1;\n"; - file_put_contents($sqlFile, $sqlContent); + $zip->close(); +} - // === 4. Zip the uploads folder - $zipFolder("../uploads", $uploadsZip); +if (isset($_GET['download_backup'])) { - // === 5. Create version.txt - $commitHash = trim(shell_exec('git log -1 --format=%H')) ?: 'N/A'; - $gitBranch = trim(shell_exec('git rev-parse --abbrev-ref HEAD')) ?: 'N/A'; + validateCSRFToken($_GET['csrf_token']); - $versionContent = "ITFlow Backup Metadata\n"; + $timestamp = date('YmdHis'); + $baseName = "itflow_{$timestamp}"; + $downloadName = $baseName . ".zip"; + + // === Scoped cleanup of temp files === + $cleanupFiles = []; + $registerTempFileForCleanup = function ($file) use (&$cleanupFiles) { + $cleanupFiles[] = $file; + }; + register_shutdown_function(function () use (&$cleanupFiles) { + foreach ($cleanupFiles as $file) { + if (is_file($file)) { @unlink($file); } + } + }); + + // === Create temp files === + $sqlFile = tempnam(sys_get_temp_dir(), $baseName . "_sql_"); + $uploadsZip = tempnam(sys_get_temp_dir(), $baseName . "_uploads_"); + $versionFile = tempnam(sys_get_temp_dir(), $baseName . "_version_"); + $finalZip = tempnam(sys_get_temp_dir(), $baseName . "_backup_"); + + foreach ([$sqlFile, $uploadsZip, $versionFile, $finalZip] as $f) { + $registerTempFileForCleanup($f); + @chmod($f, 0600); + } + + // === Generate SQL Dump (streaming) === + dump_database_streaming($mysqli, $sqlFile); + + // === Zip the uploads folder (strict) === + zipFolderStrict("../uploads", $uploadsZip); + + // === Gather metadata & checksums === + $commitHash = (function_exists('shell_exec') ? trim(shell_exec('git log -1 --format=%H 2>/dev/null')) : '') ?: 'N/A'; + $gitBranch = (function_exists('shell_exec') ? trim(shell_exec('git rev-parse --abbrev-ref HEAD 2>/dev/null')) : '') ?: 'N/A'; + + $dbSha = hash_file('sha256', $sqlFile) ?: 'N/A'; + $upSha = hash_file('sha256', $uploadsZip) ?: 'N/A'; + + $versionContent = "ITFlow Backup Metadata\n"; $versionContent .= "-----------------------------\n"; $versionContent .= "Generated: " . date('Y-m-d H:i:s') . "\n"; - $versionContent .= "Backup File: " . basename($finalZip) . "\n"; - $versionContent .= "Generated By: $session_name\n"; + $versionContent .= "Backup File: " . $downloadName . "\n"; + $versionContent .= "Generated By: " . ($session_name ?? 'Unknown User') . "\n"; $versionContent .= "Host: " . gethostname() . "\n"; $versionContent .= "Git Branch: $gitBranch\n"; $versionContent .= "Git Commit: $commitHash\n"; $versionContent .= "ITFlow Version: " . (defined('APP_VERSION') ? APP_VERSION : 'Unknown') . "\n"; $versionContent .= "Database Version: " . (defined('CURRENT_DATABASE_VERSION') ? CURRENT_DATABASE_VERSION : 'Unknown') . "\n"; - $versionContent .= "Checksum (SHA256): \n"; + $versionContent .= "Checksums (SHA256):\n"; + $versionContent .= " db.sql: $dbSha\n"; + $versionContent .= " uploads.zip: $upSha\n"; file_put_contents($versionFile, $versionContent); + @chmod($versionFile, 0600); - // === 6. Build final ZIP + // === Build final ZIP === $final = new ZipArchive(); if ($final->open($finalZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) { error_log("Failed to create final zip: $finalZip"); http_response_code(500); exit("Internal Server Error: Unable to create backup archive."); } - $final->addFile($sqlFile, "db.sql"); $final->addFile($uploadsZip, "uploads.zip"); $final->addFile($versionFile, "version.txt"); $final->close(); - chmod($finalZip, 0600); + @chmod($finalZip, 0600); - $checksum = hash_file('sha256', $finalZip); - file_put_contents($versionFile, $versionContent . "$checksum\n"); - - // === 7. Serve final ZIP + // === Serve final ZIP with a stable filename === header('Content-Type: application/zip'); - header('Content-Disposition: attachment; filename="' . basename($finalZip) . '"'); + header('X-Content-Type-Options: nosniff'); + header('Content-Disposition: attachment; filename="' . $downloadName . '"'); header('Content-Length: ' . filesize($finalZip)); header('Pragma: public'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Content-Transfer-Encoding: binary'); + // Push file flush(); $fp = fopen($finalZip, 'rb'); fpassthru($fp); fclose($fp); - logAction("System", "Backup Download", "$session_name downloaded full backup."); + // Log + UX + logAction("System", "Backup Download", ($session_name ?? 'Unknown User') . " downloaded full backup."); flash_alert("Full backup downloaded."); exit; } - if (isset($_POST['backup_master_key'])) { validateCSRFToken($_POST['csrf_token']); diff --git a/setup/index.php b/setup/index.php index bd30655c..112f402f 100644 --- a/setup/index.php +++ b/setup/index.php @@ -5,7 +5,8 @@ if (file_exists("../config.php")) { } -include "../functions.php"; +include "../functions.php"; // Global Functions +include "setup_functions.php"; // Setup Only Functions include "../includes/database_version.php"; if (!isset($config_enable_setup)) { @@ -127,134 +128,220 @@ if (isset($_POST['add_database'])) { if (isset($_POST['restore'])) { - // === 1. Validate uploaded file === + // --- CSRF check (add a token to the form; see form snippet below) --- + if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) { + http_response_code(403); + exit("Invalid CSRF token."); + } + + // --- Basic env guards for long operations --- + @set_time_limit(0); + if (function_exists('ini_set')) { @ini_set('memory_limit', '1024M'); } + + // --- 1) Validate uploaded file --- if (!isset($_FILES['backup_zip']) || $_FILES['backup_zip']['error'] !== UPLOAD_ERR_OK) { die("No backup file uploaded or upload failed."); } - $file = $_FILES['backup_zip']; - $fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); - if ($fileExt !== "zip") { + + // Size limit (e.g., 4 GB) + if ($file['size'] > 4 * 1024 * 1024 * 1024) { + die("Backup archive is too large."); + } + + // MIME check + $fi = new finfo(FILEINFO_MIME_TYPE); + $mime = $fi->file($file['tmp_name']); + if ($mime !== 'application/zip' && $mime !== 'application/x-zip-compressed') { + die("Invalid archive type; only .zip is supported."); + } + + // Extension check (defense in depth) + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if ($ext !== 'zip') { die("Only .zip files are allowed."); } - // === 2. Move to secure temp location === - $tempZip = tempnam(sys_get_temp_dir(), "restore_"); + // --- 2) Move to secure temp location --- + $timestamp = date('YmdHis'); + $tempZip = tempnam(sys_get_temp_dir(), "restore_{$timestamp}_"); if (!move_uploaded_file($file["tmp_name"], $tempZip)) { die("Failed to save uploaded backup file."); } + @chmod($tempZip, 0600); + + // --- 3) Extract safely to unique temp dir --- + $tempDir = sys_get_temp_dir() . "/restore_temp_" . bin2hex(random_bytes(6)); + if (!mkdir($tempDir, 0700, true)) { + @unlink($tempZip); + die("Failed to create temp directory."); + } $zip = new ZipArchive; if ($zip->open($tempZip) !== TRUE) { - unlink($tempZip); + @unlink($tempZip); + deleteDir($tempDir); die("Failed to open backup zip file."); } - // === 3. Zip-slip protection and extract to unique dir === - $tempDir = sys_get_temp_dir() . "/restore_temp_" . uniqid(); - mkdir($tempDir, 0700, true); - - for ($i = 0; $i < $zip->numFiles; $i++) { - $stat = $zip->statIndex($i); - if (strpos($stat['name'], '..') !== false) { - $zip->close(); - unlink($tempZip); - die("Invalid file path in ZIP."); - } - } - - if (!$zip->extractTo($tempDir)) { + try { + safeExtractZip($zip, $tempDir); + } catch (Throwable $e) { $zip->close(); - unlink($tempZip); - die("Failed to extract backup contents."); + @unlink($tempZip); + deleteDir($tempDir); + die("Invalid backup archive: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8')); } - $zip->close(); - unlink($tempZip); + @unlink($tempZip); - // === 4. Restore SQL === - $sqlPath = "$tempDir/db.sql"; - if (file_exists($sqlPath)) { - mysqli_query($mysqli, "SET foreign_key_checks = 0"); - $tables = mysqli_query($mysqli, "SHOW TABLES"); - while ($row = mysqli_fetch_array($tables)) { - mysqli_query($mysqli, "DROP TABLE IF EXISTS `" . $row[0] . "`"); - } - mysqli_query($mysqli, "SET foreign_key_checks = 1"); + // Paths inside extracted archive + $sqlPath = $tempDir . "/db.sql"; + $uploadsZip = $tempDir . "/uploads.zip"; + $versionTxt = $tempDir . "/version.txt"; - // Use env var to avoid exposing password - putenv("MYSQL_PWD=$dbpassword"); - $command = sprintf( - 'mysql -h%s -u%s %s < %s', - escapeshellarg($dbhost), - escapeshellarg($dbusername), - escapeshellarg($database), - escapeshellarg($sqlPath) - ); - - exec($command, $output, $returnCode); - if ($returnCode !== 0) { - deleteDir($tempDir); - die("SQL import failed. Error code: $returnCode"); - } - } else { + if (!is_file($sqlPath) || !is_readable($sqlPath)) { deleteDir($tempDir); die("Missing db.sql in the backup archive."); } - - // === 5. Restore uploads directory === - $uploadDir = __DIR__ . "/../uploads/"; - $uploadsZip = "$tempDir/uploads.zip"; - - if (file_exists($uploadsZip)) { - $uploads = new ZipArchive; - if ($uploads->open($uploadsZip) === TRUE) { - // Clean existing uploads - foreach (new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($uploadDir, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ) as $item) { - $item->isDir() ? rmdir($item) : unlink($item); - } - - $uploads->extractTo($uploadDir); - $uploads->close(); - } else { - deleteDir($tempDir); - die("Failed to open uploads.zip in backup."); - } - } else { + if (!is_file($uploadsZip) || !is_readable($uploadsZip)) { deleteDir($tempDir); die("Missing uploads.zip in the backup archive."); } - // === 6. Read version.txt (optional display/logging) === - $versionTxt = "$tempDir/version.txt"; - if (file_exists($versionTxt)) { - $versionInfo = file_get_contents($versionTxt); - logAction("Backup Restore", "Version Info", $versionInfo); + // --- 4) Optional: check version compatibility --- + if (defined('LATEST_DATABASE_VERSION') && is_file($versionTxt)) { + $txt = @file_get_contents($versionTxt) ?: ''; + // Try to find line "Database Version: X" + if (preg_match('/^Database Version:\s*(.+)$/mi', $txt, $m)) { + $backupVersion = trim($m[1]); + $running = LATEST_DATABASE_VERSION; + // If backup schema is newer, abort with instruction + if (version_compare($backupVersion, $running, '>')) { + deleteDir($tempDir); + die("Backup schema ($backupVersion) is newer than this app ($running). Please upgrade ITFlow first, then retry restore."); + } + } } - // === 7. Clean up temp dir === - function deleteDir($dir) { - if (!is_dir($dir)) return; - $items = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($items as $item) { - $item->isDir() ? rmdir($item) : unlink($item); + // --- 5) Restore SQL (drop + import) --- + // Drop all tables + mysqli_query($mysqli, "SET FOREIGN_KEY_CHECKS = 0"); + $tables = mysqli_query($mysqli, "SHOW TABLES"); + if ($tables) { + while ($row = mysqli_fetch_array($tables)) { + $tbl = $row[0]; + mysqli_query($mysqli, "DROP TABLE IF EXISTS `".$mysqli->real_escape_string($tbl)."`"); } - rmdir($dir); } + mysqli_query($mysqli, "SET FOREIGN_KEY_CHECKS = 1"); + + try { + importSqlFile($mysqli, $sqlPath); + } catch (Throwable $e) { + deleteDir($tempDir); + die("SQL import failed: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8')); + } + + // --- 6) Restore uploads via staging + atomic swap --- + $appRoot = realpath(__DIR__ . "/.."); + $uploadDir = realpath($appRoot . "/uploads"); + if ($uploadDir === false) { + // uploads might not exist yet + $uploadDir = $appRoot . "/uploads"; + if (!mkdir($uploadDir, 0750, true)) { + deleteDir($tempDir); + die("Failed to create uploads directory."); + } + $uploadDir = realpath($uploadDir); + } + + if ($uploadDir === false || str_starts_with($uploadDir, $appRoot) === false) { + deleteDir($tempDir); + die("Uploads directory path invalid."); + } + + $staging = $appRoot . "/uploads_restoring_" . bin2hex(random_bytes(4)); + if (!mkdir($staging, 0700, true)) { + deleteDir($tempDir); + die("Failed to create staging directory."); + } + + $uz = new ZipArchive; + if ($uz->open($uploadsZip) !== TRUE) { + deleteDir($staging); + deleteDir($tempDir); + die("Failed to open uploads.zip in backup."); + } + + // IMPORTANT: staging dir should be empty here (as in your existing flow) + $result = extractUploadsZipWithValidationReport($uz, $staging, [ + 'max_file_bytes' => 200 * 1024 * 1024, // adjust per-file size cap + 'blocked_exts' => [ + 'php','php3','php4','php5','php7','php8','phtml','phar', + 'cgi','pl','sh','bash','zsh','exe','dll','bat','cmd','com', + 'ps1','vbs','vb','jar','jsp','asp','aspx','so','dylib','bin' + ], + ]); + $uz->close(); + + if (!$result['ok']) { + // Build a user-friendly report + $lines = ["Unsafe file(s) detected in uploads.zip:"]; + foreach ($result['issues'] as $issue) { + $p = htmlspecialchars($issue['path'], ENT_QUOTES, 'UTF-8'); + $r = htmlspecialchars($issue['reason'], ENT_QUOTES, 'UTF-8'); + $lines[] = "• {$p} — {$r}"; + } + + // Clean staging and temp and show the report + deleteDir($staging); + deleteDir($tempDir); + + $_SESSION['alert_message'] = nl2br(implode("\n", $lines)); + header("Location: ?restore"); + exit; + + } + + // Rotate old uploads out, promote staging in + $backupOld = $appRoot . "/uploads_old_" . time(); + if (!rename($uploadDir, $backupOld)) { + deleteDir($staging); + deleteDir($tempDir); + die("Failed to rotate old uploads."); + } + if (!rename($staging, $uploadDir)) { + // try to revert + @rename($backupOld, $uploadDir); + deleteDir($tempDir); + die("Failed to promote restored uploads."); + } + // Optional: clean old uploads now or keep briefly for rollback + // deleteDir($backupOld); + + // --- 7) Log version info (optional) --- + if (is_file($versionTxt)) { + $versionInfo = @file_get_contents($versionTxt); + if ($versionInfo !== false) { + logAction("Backup Restore", "Version Info", $versionInfo); + } + } + + // --- 8) Cleanup temp dir --- deleteDir($tempDir); - // === 8. Optional: finalize setup flag === - $myfile = fopen("../config.php", "a"); - fwrite($myfile, "\$config_enable_setup = 0;\n\n"); - fclose($myfile); + // --- 9) Finalize setup flag (idempotent) --- + try { + setConfigFlag("../config.php", "config_enable_setup", 0); + } catch (Throwable $e) { + // Non-fatal; warn but continue to login + $_SESSION['alert_message'] = "Backup restored, but failed to finalize setup flag in config.php: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'); + header("Location: ../login.php"); + exit; + } - // === 9. Done === + // --- 10) Done --- $_SESSION['alert_message'] = "Full backup restored successfully."; header("Location: ../login.php"); exit; @@ -1109,9 +1196,15 @@ if (isset($_POST['add_telemetry'])) {

Restore from Backup

-
+ + + +

Large restores may take several minutes. Do not close this page.