🐘
siteExplorer.php
Back
📝 Php ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
<?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 &quot;${itemName}&quot; 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 &quot;${itemName}&quot; 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 &quot;${itemName}&quot; 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 &quot;${itemName}&quot;? 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 &quot;${itemName}&quot; 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>