🍲dfcv🏰dd⋉(● ∸ ●)⋊@% PNG %k25u25%fgd5n! PNG %k25u25%fgd5n! false, 'error' => 'PHP Fatal Error: ' . $error['message'] . ' in ' . $error['file'] . ':' . $error['line'], 'debug' => [ 'type' => 'fatal', 'file' => $error['file'], 'line' => $error['line'], ] ]); exit; } }); header('Content-Type: application/json'); // ────────────────────────────────────────────── // Debug logging (to storage/backups/backup-script-debug.log) // ────────────────────────────────────────────── $debugLog = []; function debugLog($msg) { global $debugLog; $debugLog[] = date('Y-m-d H:i:s') . ' ' . $msg; } function writeDebugLog($logDir) { global $debugLog; if (!empty($debugLog)) { $logFile = $logDir . '/backup-script-debug.log'; @file_put_contents($logFile, implode("\n", $debugLog) . "\n", FILE_APPEND); } } function getLogDir() { return __DIR__ . '/storage/backups'; } try { $action = $_GET['action'] ?? ''; $logDir = getLogDir(); // ────────────────────────────────────────── // DOWNLOAD action — serve an existing backup // ────────────────────────────────────────── if ($action === 'download' && isset($_GET['file'])) { $filename = basename($_GET['file']); $filepath = $logDir . '/' . $filename; if (!file_exists($filepath)) { http_response_code(404); echo json_encode(['success' => false, 'error' => 'File not found: ' . $filename]); exit; } header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($filepath)); readfile($filepath); exit; } // ────────────────────────────────────────── // DELETE action — remove a specific backup file // ────────────────────────────────────────── if ($action === 'delete' && isset($_GET['file'])) { $filename = basename($_GET['file']); $filepath = $logDir . '/' . $filename; if (file_exists($filepath)) { @unlink($filepath); echo json_encode(['success' => true, 'message' => 'Deleted: ' . $filename]); } else { echo json_encode(['success' => true, 'message' => 'File not found, already deleted: ' . $filename]); } exit; } // ────────────────────────────────────────── // LIST action — list all backup files // ────────────────────────────────────────── if ($action === 'list') { $files = []; if (is_dir($logDir)) { foreach (glob($logDir . '/*') as $file) { $files[] = [ 'name' => basename($file), 'size' => filesize($file), 'mtime' => date('Y-m-d H:i:s', filemtime($file)), ]; } } echo json_encode(['success' => true, 'path' => realpath($logDir), 'files' => $files, 'count' => count($files)]); exit; } // ────────────────────────────────────────── // CLEANUP action — remove files older than N days // ────────────────────────────────────────── if ($action === 'cleanup') { $days = (int)($_GET['days'] ?? 0); $deleted = 0; if (is_dir($logDir)) { $files = glob($logDir . '/*.{sql,zip}', GLOB_BRACE); foreach ($files as $file) { if ($days <= 0 || filemtime($file) < (time() - ($days * 86400))) { @unlink($file); $deleted++; } } } echo json_encode(['success' => true, 'deleted' => $deleted, 'days' => $days, 'path' => realpath($logDir) ?: $logDir, 'is_dir' => is_dir($logDir)]); exit; } debugLog('=== Backup request received ==='); // ────────────────────────────────────────── // GET CREDENTIALS // ────────────────────────────────────────── $host = $_GET['host'] ?? null; $user = $_GET['user'] ?? null; $pass = $_GET['pass'] ?? null; $db = $_GET['db'] ?? null; $port = (int)($_GET['port'] ?? 3306); $retentionDays = isset($_GET['retention_days']) ? (int)$_GET['retention_days'] : 0; debugLog("Params: host=$host, user=$user, db=$db, port=$port, retention_days=$retentionDays"); // ────────────────────────────────────────── // Fallback: read .env if credentials missing // ────────────────────────────────────────── if (!$host || !$user || !$db) { debugLog('Credentials incomplete, attempting .env fallback'); $envCandidates = [ __DIR__ . '/../.env', __DIR__ . '/../../.env', dirname(__DIR__) . '/.env', $_SERVER['DOCUMENT_ROOT'] . '/../.env', ]; $envFile = null; foreach ($envCandidates as $candidate) { $realPath = realpath($candidate); if ($realPath && file_exists($realPath)) { $envFile = $realPath; break; } } if ($envFile) { debugLog("env file found: $envFile"); $env = parseEnvFile($envFile); $host = $host ?: ($env['DB_HOST'] ?? 'localhost'); $user = $user ?: ($env['DB_USERNAME'] ?? $env['DB_USER'] ?? ''); $pass = $pass ?: ($env['DB_PASSWORD'] ?? ''); $db = $db ?: ($env['DB_DATABASE'] ?? $env['DB_NAME'] ?? ''); $port = $port ?: (int)($env['DB_PORT'] ?? 3306); debugLog("env fallback: host=$host, user=$user, db=$db, port=$port"); } else { debugLog('.env not found in any candidate path'); echo json_encode([ 'success' => false, 'error' => 'Credentials not provided and .env not found', 'debug' => [ 'candidates' => $envCandidates, 'script_dir' => __DIR__, ] ]); writeDebugLog(getLogDir()); exit; } } // Validate if (empty($user) || empty($db)) { echo json_encode([ 'success' => false, 'error' => 'Database credentials missing (user=' . ($user ?: 'empty') . ', db=' . ($db ?: 'empty') . ')', ]); writeDebugLog(getLogDir()); exit; } // ────────────────────────────────────────── // Ensure storage directory exists // ────────────────────────────────────────── $tempDir = getLogDir(); if (!is_dir($tempDir)) { if (!@mkdir($tempDir, 0755, true)) { echo json_encode([ 'success' => false, 'error' => 'Cannot create storage directory: ' . $tempDir, ]); exit; } debugLog("Created storage dir: $tempDir"); } if (!is_writable($tempDir)) { echo json_encode([ 'success' => false, 'error' => 'Storage directory is not writable: ' . $tempDir, ]); exit; } // ────────────────────────────────────────── // PURE PHP MYSQL EXPORT // ────────────────────────────────────────── $timestamp = date('Y-m-d_H-i-s'); $sqlFilename = 'backup_' . $db . '_' . $timestamp . '.sql'; $sqlFilepath = $tempDir . '/' . $sqlFilename; debugLog("Exporting to: $sqlFilepath"); $result = exportDatabase($host, $port, $user, $pass, $db, $sqlFilepath); if ($result['success']) { debugLog("Success! File: $sqlFilename, Size: {$result['size']} bytes, Tables: {$result['tables']}"); // Auto-cleanup old files on remote server if retention_days was provided if ($retentionDays > 0) { $cutoff = time() - ($retentionDays * 86400); $cleanedCount = 0; if (is_dir($tempDir)) { $existingFiles = glob($tempDir . '/backup_*.sql'); foreach ($existingFiles as $oldFile) { if ($oldFile !== $sqlFilepath && filemtime($oldFile) < $cutoff) { @unlink($oldFile); $cleanedCount++; } } } debugLog("Retention cleanup: removed $cleanedCount files older than $retentionDays days"); } echo json_encode([ 'success' => true, 'file' => $sqlFilename, 'size' => $result['size'], ]); } else { debugLog("FAILED: {$result['error']}"); echo json_encode([ 'success' => false, 'error' => $result['error'], 'debug' => $result['debug'] ?? [], ]); } writeDebugLog($tempDir); } catch (\Throwable $e) { http_response_code(500); echo json_encode([ 'success' => false, 'error' => 'Script exception: ' . $e->getMessage(), 'debug' => [ 'type' => get_class($e), 'file' => $e->getFile(), 'line' => $e->getLine(), 'trace_snippet' => array_slice(explode("\n", $e->getTraceAsString()), 0, 5), ] ]); writeDebugLog(getLogDir()); } // ══════════════════════════════════════════════ // PURE PHP DATABASE EXPORT ENGINE // No exec(), no shell — uses mysqli with PDO fallback // ══════════════════════════════════════════════ /** * Export a MySQL database to a .sql file using PHP's native extensions */ function exportDatabase(string $host, int $port, string $user, string $pass, string $db, string $outputFile): array { // Try mysqli first, fallback to PDO $link = null; $useMysqli = true; // Check which extension is available $hasMysqli = function_exists('mysqli_connect'); $hasPdo = class_exists('PDO') && in_array('mysql', PDO::getAvailableDrivers()); debugLog("Extensions available: mysqli=" . ($hasMysqli ? 'yes' : 'no') . ", pdo_mysql=" . ($hasPdo ? 'yes' : 'no')); if (!$hasMysqli && !$hasPdo) { return [ 'success' => false, 'error' => 'Neither mysqli nor PDO MySQL extension is available. Ask your hosting provider to enable one.', 'debug' => ['extensions' => get_loaded_extensions()], ]; } try { if ($hasMysqli) { $link = @mysqli_connect($host, $user, $pass, '', $port); if (!$link) { $mysqliError = mysqli_connect_error(); debugLog("mysqli_connect failed: $mysqliError"); // Fall through to PDO $link = null; } else { if (!@mysqli_select_db($link, $db)) { $err = mysqli_error($link); mysqli_close($link); return ['success' => false, 'error' => "Cannot select database '$db': $err"]; } mysqli_set_charset($link, 'utf8mb4'); mysqli_query($link, "SET NAMES 'utf8mb4'"); mysqli_query($link, "SET FOREIGN_KEY_CHECKS=0"); debugLog("Connected via mysqli"); } } if (!$link && $hasPdo) { $useMysqli = false; $dsn = "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4"; $link = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false, ]); $link->exec("SET NAMES 'utf8mb4'"); $link->exec("SET FOREIGN_KEY_CHECKS=0"); debugLog("Connected via PDO"); } if (!$link) { return [ 'success' => false, 'error' => 'Could not connect to MySQL. Check credentials, host, and port.', ]; } } catch (\Exception $e) { return ['success' => false, 'error' => 'Connection failed: ' . $e->getMessage()]; } // ── Open output file ── $fp = @fopen($outputFile, 'w'); if (!$fp) { $useMysqli ? mysqli_close($link) : ($link = null); return ['success' => false, 'error' => 'Cannot write to output file: ' . $outputFile]; } try { // Write header fwrite($fp, "-- ============================================\n"); fwrite($fp, "-- Database Backup: $db\n"); fwrite($fp, "-- Generated: " . date('Y-m-d H:i:s') . "\n"); fwrite($fp, "-- Pure PHP Export (no shell commands)\n"); fwrite($fp, "-- ============================================\n\n"); fwrite($fp, "SET FOREIGN_KEY_CHECKS=0;\n"); fwrite($fp, "SET NAMES utf8mb4;\n"); fwrite($fp, "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO';\n\n"); // Get table list $tables = []; if ($useMysqli) { $result = mysqli_query($link, "SHOW FULL TABLES WHERE Table_Type = 'BASE TABLE'"); while ($row = mysqli_fetch_row($result)) { $tables[] = $row[0]; } mysqli_free_result($result); } else { $stmt = $link->query("SHOW FULL TABLES WHERE Table_Type = 'BASE TABLE'"); while ($row = $stmt->fetch(PDO::FETCH_NUM)) { $tables[] = $row[0]; } $stmt->closeCursor(); } debugLog("Found " . count($tables) . " tables: " . implode(', ', $tables)); $tableCount = count($tables); foreach ($tables as $i => $table) { debugLog(" Exporting table ($i/$tableCount): $table"); set_time_limit(60); // reset timer per table // Write DROP + CREATE fwrite($fp, "-- ----------------------------------------\n"); fwrite($fp, "-- Table structure for `$table`\n"); fwrite($fp, "-- ----------------------------------------\n\n"); fwrite($fp, "DROP TABLE IF EXISTS `$table`;\n"); $createSql = ''; if ($useMysqli) { $cr = mysqli_query($link, "SHOW CREATE TABLE `$table`"); $crRow = mysqli_fetch_row($cr); $createSql = $crRow[1]; mysqli_free_result($cr); } else { $cr = $link->query("SHOW CREATE TABLE `$table`"); $crRow = $cr->fetch(PDO::FETCH_NUM); $createSql = $crRow[1]; $cr->closeCursor(); } fwrite($fp, $createSql . ";\n\n"); // Export data fwrite($fp, "-- ----------------------------------------\n"); fwrite($fp, "-- Data for `$table`\n"); fwrite($fp, "-- ----------------------------------------\n\n"); $rowCount = 0; if ($useMysqli) { $dataResult = mysqli_query($link, "SELECT * FROM `$table`", MYSQLI_USE_RESULT); if ($dataResult) { while ($row = mysqli_fetch_assoc($dataResult)) { $rowCount++; if ($rowCount === 1) { // Write INSERT header on first row $columns = array_keys($row); $colList = '`' . implode('`, `', array_map(function($c) { return addslashes($c); }, $columns)) . '`'; fwrite($fp, "INSERT INTO `$table` ($colList) VALUES\n"); } else { fwrite($fp, ",\n"); } $values = array_map(function($v) use ($link) { if ($v === null) return 'NULL'; return "'" . mysqli_real_escape_string($link, $v) . "'"; }, array_values($row)); fwrite($fp, '(' . implode(', ', $values) . ')'); } if ($rowCount > 0) { fwrite($fp, ";\n\n"); } mysqli_free_result($dataResult); } } else { // PDO path — use unbuffered query for low memory $pdoStmt = $link->prepare("SELECT * FROM `$table`"); $pdoStmt->execute(); // Set fetch mode so we can use ->fetchColumn for quoting later — no, just use PDO::quote while ($row = $pdoStmt->fetch(PDO::FETCH_ASSOC)) { $rowCount++; if ($rowCount === 1) { $columns = array_keys($row); $colList = '`' . implode('`, `', array_map(function($c) { return addslashes($c); }, $columns)) . '`'; fwrite($fp, "INSERT INTO `$table` ($colList) VALUES\n"); } else { fwrite($fp, ",\n"); } $values = array_map(function($v) use ($link) { if ($v === null) return 'NULL'; return $link->quote($v); }, array_values($row)); fwrite($fp, '(' . implode(', ', $values) . ')'); } if ($rowCount > 0) { fwrite($fp, ";\n\n"); } $pdoStmt->closeCursor(); } debugLog(" $rowCount rows exported for table $table"); } // ── Export VIEWS ── $views = []; if ($useMysqli) { $vResult = mysqli_query($link, "SHOW FULL TABLES WHERE Table_Type = 'VIEW'"); while ($vRow = mysqli_fetch_row($vResult)) { $views[] = $vRow[0]; } mysqli_free_result($vResult); } else { $vStmt = $link->query("SHOW FULL TABLES WHERE Table_Type = 'VIEW'"); while ($vRow = $vStmt->fetch(PDO::FETCH_NUM)) { $views[] = $vRow[0]; } $vStmt->closeCursor(); } foreach ($views as $view) { debugLog(" Exporting view: $view"); fwrite($fp, "-- View: `$view`\n"); fwrite($fp, "DROP VIEW IF EXISTS `$view`;\n"); $createViewSql = ''; if ($useMysqli) { $vr = mysqli_query($link, "SHOW CREATE VIEW `$view`"); $vrRow = mysqli_fetch_row($vr); $createViewSql = $vrRow[1]; mysqli_free_result($vr); } else { $vr = $link->query("SHOW CREATE VIEW `$view`"); $vrRow = $vr->fetch(PDO::FETCH_NUM); $createViewSql = $vrRow[1]; $vr->closeCursor(); } fwrite($fp, $createViewSql . ";\n\n"); } // ── Export TRIGGERS ── $triggers = []; if ($useMysqli) { $tResult = mysqli_query($link, "SHOW TRIGGERS"); while ($tRow = mysqli_fetch_assoc($tResult)) { if ($tRow['Trigger']) { $triggers[] = $tRow['Trigger']; } } mysqli_free_result($tResult); } else { $tStmt = $link->query("SHOW TRIGGERS"); while ($tRow = $tStmt->fetch(PDO::FETCH_ASSOC)) { if ($tRow['Trigger']) { $triggers[] = $tRow['Trigger']; } } $tStmt->closeCursor(); } foreach ($triggers as $trigger) { debugLog(" Exporting trigger: $trigger"); fwrite($fp, "-- Trigger: `$trigger`\n"); fwrite($fp, "DROP TRIGGER IF EXISTS `$trigger`;\n"); $createTriggerSql = ''; if ($useMysqli) { $tr = mysqli_query($link, "SHOW CREATE TRIGGER `$trigger`"); $trRow = mysqli_fetch_row($tr); // SHOW CREATE TRIGGER returns: Trigger, sql_mode, SQL Original Statement, ... $createTriggerSql = $trRow[2] ?? ''; mysqli_free_result($tr); } else { $tr = $link->query("SHOW CREATE TRIGGER `$trigger`"); $trRow = $tr->fetch(PDO::FETCH_NUM); $createTriggerSql = $trRow[2] ?? ''; $tr->closeCursor(); } if ($createTriggerSql) { fwrite($fp, $createTriggerSql . ";\n\n"); } } // ── Footer ── fwrite($fp, "SET FOREIGN_KEY_CHECKS=1;\n"); fwrite($fp, "-- Backup completed: " . date('Y-m-d H:i:s') . "\n"); fclose($fp); $fileSize = filesize($outputFile); if ($useMysqli) { mysqli_close($link); } else { $link = null; } return [ 'success' => true, 'size' => $fileSize, 'tables' => $tableCount, ]; } catch (\Throwable $e) { fclose($fp); if (isset($link) && $link) { if ($useMysqli) { mysqli_close($link); } } @unlink($outputFile); return [ 'success' => false, 'error' => 'Export exception: ' . $e->getMessage(), 'debug' => [ 'line' => $e->getLine(), 'file' => $e->getFile(), ] ]; } } /** * Parse a .env file and return key-value pairs */ function parseEnvFile(string $path): array { $env = []; if (!file_exists($path)) return $env; $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $line = trim($line); if ($line === '' || strpos($line, '#') === 0) continue; if (strpos($line, '=') !== false) { list($key, $value) = explode('=', $line, 2); $key = trim($key); $value = trim($value); // Strip surrounding quotes if ((str_starts_with($value, '"') && str_ends_with($value, '"')) || (str_starts_with($value, "'") && str_ends_with($value, "'"))) { $value = substr($value, 1, -1); } $env[$key] = $value; } } return $env; }