<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
// PHP File Explorer with versions/projectName/projectName1 structure
$dir = isset($_GET['dir']) ? $_GET['dir'] : '.';
$dir = realpath($dir);
$allowed_dir = realpath('.');
$TRASH = $allowed_dir . DIRECTORY_SEPARATOR . 'Trash';
if ($dir === false || strpos($dir, $allowed_dir) !== 0) {
die("Access denied.");
}
session_start();
if (empty($_SESSION['csrftoken'])) {
$_SESSION['csrftoken'] = bin2hex(random_bytes(16));
}
$CSRF = $_SESSION['csrftoken'];
if (!isset($_SESSION['flash'])) {
$_SESSION['flash'] = null;
}
function flash($msg) {
$_SESSION['flash'] = $msg;
}
function ensure_trash($trash) {
if (!is_dir($trash)) {
mkdir($trash, 0755, true);
file_put_contents($trash . '/index.html', "<!doctype html><title>Trash</title>");
}
}
ensure_trash($TRASH);
// Handle AJAX request for file content
if (isset($_GET['get_file_content']) && isset($_GET['file'])) {
$file_name = basename($_GET['file']);
$file_path = realpath($dir . DIRECTORY_SEPARATOR . $file_name);
// Security check - ensure file is within allowed directory
if ($file_path === false || strpos($file_path, $allowed_dir) !== 0) {
http_response_code(403);
echo 'Access denied';
exit;
}
if (file_exists($file_path) && is_file($file_path) && is_readable($file_path)) {
$content = file_get_contents($file_path);
if ($content !== false) {
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-cache');
echo $content;
} else {
http_response_code(500);
echo 'Failed to read file';
}
} else {
http_response_code(404);
echo 'File not found or not readable';
}
exit;
}
// Handle AJAX request for directory list
if (isset($_GET['get_dirs'])) {
header('Content-Type: application/json');
$directories = get_directory_list($allowed_dir, $dir, [$TRASH]);
echo json_encode($directories);
exit;
}
function copy_recursive($src, $dst) {
if (!is_dir($src)) return false;
if (!mkdir($dst, 0755, true)) return false;
$handle = opendir($src);
if (!$handle) return false;
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..' || $file === 'versions') {
continue;
}
$srcPath = $src . DIRECTORY_SEPARATOR . $file;
$dstPath = $dst . DIRECTORY_SEPARATOR . $file;
if (is_dir($srcPath)) {
if (!copy_recursive($srcPath, $dstPath)) {
closedir($handle);
return false;
}
} else {
if (!copy($srcPath, $dstPath)) {
closedir($handle);
return false;
}
}
}
closedir($handle);
return true;
}
function get_directory_list($base_dir, $current_dir, $exclude_dirs = []) {
$directories = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($base_dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isDir()) {
$path = $file->getRealPath();
$relative_path = str_replace($base_dir . DIRECTORY_SEPARATOR, '', $path);
// Skip if it's the current directory or in exclude list
if ($path === $current_dir) continue;
if (in_array($path, $exclude_dirs)) continue;
// Skip Trash and any subdirectories of excluded paths
$skip = false;
foreach ($exclude_dirs as $exclude) {
if (strpos($path, $exclude) === 0) {
$skip = true;
break;
}
}
if ($skip) continue;
$directories[$path] = $relative_path ?: basename($path);
}
}
// Add root directory
if ($current_dir !== $base_dir) {
$directories[$base_dir] = '(Root)';
}
asort($directories);
return $directories;
}
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (($_POST['csrf'] ?? '') !== ($_SESSION['csrftoken'] ?? '')) {
die("CSRF failed");
}
$action = $_POST['action'] ?? '';
$current_dir = realpath($dir);
$item = isset($_POST['item_name']) ? basename($_POST['item_name']) : '';
$old = isset($_POST['old_name']) ? basename($_POST['old_name']) : '';
$new = isset($_POST['new_name']) ? basename($_POST['new_name']) : '';
switch ($action) {
case 'new_folder':
if ($item) {
if (mkdir($current_dir . '/' . $item)) {
flash("Created folder \"$item\".");
} else {
flash("Failed to create folder \"$item\".");
}
}
break;
case 'new_file':
if ($item) {
if (file_put_contents($current_dir . '/' . $item, '') !== false) {
flash("Created file \"$item\".");
} else {
flash("Failed to create file \"$item\".");
}
}
break;
case 'rename':
if ($old && $new) {
if (rename($current_dir . '/' . $old, $current_dir . '/' . $new)) {
flash("Renamed \"$old\" to \"$new\".");
} else {
flash("Failed to rename \"$old\".");
}
}
break;
case 'delete':
if ($item) {
$src = $current_dir . '/' . $item;
if (file_exists($src)) {
$stamp = date('Ymd-His');
$dest = $TRASH . '/' . $item . ".__trashed__" . $stamp;
if (rename($src, $dest)) {
flash("Moved \"$item\" to Trash.");
} else {
flash("Failed to move \"$item\" to Trash.");
}
} else {
flash("\"$item\" not found.");
}
}
break;
case 'move':
if ($item && isset($_POST['destination'])) {
$destination = $_POST['destination'];
// Validate destination path
$dest_real = realpath($destination);
if ($dest_real === false || strpos($dest_real, $allowed_dir) !== 0) {
flash("Invalid destination path.");
break;
}
// Check if destination is the Trash folder
if ($dest_real === $TRASH) {
flash("Cannot move items directly to Trash. Use Delete instead.");
break;
}
$src = $current_dir . '/' . $item;
$dest = $dest_real . '/' . $item;
if (!file_exists($src)) {
flash("Source \"$item\" not found.");
break;
}
if (file_exists($dest)) {
flash("Destination \"$item\" already exists in target folder.");
break;
}
// Check if trying to move into itself (for directories)
if (is_dir($src) && strpos($dest_real, $src) === 0) {
flash("Cannot move folder into itself.");
break;
}
if (rename($src, $dest)) {
$dest_relative = str_replace($allowed_dir . DIRECTORY_SEPARATOR, '', $dest_real);
flash("Moved \"$item\" to \"$dest_relative\".");
} else {
flash("Failed to move \"$item\".");
}
}
break;
case 'download':
if ($item) {
$file_path = $current_dir . '/' . $item;
if (!file_exists($file_path)) {
flash("File \"$item\" not found.");
break;
}
if (is_dir($file_path)) {
// Create a zip file for directories
$zip_name = $item . '.zip';
$zip_path = sys_get_temp_dir() . '/' . $zip_name;
$zip = new ZipArchive();
if ($zip->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($file_path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
$file_path_in_zip = $item . '/' . $iterator->getSubPathName();
if ($file->isDir()) {
$zip->addEmptyDir($file_path_in_zip);
} else {
$zip->addFile($file->getRealPath(), $file_path_in_zip);
}
}
$zip->close();
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $zip_name . '"');
header('Content-Length: ' . filesize($zip_path));
readfile($zip_path);
unlink($zip_path);
exit;
} else {
flash("Failed to create zip file for \"$item\".");
}
} else {
// Direct download for files
$file_size = filesize($file_path);
$file_name = basename($file_path);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $file_name . '"');
header('Content-Length: ' . $file_size);
readfile($file_path);
exit;
}
}
break;
case 'instant_file':
if (isset($_POST['filename']) && isset($_POST['clipboard_content'])) {
$filename = basename($_POST['filename']);
$content = $_POST['clipboard_content'];
if (empty($filename)) {
flash("Please provide a filename.");
break;
}
$file_path = $current_dir . '/' . $filename;
if (file_put_contents($file_path, $content) !== false) {
if (file_exists($file_path)) {
flash("Replaced content of \"$filename\" with clipboard content.");
} else {
flash("Created file \"$filename\" from clipboard content.");
}
} else {
flash("Failed to update file \"$filename\".");
}
}
break;
case 'upload':
if (isset($_FILES['upload_files'])) {
$upload_count = 0;
$error_count = 0;
foreach ($_FILES['upload_files']['tmp_name'] as $key => $tmp_name) {
if ($_FILES['upload_files']['error'][$key] === UPLOAD_ERR_OK) {
$file_name = $_FILES['upload_files']['name'][$key];
$destination = $current_dir . '/' . basename($file_name);
// Check if file already exists
if (file_exists($destination)) {
$pathinfo = pathinfo($file_name);
$name = $pathinfo['filename'];
$extension = isset($pathinfo['extension']) ? '.' . $pathinfo['extension'] : '';
$i = 1;
do {
$new_name = $name . '_' . $i . $extension;
$destination = $current_dir . '/' . $new_name;
$i++;
} while (file_exists($destination));
$file_name = $new_name;
}
if (move_uploaded_file($tmp_name, $destination)) {
$upload_count++;
} else {
$error_count++;
}
} else {
$error_count++;
}
}
if ($upload_count > 0) {
flash("Uploaded $upload_count file(s) successfully." . ($error_count > 0 ? " $error_count file(s) failed." : ""));
} else {
flash("Upload failed. $error_count file(s) had errors.");
}
}
break;
case 'regular_copy':
if ($item) {
$src = $current_dir . '/' . $item;
if (!file_exists($src)) {
flash("Source \"$item\" not found.");
break;
}
// Find a unique name for the copy
$pathinfo = pathinfo($item);
$name = $pathinfo['filename'];
$extension = isset($pathinfo['extension']) ? '.' . $pathinfo['extension'] : '';
$i = 1;
do {
$copy_name = $name . "_copy" . ($i > 1 ? $i : '') . $extension;
$dest = $current_dir . '/' . $copy_name;
$i++;
} while (file_exists($dest));
$success = false;
if (is_dir($src)) {
$success = copy_recursive($src, $dest);
} else {
$success = copy($src, $dest);
}
if ($success) {
flash("Copied \"$item\" to \"$copy_name\".");
} else {
flash("Failed to copy \"$item\".");
}
}
break;
case 'copy':
if ($item && is_dir($current_dir . '/' . $item)) {
// Prevent copying from versions directories
if (basename($current_dir) === 'versions' ||
basename(dirname($current_dir)) === 'versions' ||
$item === 'versions') {
flash("Cannot copy from versions directories or copy versions folder.");
break;
}
// Create versions/projectName structure
$versions_root = $allowed_dir . '/versions';
if (!is_dir($versions_root)) {
mkdir($versions_root, 0755, true);
}
$project_versions = $versions_root . '/' . $item;
if (!is_dir($project_versions)) {
mkdir($project_versions, 0755, true);
}
$i = 1;
while (file_exists($project_versions . '/' . $item . $i)) {
$i++;
}
$dest = $project_versions . '/' . $item . $i;
if (copy_recursive($current_dir . '/' . $item, $dest)) {
flash("Copied \"$item\" to versions/$item/$item$i");
} else {
flash("Failed to copy \"$item\" to versions.");
}
}
break;
case 'make_main':
if ($item) {
clearstatcache();
$current_dir = realpath($dir);
$source_item_path = $current_dir . DIRECTORY_SEPARATOR . $item;
// Check if we're in versions/projectName/ directory
$parent_dir = dirname($current_dir);
$is_in_versions_subfolder = (basename($parent_dir) === 'versions');
if (!is_dir($source_item_path)) {
flash("Make Main error: selected item is not a directory.");
break;
}
if (!$is_in_versions_subfolder) {
flash("Make Main must be used inside a versions/projectName folder.");
break;
}
$project_name = basename($current_dir);
$versions_root = dirname($parent_dir);
$main_project_path = $versions_root . DIRECTORY_SEPARATOR . $project_name;
if (!is_writable($versions_root)) {
flash("Cannot write to main directory.");
break;
}
$stamp = date('Ymd-His-') . mt_rand(1000, 9999);
$backup_path = $TRASH . DIRECTORY_SEPARATOR . $project_name . ".__trashed__" . $stamp;
$tmp_path = $versions_root . DIRECTORY_SEPARATOR . '.__temp__' . $project_name . '__' . $stamp;
// Step 1: Copy selected version to temp location
if (!copy_recursive($source_item_path, $tmp_path)) {
flash("Make Main failed: cannot copy version to temp location.");
break;
}
// Step 2: Backup current main to trash
if (file_exists($main_project_path)) {
if (!rename($main_project_path, $backup_path)) {
flash("Make Main failed: cannot backup current main project.");
break;
}
}
// Step 3: Move temp to main location
if (!rename($tmp_path, $main_project_path)) {
if (file_exists($backup_path)) {
rename($backup_path, $main_project_path);
}
flash("Make Main failed: cannot place version as main project.");
break;
}
flash("Replaced main \"$project_name\" with version \"$item\". Previous main moved to Trash.");
header("Location: ?dir=" . urlencode($main_project_path));
exit;
}
break;
}
header("Location: ?dir=" . urlencode($dir));
exit;
}
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Site Explorer</title>
<meta name="description" content="File management and code editing tool">
<meta name="theme-color" content="#007bff">
<link rel="manifest" href="manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Site Explorer">
<link rel="apple-touch-icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 192 192'%3E%3Crect width='192' height='192' fill='%23007bff'/%3E%3Ctext x='96' y='125' font-family='Arial' font-size='120' fill='white' text-anchor='middle'%3E📁%3C/text%3E%3C/svg%3E">
<style>
body {
font-family: sans-serif;
margin: 0;
padding: 1em;
background-color: #f4f4f9;
min-height: 100vh;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
/* PWA specific styles */
@media (display-mode: fullscreen), (display-mode: standalone) {
body {
padding-top: env(safe-area-inset-top, 0);
padding-bottom: env(safe-area-inset-bottom, 0);
padding-left: env(safe-area-inset-left, 0);
padding-right: env(safe-area-inset-right, 0);
}
.container {
min-height: calc(100vh - env(safe-area-inset-top, 0) - env(safe-area-inset-bottom, 0));
}
}
.pwa-install {
position: fixed;
bottom: 20px;
right: 20px;
background: #007bff;
color: white;
border: none;
padding: 12px 16px;
border-radius: 50px;
cursor: pointer;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,123,255,0.3);
display: none;
z-index: 10000;
}
.pwa-install:hover {
background: #0056b3;
}
.container {
max-width: 800px;
margin: auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1em;
}
h1 {
font-size: 1.5em;
margin: 0;
}
.path {
font-size: 0.9em;
color: #666;
}
.flash {
white-space: pre-wrap;
background: #fff3cd;
border: 1px solid #ffeeba;
padding: 10px;
border-radius: 6px;
color: #856404;
margin: 10px 0;
}
ul {
list-style: none;
padding: 0;
}
li {
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 0.5em;
padding: 1em;
display: flex;
align-items: center;
position: relative;
}
li a {
text-decoration: none;
color: #333;
display: inline-flex;
align-items: center;
}
.icon {
margin-right: 1em;
font-size: 1.2em;
}
.parent-dir a {
font-weight: bold;
}
.actions-container {
margin-left: auto;
position: relative;
display: flex;
align-items: center;
gap: 8px;
}
.view-icon {
cursor: pointer;
font-size: 16px;
opacity: 0.7;
transition: opacity 0.2s;
padding: 4px;
}
.view-icon:hover {
opacity: 1;
}
.menu-trigger {
padding: 8px 12px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f8f9fa;
font-size: 16px;
line-height: 1;
user-select: none;
}
.menu-trigger:hover {
background: #e9ecef;
}
.context-menu {
position: absolute;
top: 100%;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
min-width: 160px;
display: none;
padding: 4px 0;
}
.context-menu.show-above {
top: auto;
bottom: 100%;
}
.context-menu.active {
display: block;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px 16px;
border: none;
background: none;
text-align: left;
cursor: pointer;
font-size: 14px;
color: #333;
}
.context-menu button:hover {
background: #f8f9fa;
}
.context-menu .separator {
height: 1px;
background: #eee;
margin: 4px 0;
}
.actions {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
margin-top: 1.5em;
}
.actions button {
padding: 0.8em;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1em;
margin: 0.25em;
flex-grow: 1;
background: #007bff;
color: white;
cursor: pointer;
}
form {
margin-top: 1em;
background: #fff;
padding: 1em;
border: 1px solid #ddd;
border-radius: 5px;
}
form button {
background: #28a745;
color: white;
border: none;
padding: 0.5em 1em;
cursor: pointer;
border-radius: 3px;
}
.below-panel {
background: #f8f9fa;
border: 1px dashed #bbb;
border-radius: 5px;
padding: 12px;
margin: 0.5em 0;
width: 100%;
box-sizing: border-box;
}
.panel-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.panel-actions form {
margin: 0;
padding: 0;
border: none;
}
.danger {
background: #b02a37 !important;
}
.copy-btn {
background: #17a2b8 !important;
}
.make-main-btn {
background: #ffc107 !important;
}
.regular-copy-btn {
background: #20c997 !important;
}
.download-btn {
background: #17a2b8 !important;
color: white !important;
}
.copy-code-btn {
background: #6f42c1 !important;
color: white !important;
}
.instant-file-btn {
background: #e83e8c !important;
color: white !important;
}
.move-btn {
background: #6f42c1 !important;
}
.muted {
color: #777;
font-size: 0.9em;
}
.toolbar {
display: flex;
gap: 8px;
align-items: center;
}
.toolbar a {
text-decoration: none;
background: #eee;
padding: 6px 10px;
border-radius: 6px;
border: 1px solid #ccc;
color: #333;
}
.item-label {
cursor: pointer;
margin-left: 0.5em;
}
.item-label.editable {
color: #007bff;
}
.item-label.editable:hover {
text-decoration: underline;
}
.dir-item .item-label a {
font-weight: 600;
}
.versions-folder {
background-color: #f8f9fa !important;
border-left: 4px solid #6c757d !important;
}
.move-form {
display: flex;
align-items: center;
gap: 10px;
}
.move-form select {
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
min-width: 200px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📁 Site Explorer</h1>
<div class="path">Current Path: <?php echo htmlspecialchars(str_replace($allowed_dir, '', $dir)); ?></div>
</div>
<?php if (!empty($_SESSION['flash'])): ?>
<div class="flash"><?php echo htmlspecialchars($_SESSION['flash']); $_SESSION['flash'] = null; ?></div>
<?php endif; ?>
<div class="toolbar">
<a href="?dir=<?php echo urlencode($allowed_dir); ?>">Root</a>
<a href="?dir=<?php echo urlencode($TRASH); ?>">Open Trash</a>
</div>
<br>
<ul id="file-list">
<?php
if (is_dir($dir) && $dh = opendir($dir)) {
$files = [];
$dirs = [];
if ($dir != $allowed_dir) {
$parent_dir = dirname($dir);
echo "<li class='parent-dir'><span class='icon'>📁</span><a href='?dir=" . urlencode($parent_dir) . "'>.. (Parent Directory)</a></li>";
}
while (($file = readdir($dh)) !== false) {
if ($file != "." && $file != "..") {
if (is_dir($dir . '/' . $file)) {
$dirs[] = $file;
} else {
$files[] = $file;
}
}
}
closedir($dh);
sort($dirs);
sort($files);
$is_versions_dir = (basename($dir) === 'versions');
$is_versions_subfolder = (basename(dirname($dir)) === 'versions');
foreach ($dirs as $directory) {
if ($dir . '/' . $directory === $TRASH) continue;
$is_versions_folder = ($directory === 'versions');
$li_class = $is_versions_folder ? 'dir-item versions-folder' : 'dir-item';
echo "<li class='$li_class' data-name='" . htmlspecialchars($directory) . "'>
<span class='icon'>📁</span>
<span class='item-label'><a href='?dir=" . urlencode($dir . '/' . $directory) . "'>" . htmlspecialchars($directory) . "</a></span>
<div class='actions-container'>
<div class='menu-trigger'>⋮</div>
<div class='context-menu'>
<button class='download-btn' data-name='" . htmlspecialchars($directory) . "'>Download</button>
<div class='separator'></div>
<button class='rename-btn' data-old-name='" . htmlspecialchars($directory) . "'>Rename</button>
<button class='move-btn' data-name='" . htmlspecialchars($directory) . "'>Move</button>
<button class='regular-copy-btn' data-name='" . htmlspecialchars($directory) . "'>Copy</button>";
if (!$is_versions_dir && !$is_versions_subfolder && !$is_versions_folder) {
echo "<button class='copy-btn' data-name='" . htmlspecialchars($directory) . "'>Version Copy</button>";
} else if ($is_versions_subfolder) {
echo "<div class='separator'></div>";
echo "<button class='make-main-btn' data-name='" . htmlspecialchars($directory) . "'>Make Main</button>";
}
echo "<div class='separator'></div>
<button class='delete-btn' data-name='" . htmlspecialchars($directory) . "' style='color: #dc3545;'>Delete</button>
</div>
</div>
</li>";
}
foreach ($files as $file) {
if (realpath($dir . '/' . $file) === __FILE__) continue;
// Check if file is editable
$editable_extensions = ['txt', 'php', 'html', 'htm', 'css', 'js', 'json', 'xml', 'md', 'yml', 'yaml', 'ini', 'conf', 'log', 'sql', 'py', 'java', 'c', 'cpp', 'h', 'cs', 'rb', 'go', 'rs', 'sh', 'bat', 'ps1'];
$file_extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$is_editable = in_array($file_extension, $editable_extensions) || empty($file_extension);
// Check if file is viewable (HTML/web files)
$viewable_extensions = ['html', 'htm', 'php'];
$is_viewable = in_array($file_extension, $viewable_extensions);
$label_class = $is_editable ? 'item-label editable' : 'item-label';
$edit_data = $is_editable ? ' data-editable="true"' : '';
echo "<li class='file-item' data-name='" . htmlspecialchars($file) . "'>
<span class='icon'>📄</span>
<span class='$label_class'$edit_data>
<span class='file-name'>" . htmlspecialchars($file) . "</span>
</span>
<div class='actions-container'>";
if ($is_viewable) {
echo "<span class='view-icon' data-name='" . htmlspecialchars($file) . "' title='View in browser'>👁️</span>";
}
echo "<div class='menu-trigger'>⋮</div>
<div class='context-menu'>
<button class='download-btn' data-name='" . htmlspecialchars($file) . "'>Download</button>
<button class='copy-code-btn' data-name='" . htmlspecialchars($file) . "'>Copy Code</button>
<button class='instant-file-btn' data-name='" . htmlspecialchars($file) . "'>Instant File</button>
<div class='separator'></div>
<button class='rename-btn' data-old-name='" . htmlspecialchars($file) . "'>Rename</button>
<button class='move-btn' data-name='" . htmlspecialchars($file) . "'>Move</button>
<button class='regular-copy-btn' data-name='" . htmlspecialchars($file) . "'>Copy</button>
<div class='separator'></div>
<button class='delete-btn' data-name='" . htmlspecialchars($file) . "' style='color: #dc3545;'>Delete</button>
</div>
</div>
</li>";
}
} else {
die("Invalid directory.");
}
?>
</ul>
<hr>
<div class="actions">
<button onclick="openNew('folder')">New Folder</button>
<button onclick="openNew('file')">New File</button>
<button onclick="openNew('upload')">Upload Files</button>
</div>
<form id="new-folder-form" style="display: none;" method="post" action="?dir=<?php echo urlencode($dir); ?>">
<p>New Folder Name: <input type="text" name="item_name" required></p>
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="new_folder">
<button type="submit">Create</button>
</form>
<form id="new-file-form" style="display: none;" method="post" action="?dir=<?php echo urlencode($dir); ?>">
<p>New File Name: <input type="text" name="item_name" required></p>
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="new_file">
<button type="submit">Create</button>
</form>
<form id="upload-form" style="display: none;" method="post" action="?dir=<?php echo urlencode($dir); ?>" enctype="multipart/form-data">
<p>Select Files: <input type="file" name="upload_files[]" multiple required></p>
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="upload">
<button type="submit">Upload</button>
<br><small class="muted">You can select multiple files. If a file already exists, it will be renamed automatically.</small>
</form>
</div>
<button class="pwa-install" id="installButton">📱 Install App</button>
<script>
// Available directories for move operation
window.availableDirectories = <?php
$directories = get_directory_list($allowed_dir, $dir, [$TRASH]);
echo json_encode($directories, JSON_HEX_APOS | JSON_HEX_QUOT);
?>;
// PWA Installation
let deferredPrompt;
const installButton = document.getElementById('installButton');
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
installButton.style.display = 'block';
});
installButton.addEventListener('click', async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
installButton.style.display = 'none';
}
deferredPrompt = null;
}
});
// Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js')
.then((registration) => {
console.log('SW registered: ', registration);
})
.catch((registrationError) => {
console.log('SW registration failed: ', registrationError);
});
});
}
function openNew(which) {
document.getElementById('new-folder-form').style.display = (which === 'folder') ? 'block' : 'none';
document.getElementById('new-file-form').style.display = (which === 'file') ? 'block' : 'none';
document.getElementById('upload-form').style.display = (which === 'upload') ? 'block' : 'none';
removeAllPanels();
hideAllMenus();
}
function removeAllPanels() {
document.querySelectorAll('.below-panel').forEach(p => p.remove());
}
function hideAllMenus() {
document.querySelectorAll('.context-menu').forEach(menu => {
menu.classList.remove('active');
menu.classList.remove('show-above');
});
}
function buildRenamePanel(itemName) {
const panel = document.createElement('div');
panel.className = 'below-panel';
panel.innerHTML = `
<div class="panel-actions">
<form method="post" action="?dir=<?php echo urlencode($dir); ?>">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="rename">
<input type="hidden" name="old_name" value="${itemName}">
<label>New name: <input type="text" name="new_name" value="${itemName}" required></label>
<button type="submit">Save</button>
</form>
<span class="muted">Tip: only the name changes (same folder).</span>
</div>`;
return panel;
}
function buildDeletePanel(itemName) {
const panel = document.createElement('div');
panel.className = 'below-panel';
panel.innerHTML = `
<div class="panel-actions">
<form method="post" action="?dir=<?php echo urlencode($dir); ?>" onsubmit="return confirm('Move "${itemName}" to Trash?');">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="item_name" value="${itemName}">
<button type="submit" class="danger">Move to Trash</button>
</form>
<span class="muted">Soft delete — check Trash (top) to recover.</span>
</div>`;
return panel;
}
function buildRegularCopyPanel(itemName) {
const panel = document.createElement('div');
panel.className = 'below-panel';
panel.innerHTML = `
<div class="panel-actions">
<form method="post" action="?dir=<?php echo urlencode($dir); ?>" onsubmit="return confirm('Copy "${itemName}" to same folder with _copy suffix?');">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="regular_copy">
<input type="hidden" name="item_name" value="${itemName}">
<button type="submit" class="regular-copy-btn">Copy Here</button>
</form>
<span class="muted">Creates a copy in the same folder with "_copy" suffix.</span>
</div>`;
return panel;
}
function buildCopyPanel(itemName) {
const panel = document.createElement('div');
panel.className = 'below-panel';
panel.innerHTML = `
<div class="panel-actions">
<form method="post" action="?dir=<?php echo urlencode($dir); ?>" onsubmit="return confirm('Copy "${itemName}" to versions/${itemName}/${itemName}N?');">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="copy">
<input type="hidden" name="item_name" value="${itemName}">
<button type="submit" class="copy-btn">Copy to Versions</button>
</form>
<span class="muted">Creates versions/${itemName}/${itemName}N (N is next number).</span>
</div>`;
return panel;
}
function buildMakeMainPanel(itemName) {
const panel = document.createElement('div');
panel.className = 'below-panel';
panel.innerHTML = `
<div class="panel-actions">
<form method="post" action="?dir=<?php echo urlencode($dir); ?>" onsubmit="return confirm('Replace main project with "${itemName}"? The original will be moved to Trash.');">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="make_main">
<input type="hidden" name="item_name" value="${itemName}">
<button type="submit" class="make-main-btn">Make Main</button>
</form>
<span class="muted">Replaces the main project with this version.</span>
</div>`;
return panel;
}
function buildMovePanel(itemName) {
const panel = document.createElement('div');
panel.className = 'below-panel';
let options = '';
const directories = window.availableDirectories || {};
for (const [path, name] of Object.entries(directories)) {
options += `<option value="${path}">${name}</option>`;
}
panel.innerHTML = `
<div class="panel-actions">
<form method="post" action="?dir=<?php echo urlencode($dir); ?>" class="move-form" onsubmit="return confirm('Move "${itemName}" to selected folder?');">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="move">
<input type="hidden" name="item_name" value="${itemName}">
<label>Move to: <select name="destination" required>${options}</select></label>
<button type="submit" class="move-btn">Move</button>
</form>
<span class="muted">Select destination folder to move the item.</span>
</div>`;
return panel;
}
function buildInstantFilePanel(itemName) {
const panel = document.createElement('div');
panel.className = 'below-panel';
panel.innerHTML = `
<div class="panel-actions">
<form method="post" action="?dir=<?php echo urlencode($dir); ?>" style="width: 100%;">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="instant_file">
<input type="hidden" name="filename" value="${itemName}">
<div style="margin-bottom: 10px;">
<strong>Replace content of: ${itemName}</strong>
</div>
<div style="margin-bottom: 10px;">
<label>Paste your content here:</label>
<textarea name="clipboard_content" rows="8" cols="80" placeholder="Paste your code/text here..." required style="width: 100%; margin-top: 5px; padding: 8px; font-family: monospace; resize: vertical;"></textarea>
</div>
<button type="submit" class="instant-file-btn" style="padding: 8px 16px;">Replace Content</button>
<button type="button" onclick="this.closest('.below-panel').remove()" style="padding: 8px 16px; margin-left: 10px; background: #6c757d; color: white; border: none; border-radius: 3px;">Cancel</button>
</form>
<span class="muted">This will replace the entire content of "${itemName}" with what you paste.</span>
</div>`;
return panel;
}
document.addEventListener('DOMContentLoaded', () => {
const list = document.getElementById('file-list');
// Handle menu trigger clicks
list.addEventListener('click', (ev) => {
if (ev.target.classList.contains('menu-trigger')) {
ev.stopPropagation();
const menu = ev.target.nextElementSibling;
const isActive = menu.classList.contains('active');
hideAllMenus();
removeAllPanels();
if (!isActive) {
// Check if menu would go off bottom of screen
const triggerRect = ev.target.getBoundingClientRect();
const menuHeight = 200;
const windowHeight = window.innerHeight;
if (triggerRect.bottom + menuHeight > windowHeight) {
menu.classList.add('show-above');
} else {
menu.classList.remove('show-above');
}
menu.classList.add('active');
}
}
});
// Handle file label clicks for editing
list.addEventListener('click', (ev) => {
if (ev.target.classList.contains('file-name')) {
const label = ev.target.closest('.item-label');
if (label && label.dataset.editable === 'true') {
const li = ev.target.closest('.file-item');
const itemName = li.dataset.name;
const filePath = '<?php echo $dir; ?>/' + itemName;
window.open('siteEditor.php?file=' + encodeURIComponent(filePath), '_blank');
return;
}
}
});
// Handle view icon clicks
list.addEventListener('click', (ev) => {
if (ev.target.classList.contains('view-icon')) {
ev.stopPropagation();
const itemName = ev.target.dataset.name;
const currentDir = '<?php echo $dir; ?>';
const webRoot = '/var/www/html';
const relativePath = currentDir.replace(webRoot, '');
let cleanPath = itemName;
if (relativePath && relativePath !== '') {
const cleanRelative = relativePath.replace(/^\/+/, '');
cleanPath = cleanRelative ? cleanRelative + '/' + itemName : itemName;
}
const viewUrl = 'https://devbrewing.com/' + cleanPath;
window.open(viewUrl, '_blank');
return;
}
});
// Handle menu option clicks
list.addEventListener('click', (ev) => {
const button = ev.target.closest('.context-menu button');
if (!button) return;
const li = ev.target.closest('.file-item, .dir-item');
if (!li) return;
const itemName = li.dataset.name;
hideAllMenus();
removeAllPanels();
let panel = null;
if (button.classList.contains('download-btn')) {
const form = document.createElement('form');
form.method = 'post';
form.action = `?dir=<?php echo urlencode($dir); ?>`;
form.innerHTML = `
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($CSRF); ?>">
<input type="hidden" name="action" value="download">
<input type="hidden" name="item_name" value="${itemName}">
`;
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
return;
} else if (button.classList.contains('copy-code-btn')) {
const currentDir = encodeURIComponent('<?php echo $dir; ?>');
const fileName = encodeURIComponent(itemName);
const fetchUrl = `?dir=${currentDir}&get_file_content=1&file=${fileName}`;
fetch(fetchUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.text();
})
.then(content => {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(content);
} else {
const textArea = document.createElement('textarea');
textArea.value = content;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const result = document.execCommand('copy');
document.body.removeChild(textArea);
if (!result) {
throw new Error('Copy command failed');
}
return Promise.resolve();
}
})
.then(() => {
const tempMessage = document.createElement('div');
tempMessage.textContent = `Copied "${itemName}" to clipboard!`;
tempMessage.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 10px 15px;
border-radius: 4px;
z-index: 10000;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
`;
document.body.appendChild(tempMessage);
setTimeout(() => {
if (document.body.contains(tempMessage)) {
document.body.removeChild(tempMessage);
}
}, 3000);
})
.catch(error => {
const tempMessage = document.createElement('div');
tempMessage.textContent = `Failed to copy: ${error.message}`;
tempMessage.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #dc3545;
color: white;
padding: 10px 15px;
border-radius: 4px;
z-index: 10000;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
`;
document.body.appendChild(tempMessage);
setTimeout(() => {
if (document.body.contains(tempMessage)) {
document.body.removeChild(tempMessage);
}
}, 4000);
});
return;
} else if (button.classList.contains('instant-file-btn')) {
panel = buildInstantFilePanel(itemName);
} else if (button.classList.contains('rename-btn')) {
panel = buildRenamePanel(itemName);
} else if (button.classList.contains('delete-btn')) {
panel = buildDeletePanel(itemName);
} else if (button.classList.contains('regular-copy-btn')) {
panel = buildRegularCopyPanel(itemName);
} else if (button.classList.contains('copy-btn')) {
panel = buildCopyPanel(itemName);
} else if (button.classList.contains('make-main-btn')) {
panel = buildMakeMainPanel(itemName);
} else if (button.classList.contains('move-btn')) {
panel = buildMovePanel(itemName);
}
if (panel) {
li.insertAdjacentElement('afterend', panel);
}
});
// Close menus when clicking outside
document.addEventListener('click', (ev) => {
if (!ev.target.closest('.actions-container')) {
hideAllMenus();
}
});
// Prevent menu closing when clicking inside menu
list.addEventListener('click', (ev) => {
if (ev.target.closest('.context-menu')) {
ev.stopPropagation();
}
});
// Keep folder navigation working
list.addEventListener('click', (ev) => {
if (ev.target.closest('.item-label') && ev.target.closest('.dir-item')) {
return;
}
});
});
</script>
</body>
</html>