diff --git a/README.md b/README.md index 690087b..649e7e5 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,14 @@ This project is a lightweight, secure web application for uploading, editing, an --- -## Changelog +## changes 3/8/2025 + +- Validation was added in endpoints. +- Toast notifications were implemented in domUtils.js and integrated throughout the app. +- Modals replaced inline prompts and confirms for rename, create, delete, copy, and move actions. +- Folder tree UI was added and improved to be interactive plus reflect the current state after actions. + +## changes 3/7/2025 - **Module Refactoring:** - Split the original `utils.js` into multiple ES6 modules for network requests, DOM utilities, file management, folder management, uploads, and authentication. diff --git a/addUser.php b/addUser.php index 55fa83f..7ced597 100644 --- a/addUser.php +++ b/addUser.php @@ -41,6 +41,12 @@ if (!$newUsername || !$newPassword) { exit; } +// Validate username using preg_match (allow letters, numbers, underscores, dashes, and spaces). +if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) { + echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]); + exit; +} + // Ensure users.txt exists if (!file_exists($usersFile)) { file_put_contents($usersFile, ''); diff --git a/auth.js b/auth.js index 0e37cf9..086b04d 100644 --- a/auth.js +++ b/auth.js @@ -1,7 +1,7 @@ // auth.js import { sendRequest } from './networkUtils.js'; -import { toggleVisibility } from './domUtils.js'; +import { toggleVisibility, showToast } from './domUtils.js'; // Import loadFileList from fileManager.js to refresh the file list upon login. import { loadFileList } from './fileManager.js'; @@ -23,33 +23,15 @@ export function initAuth() { if (data.success) { console.log("βœ… Login successful. Reloading page."); window.location.reload(); + sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!"); } else { - alert("Login failed: " + (data.error || "Unknown error")); + showToast("Login failed: " + (data.error || "Unknown error")); } }) .catch(error => console.error("❌ Error logging in:", error)); }); } -// Helper function to update UI based on authentication. -function updateUIOnLogin(isAdmin) { - toggleVisibility("loginForm", false); - toggleVisibility("mainOperations", true); - toggleVisibility("uploadFileForm", true); - toggleVisibility("fileListContainer", true); - - if (isAdmin) { - document.getElementById("addUserBtn").style.display = "block"; - document.getElementById("removeUserBtn").style.display = "block"; - } else { - document.getElementById("addUserBtn").style.display = "none"; - document.getElementById("removeUserBtn").style.display = "none"; - } - - document.querySelector(".header-buttons").style.visibility = "visible"; - loadFileList(window.currentFolder || "root"); -} - // Set up the logout button. document.getElementById("logoutBtn").addEventListener("click", function () { fetch("logout.php", { method: "POST" }) @@ -68,7 +50,7 @@ function updateUIOnLogin(isAdmin) { const newPassword = document.getElementById("newPassword").value.trim(); const isAdmin = document.getElementById("isAdmin").checked; if (!newUsername || !newPassword) { - alert("Username and password are required!"); + showToast("Username and password are required!"); return; } let url = "addUser.php"; @@ -83,11 +65,11 @@ function updateUIOnLogin(isAdmin) { .then(response => response.json()) .then(data => { if (data.success) { - alert("User added successfully!"); + showToast("User added successfully!"); closeAddUserModal(); checkAuthentication(); } else { - alert("Error: " + (data.error || "Could not add user")); + showToast("Error: " + (data.error || "Could not add user")); } }) .catch(error => console.error("Error adding user:", error)); @@ -107,7 +89,7 @@ function updateUIOnLogin(isAdmin) { const selectElem = document.getElementById("removeUsernameSelect"); const usernameToRemove = selectElem.value; if (!usernameToRemove) { - alert("Please select a user to remove."); + showToast("Please select a user to remove."); return; } if (!confirm("Are you sure you want to delete user " + usernameToRemove + "?")) { @@ -121,11 +103,11 @@ function updateUIOnLogin(isAdmin) { .then(response => response.json()) .then(data => { if (data.success) { - alert("User removed successfully!"); + showToast("User removed successfully!"); closeRemoveUserModal(); loadUserList(); } else { - alert("Error: " + (data.error || "Could not remove user")); + showToast("Error: " + (data.error || "Could not remove user")); } }) .catch(error => console.error("Error removing user:", error)); @@ -140,6 +122,7 @@ export function checkAuthentication() { .then(data => { if (data.setup) { window.setupMode = true; + showToast("Setup mode: No users found. Please add an admin user."); // In setup mode, hide login and main operations; show Add User modal. toggleVisibility("loginForm", false); toggleVisibility("mainOperations", false); @@ -168,6 +151,7 @@ export function checkAuthentication() { } document.querySelector(".header-buttons").style.visibility = "visible"; } else { + showToast("Please log in to continue."); toggleVisibility("loginForm", true); toggleVisibility("mainOperations", false); toggleVisibility("uploadFileForm", false); @@ -213,7 +197,7 @@ function loadUserList() { selectElem.appendChild(option); }); if (selectElem.options.length === 0) { - alert("No other users found to remove."); + showToast("No other users found to remove."); closeRemoveUserModal(); } }) diff --git a/auth.php b/auth.php index 5970bda..4f699c5 100644 --- a/auth.php +++ b/auth.php @@ -16,27 +16,38 @@ function authenticate($username, $password) { foreach ($lines as $line) { list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3); if ($username === $storedUser && password_verify($password, $storedPass)) { - return $storedRole; // + return $storedRole; // Return the user's role } } - return false; } // Get JSON input $data = json_decode(file_get_contents("php://input"), true); -$username = $data["username"] ?? ""; -$password = $data["password"] ?? ""; +$username = trim($data["username"] ?? ""); +$password = trim($data["password"] ?? ""); + +// Validate input: ensure both fields are provided. +if (!$username || !$password) { + echo json_encode(["error" => "Username and password are required"]); + exit; +} + +// Validate username format: allow only letters, numbers, underscores, dashes, and spaces. +if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) { + echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]); + exit; +} // Authenticate user $userRole = authenticate($username, $password); if ($userRole !== false) { $_SESSION["authenticated"] = true; $_SESSION["username"] = $username; - $_SESSION["isAdmin"] = ($userRole === "1"); // correctly recognize admin status + $_SESSION["isAdmin"] = ($userRole === "1"); // "1" indicates admin echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]); } else { echo json_encode(["error" => "Invalid credentials"]); } -?> +?> \ No newline at end of file diff --git a/checkAuth.php b/checkAuth.php index a73da6c..3c1e105 100644 --- a/checkAuth.php +++ b/checkAuth.php @@ -19,4 +19,4 @@ echo json_encode([ "authenticated" => true, "isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false ]); -?> +?> \ No newline at end of file diff --git a/copyFiles.php b/copyFiles.php index cdf60ec..ee06428 100644 --- a/copyFiles.php +++ b/copyFiles.php @@ -10,7 +10,12 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { } $data = json_decode(file_get_contents("php://input"), true); -if (!$data || !isset($data['source']) || !isset($data['destination']) || !isset($data['files'])) { +if ( + !$data || + !isset($data['source']) || + !isset($data['destination']) || + !isset($data['files']) +) { echo json_encode(["error" => "Invalid request"]); exit; } @@ -19,9 +24,29 @@ $sourceFolder = trim($data['source']); $destinationFolder = trim($data['destination']); $files = $data['files']; +// Validate folder names: allow letters, numbers, underscores, dashes, spaces, and forward slashes. +$folderPattern = '/^[A-Za-z0-9_\- \/]+$/'; +if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) { + echo json_encode(["error" => "Invalid source folder name."]); + exit; +} +if ($destinationFolder !== 'root' && !preg_match($folderPattern, $destinationFolder)) { + echo json_encode(["error" => "Invalid destination folder name."]); + exit; +} + +// Trim any leading/trailing slashes and spaces. +$sourceFolder = trim($sourceFolder, "/\\ "); +$destinationFolder = trim($destinationFolder, "/\\ "); + // Build the source and destination directories. -$sourceDir = ($sourceFolder === 'root') ? UPLOAD_DIR : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR; -$destDir = ($destinationFolder === 'root') ? UPLOAD_DIR : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR; +$baseDir = rtrim(UPLOAD_DIR, '/\\'); +$sourceDir = ($sourceFolder === 'root') + ? $baseDir . DIRECTORY_SEPARATOR + : $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR; +$destDir = ($destinationFolder === 'root') + ? $baseDir . DIRECTORY_SEPARATOR + : $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR; // Load metadata. $metadataFile = META_DIR . META_FILE; @@ -36,10 +61,21 @@ if (!is_dir($destDir)) { } $errors = []; + +// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, and spaces. +$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/'; + foreach ($files as $fileName) { - $basename = basename($fileName); + $basename = basename(trim($fileName)); + // Validate the file name. + if (!preg_match($safeFileNamePattern, $basename)) { + $errors[] = "$basename has an invalid name."; + continue; + } + $srcPath = $sourceDir . $basename; $destPath = $destDir . $basename; + // Build metadata keys. $srcKey = ($sourceFolder === 'root') ? $basename : $sourceFolder . "/" . $basename; $destKey = ($destinationFolder === 'root') ? $basename : $destinationFolder . "/" . $basename; @@ -67,4 +103,4 @@ if (empty($errors)) { } else { echo json_encode(["error" => implode("; ", $errors)]); } -?> +?> \ No newline at end of file diff --git a/createFolder.php b/createFolder.php index b8327ef..e9251b3 100644 --- a/createFolder.php +++ b/createFolder.php @@ -17,32 +17,44 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { // Get the JSON input and decode it $input = json_decode(file_get_contents('php://input'), true); -if (!isset($input['folder'])) { +if (!isset($input['folderName'])) { echo json_encode(['success' => false, 'error' => 'Folder name not provided.']); exit; } -$folderName = trim($input['folder']); +$folderName = trim($input['folderName']); +$parent = isset($input['parent']) ? trim($input['parent']) : ""; -// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces +// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces in folderName if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $folderName)) { echo json_encode(['success' => false, 'error' => 'Invalid folder name.']); exit; } -// Build the folder path (assuming UPLOAD_DIR is defined in config.php) -$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folderName; +// Optionally, sanitize the parent folder if needed. +if ($parent && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $parent)) { + echo json_encode(['success' => false, 'error' => 'Invalid parent folder name.']); + exit; +} -// Check if the folder already exists -if (file_exists($folderPath)) { +// Build the full folder path. +$baseDir = rtrim(UPLOAD_DIR, '/\\'); +if ($parent && strtolower($parent) !== "root") { + $fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName; +} else { + $fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName; +} + +// Check if the folder already exists. +if (file_exists($fullPath)) { echo json_encode(['success' => false, 'error' => 'Folder already exists.']); exit; } -// Attempt to create the folder -if (mkdir($folderPath, 0755, true)) { +// Attempt to create the folder. +if (mkdir($fullPath, 0755, true)) { echo json_encode(['success' => true]); } else { echo json_encode(['success' => false, 'error' => 'Failed to create folder.']); } -?> +?> \ No newline at end of file diff --git a/deleteFiles.php b/deleteFiles.php index d36c180..e0b5398 100644 --- a/deleteFiles.php +++ b/deleteFiles.php @@ -20,6 +20,16 @@ if (!isset($data['files']) || !is_array($data['files'])) { // Determine folder – default to 'root' $folder = isset($data['folder']) ? trim($data['folder']) : 'root'; + +// Validate folder: allow letters, numbers, underscores, dashes, spaces, and forward slashes +if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) { + echo json_encode(["error" => "Invalid folder name."]); + exit; +} +// Trim any leading/trailing slashes and spaces. +$folder = trim($folder, "/\\ "); + +// Build the upload directory. if ($folder !== 'root') { $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; } else { @@ -29,8 +39,19 @@ if ($folder !== 'root') { $deletedFiles = []; $errors = []; +// Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces. +$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/'; + foreach ($data['files'] as $fileName) { - $filePath = $uploadDir . basename($fileName); + $basename = basename(trim($fileName)); + + // Validate the file name. + if (!preg_match($safeFileNamePattern, $basename)) { + $errors[] = "$basename has an invalid name."; + continue; + } + + $filePath = $uploadDir . $basename; if (file_exists($filePath)) { if (unlink($filePath)) { @@ -39,7 +60,7 @@ foreach ($data['files'] as $fileName) { $errors[] = "Failed to delete $fileName"; } } else { - // If file not found, consider it already deleted. + // Consider file already deleted. $deletedFiles[] = $fileName; } } @@ -49,4 +70,4 @@ if (empty($errors)) { } else { echo json_encode(["error" => implode("; ", $errors) . ". Files deleted: " . implode(", ", $deletedFiles)]); } -?> +?> \ No newline at end of file diff --git a/deleteFolder.php b/deleteFolder.php index 6e9bcd5..dedcd03 100644 --- a/deleteFolder.php +++ b/deleteFolder.php @@ -24,12 +24,19 @@ if (!isset($input['folder'])) { $folderName = trim($input['folder']); -// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces -if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $folderName)) { +// Prevent deletion of root. +if ($folderName === 'root') { + echo json_encode(['success' => false, 'error' => 'Cannot delete root folder.']); + exit; +} + +// Allow letters, numbers, underscores, dashes, spaces, and forward slashes. +if (!preg_match('/^[A-Za-z0-9_\- \/]+$/', $folderName)) { echo json_encode(['success' => false, 'error' => 'Invalid folder name.']); exit; } +// Build the folder path (supports subfolder paths like "FolderTest/FolderTestSub") $folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folderName; // Check if the folder exists and is a directory @@ -50,4 +57,4 @@ if (rmdir($folderPath)) { } else { echo json_encode(['success' => false, 'error' => 'Failed to delete folder.']); } -?> +?> \ No newline at end of file diff --git a/domUtils.js b/domUtils.js index 2d4cd04..dcaddc7 100644 --- a/domUtils.js +++ b/domUtils.js @@ -1,70 +1,83 @@ // domUtils.js export function toggleVisibility(elementId, shouldShow) { - const element = document.getElementById(elementId); - if (element) { - element.style.display = shouldShow ? "block" : "none"; - } else { - console.error(`Element with id "${elementId}" not found.`); - } + const element = document.getElementById(elementId); + if (element) { + element.style.display = shouldShow ? "block" : "none"; + } else { + console.error(`Element with id "${elementId}" not found.`); } +} + +export function escapeHTML(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// Toggle all checkboxes (assumes checkboxes have class 'file-checkbox') +export function toggleAllCheckboxes(masterCheckbox) { + const checkboxes = document.querySelectorAll(".file-checkbox"); + checkboxes.forEach(chk => { + chk.checked = masterCheckbox.checked; + }); + updateFileActionButtons(); // call the updated function +} + +// This updateFileActionButtons function checks for checkboxes inside the file list container. +export function updateFileActionButtons() { + const fileListContainer = document.getElementById("fileList"); + const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox"); + const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); + const copyBtn = document.getElementById("copySelectedBtn"); + const moveBtn = document.getElementById("moveSelectedBtn"); + const deleteBtn = document.getElementById("deleteSelectedBtn"); + const folderDropdown = document.getElementById("copyMoveFolderSelect"); - export function escapeHTML(str) { - return String(str) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - // Toggle all checkboxes (assumes checkboxes have class 'file-checkbox') - export function toggleAllCheckboxes(masterCheckbox) { - const checkboxes = document.querySelectorAll(".file-checkbox"); - checkboxes.forEach(chk => { - chk.checked = masterCheckbox.checked; - }); - updateFileActionButtons(); // call the updated function - } - - // This updateFileActionButtons function checks for checkboxes inside the file list container. - export function updateFileActionButtons() { - const fileListContainer = document.getElementById("fileList"); - const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox"); - const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); - const copyBtn = document.getElementById("copySelectedBtn"); - const moveBtn = document.getElementById("moveSelectedBtn"); - const deleteBtn = document.getElementById("deleteSelectedBtn"); - const folderDropdown = document.getElementById("copyMoveFolderSelect"); + // Hide the buttons and dropdown if no files exist. + if (fileCheckboxes.length === 0) { + copyBtn.style.display = "none"; + moveBtn.style.display = "none"; + deleteBtn.style.display = "none"; + folderDropdown.style.display = "none"; + } else { + // Otherwise, show the buttons and dropdown. + copyBtn.style.display = "inline-block"; + moveBtn.style.display = "inline-block"; + deleteBtn.style.display = "inline-block"; + folderDropdown.style.display = "none"; - // Hide the buttons and dropdown if no files exist. - if (fileCheckboxes.length === 0) { - copyBtn.style.display = "none"; - moveBtn.style.display = "none"; - deleteBtn.style.display = "none"; - folderDropdown.style.display = "none"; + // Enable the buttons if at least one file is selected; otherwise disable. + if (selectedCheckboxes.length > 0) { + copyBtn.disabled = false; + moveBtn.disabled = false; + deleteBtn.disabled = false; } else { - // Otherwise, show the buttons and dropdown. - copyBtn.style.display = "inline-block"; - moveBtn.style.display = "inline-block"; - deleteBtn.style.display = "inline-block"; - folderDropdown.style.display = "inline-block"; - - // Enable the buttons if at least one file is selected; otherwise disable. - if (selectedCheckboxes.length > 0) { - copyBtn.disabled = false; - moveBtn.disabled = false; - deleteBtn.disabled = false; - } else { - copyBtn.disabled = true; - moveBtn.disabled = true; - deleteBtn.disabled = true; - } + copyBtn.disabled = true; + moveBtn.disabled = true; + deleteBtn.disabled = true; } } - - - - - - \ No newline at end of file +} + +export function showToast(message, duration = 3000) { + const toast = document.getElementById("customToast"); + if (!toast) { + console.error("Toast element not found"); + return; + } + toast.textContent = message; + toast.style.display = "block"; + // Force reflow so the transition works. + void toast.offsetWidth; + toast.classList.add("show"); + setTimeout(() => { + toast.classList.remove("show"); + setTimeout(() => { + toast.style.display = "none"; + }, 500); // Wait for the opacity transition to finish. + }, duration); +} diff --git a/fileManager.js b/fileManager.js index dbf5fd4..0efcee3 100644 --- a/fileManager.js +++ b/fileManager.js @@ -1,5 +1,6 @@ // fileManager.js -import { escapeHTML, updateFileActionButtons } from './domUtils.js'; +import { escapeHTML, updateFileActionButtons, showToast } from './domUtils.js'; +import { formatFolderName } from './folderManager.js'; export let fileData = []; export let sortOrder = { column: "uploaded", ascending: true }; @@ -337,114 +338,218 @@ export function handleDeleteSelected(e) { e.stopImmediatePropagation(); const checkboxes = document.querySelectorAll(".file-checkbox:checked"); if (checkboxes.length === 0) { - alert("No files selected."); + showToast("No files selected."); return; } - if (!confirm("Are you sure you want to delete the selected files?")) { - return; - } - const filesToDelete = Array.from(checkboxes).map(chk => chk.value); - fetch("deleteFiles.php", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ folder: window.currentFolder, files: filesToDelete }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - alert("Selected files deleted successfully!"); - loadFileList(window.currentFolder); - } else { - alert("Error: " + (data.error || "Could not delete files")); - } - }) - .catch(error => console.error("Error deleting files:", error)); + // Save selected file names in a global variable for use in the modal. + window.filesToDelete = Array.from(checkboxes).map(chk => chk.value); + // Update modal message (optional) + document.getElementById("deleteFilesMessage").textContent = + "Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?"; + // Show the delete modal. + document.getElementById("deleteFilesModal").style.display = "block"; } +// Attach event listeners for delete modal buttons (wrap in DOMContentLoaded): +document.addEventListener("DOMContentLoaded", function () { + const cancelDelete = document.getElementById("cancelDeleteFiles"); + if (cancelDelete) { + cancelDelete.addEventListener("click", function () { + document.getElementById("deleteFilesModal").style.display = "none"; + window.filesToDelete = []; + }); + } + const confirmDelete = document.getElementById("confirmDeleteFiles"); + if (confirmDelete) { + confirmDelete.addEventListener("click", function () { + // Proceed with deletion + fetch("deleteFiles.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ folder: window.currentFolder, files: window.filesToDelete }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("Selected files deleted successfully!"); + loadFileList(window.currentFolder); + } else { + showToast("Error: " + (data.error || "Could not delete files")); + } + }) + .catch(error => console.error("Error deleting files:", error)) + .finally(() => { + document.getElementById("deleteFilesModal").style.display = "none"; + window.filesToDelete = []; + }); + }); + } +}); + // Copy selected files. export function handleCopySelected(e) { e.preventDefault(); e.stopImmediatePropagation(); const checkboxes = document.querySelectorAll(".file-checkbox:checked"); if (checkboxes.length === 0) { - alert("No files selected for copying."); + showToast("No files selected for copying.", 5000); return; } - const targetFolder = document.getElementById("copyMoveFolderSelect").value; - if (!targetFolder) { - alert("Please select a target folder for copying."); - return; - } - const filesToCopy = Array.from(checkboxes).map(chk => chk.value); - fetch("copyFiles.php", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ source: window.currentFolder, files: filesToCopy, destination: targetFolder }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - alert("Selected files copied successfully!"); - loadFileList(window.currentFolder); - } else { - alert("Error: " + (data.error || "Could not copy files")); - } - }) - .catch(error => console.error("Error copying files:", error)); + window.filesToCopy = Array.from(checkboxes).map(chk => chk.value); + // Open the Copy modal. + document.getElementById("copyFilesModal").style.display = "block"; + // Populate target folder dropdown. + loadCopyMoveFolderListForModal("copyTargetFolder"); } +// In your loadCopyMoveFolderListForModal function, target the dropdown by its ID. +export async function loadCopyMoveFolderListForModal(dropdownId) { + try { + const response = await fetch('getFolderList.php'); + const folders = await response.json(); + console.log('Folders fetched for modal:', folders); + + const folderSelect = document.getElementById(dropdownId); + folderSelect.innerHTML = ''; + + const rootOption = document.createElement('option'); + rootOption.value = 'root'; + rootOption.textContent = '(Root)'; + folderSelect.appendChild(rootOption); + + if (Array.isArray(folders) && folders.length > 0) { + folders.forEach(folder => { + const option = document.createElement('option'); + option.value = folder; + option.textContent = formatFolderName(folder); + folderSelect.appendChild(option); + }); + } + } catch (error) { + console.error('Error loading folder list for modal:', error); + } +} + +// Attach event listeners for copy modal buttons. +document.addEventListener("DOMContentLoaded", function () { + const cancelCopy = document.getElementById("cancelCopyFiles"); + if (cancelCopy) { + cancelCopy.addEventListener("click", function () { + document.getElementById("copyFilesModal").style.display = "none"; + window.filesToCopy = []; + }); + } + const confirmCopy = document.getElementById("confirmCopyFiles"); + if (confirmCopy) { + confirmCopy.addEventListener("click", function () { + const targetFolder = document.getElementById("copyTargetFolder").value; + if (!targetFolder) { + showToast("Please select a target folder for copying.!", 5000); + return; + } + if (targetFolder === window.currentFolder) { + showToast("Error: Cannot move files to the same folder."); + return; + } + fetch("copyFiles.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ source: window.currentFolder, files: window.filesToCopy, destination: targetFolder }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("Selected files copied successfully!", 5000); + loadFileList(window.currentFolder); + } else { + showToast("Error: " + (data.error || "Could not copy files"), 5000); + } + }) + .catch(error => console.error("Error copying files:", error)) + .finally(() => { + document.getElementById("copyFilesModal").style.display = "none"; + window.filesToCopy = []; + }); + }); + } +}); + // Move selected files. export function handleMoveSelected(e) { e.preventDefault(); e.stopImmediatePropagation(); const checkboxes = document.querySelectorAll(".file-checkbox:checked"); if (checkboxes.length === 0) { - alert("No files selected for moving."); + showToast("No files selected for moving."); return; } - const targetFolder = document.getElementById("copyMoveFolderSelect").value; - if (!targetFolder) { - alert("Please select a target folder for moving."); - return; - } - if (targetFolder === window.currentFolder) { - alert("Error: Cannot move files to the same folder."); - return; - } - const filesToMove = Array.from(checkboxes).map(chk => chk.value); - fetch("moveFiles.php", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ source: window.currentFolder, files: filesToMove, destination: targetFolder }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - alert("Selected files moved successfully!"); - loadFileList(window.currentFolder); - } else { - alert("Error: " + (data.error || "Could not move files")); - } - }) - .catch(error => console.error("Error moving files:", error)); + window.filesToMove = Array.from(checkboxes).map(chk => chk.value); + // Open the Move modal. + document.getElementById("moveFilesModal").style.display = "block"; + // Populate target folder dropdown. + loadCopyMoveFolderListForModal("moveTargetFolder"); } +document.addEventListener("DOMContentLoaded", function () { + const cancelMove = document.getElementById("cancelMoveFiles"); + if (cancelMove) { + cancelMove.addEventListener("click", function () { + document.getElementById("moveFilesModal").style.display = "none"; + window.filesToMove = []; + }); + } + const confirmMove = document.getElementById("confirmMoveFiles"); + if (confirmMove) { + confirmMove.addEventListener("click", function () { + const targetFolder = document.getElementById("moveTargetFolder").value; + if (!targetFolder) { + showToast("Please select a target folder for moving."); + return; + } + if (targetFolder === window.currentFolder) { + showToast("Error: Cannot move files to the same folder."); + return; + } + fetch("moveFiles.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ source: window.currentFolder, files: window.filesToMove, destination: targetFolder }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("Selected files moved successfully!"); + loadFileList(window.currentFolder); + } else { + showToast("Error: " + (data.error || "Could not move files")); + } + }) + .catch(error => console.error("Error moving files:", error)) + .finally(() => { + document.getElementById("moveFilesModal").style.display = "none"; + window.filesToMove = []; + }); + }); + } +}); + // File Editing Functions. export function editFile(fileName, folder) { console.log("Edit button clicked for:", fileName); let existingEditor = document.getElementById("editorContainer"); if (existingEditor) { existingEditor.remove(); } const folderUsed = folder || window.currentFolder || "root"; + // For subfolders, encode each segment separately to preserve slashes. const folderPath = (folderUsed === "root") ? "uploads/" - : "uploads/" + encodeURIComponent(folderUsed) + "/"; + : "uploads/" + folderUsed.split("/").map(encodeURIComponent).join("/") + "/"; const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime(); fetch(fileUrl, { method: "HEAD" }) .then(response => { const contentLength = response.headers.get("Content-Length"); if (contentLength && parseInt(contentLength) > 10485760) { - alert("This file is larger than 10 MB and cannot be edited in the browser."); + showToast("This file is larger than 10 MB and cannot be edited in the browser."); throw new Error("File too large."); } return fetch(fileUrl); @@ -492,7 +597,7 @@ export function saveFile(fileName, folder) { }) .then(response => response.json()) .then(result => { - alert(result.success || result.error); + showToast(result.success || result.error); document.getElementById("editorContainer")?.remove(); loadFileList(folderUsed); }) @@ -546,32 +651,67 @@ export function initFileActions() { // Rename function: always available. +// Expose renameFile to global scope. export function renameFile(oldName, folder) { - const newName = prompt(`Enter new name for file "${oldName}":`, oldName); - if (!newName || newName === oldName) { - return; // No change. - } - const folderUsed = folder || window.currentFolder || "root"; - fetch("renameFile.php", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ folder: folderUsed, oldName: oldName, newName: newName }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - alert("File renamed successfully!"); - loadFileList(folderUsed); - } else { - alert("Error renaming file: " + (data.error || "Unknown error")); - } - }) - .catch(error => { - console.error("Error renaming file:", error); - alert("Error renaming file"); - }); + // Store the file name and folder globally for use in the modal. + window.fileToRename = oldName; + window.fileFolder = folder || window.currentFolder || "root"; + + // Pre-fill the input with the current file name. + document.getElementById("newFileName").value = oldName; + + // Show the rename file modal. + document.getElementById("renameFileModal").style.display = "block"; } +// Attach event listeners after DOM content is loaded. +document.addEventListener("DOMContentLoaded", () => { + // Cancel button: hide modal and clear input. + const cancelBtn = document.getElementById("cancelRenameFile"); + if (cancelBtn) { + cancelBtn.addEventListener("click", function() { + document.getElementById("renameFileModal").style.display = "none"; + document.getElementById("newFileName").value = ""; + }); + } + + // Submit button: send rename request. + const submitBtn = document.getElementById("submitRenameFile"); + if (submitBtn) { + submitBtn.addEventListener("click", function() { + const newName = document.getElementById("newFileName").value.trim(); + if (!newName || newName === window.fileToRename) { + // No change; just hide the modal. + document.getElementById("renameFileModal").style.display = "none"; + return; + } + const folderUsed = window.fileFolder; + fetch("renameFile.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ folder: folderUsed, oldName: window.fileToRename, newName: newName }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("File renamed successfully!"); + loadFileList(folderUsed); + } else { + showToast("Error renaming file: " + (data.error || "Unknown error")); + } + }) + .catch(error => { + console.error("Error renaming file:", error); + showToast("Error renaming file"); + }) + .finally(() => { + document.getElementById("renameFileModal").style.display = "none"; + document.getElementById("newFileName").value = ""; + }); + }); + } +}); + // Expose renameFile to global scope. window.renameFile = renameFile; diff --git a/folderManager.js b/folderManager.js index 9542b72..fb54d40 100644 --- a/folderManager.js +++ b/folderManager.js @@ -1,215 +1,381 @@ -// folderManager.js -import { - loadFileList - } from './fileManager.js'; - - - export function renameFolder() { - const folderSelect = document.getElementById("folderSelect"); - const selectedFolder = folderSelect.value; - const newFolderName = prompt("Enter the new folder name:", selectedFolder); - if (newFolderName && newFolderName !== selectedFolder) { - fetch("renameFolder.php", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ oldName: selectedFolder, newName: newFolderName }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - alert("Folder renamed successfully!"); - loadFolderList("root"); - } else { - alert("Error: " + (data.error || "Could not rename folder")); - } - }) - .catch(error => console.error("Error renaming folder:", error)); - } - } - - export function deleteFolder() { - const folderSelect = document.getElementById("folderSelect"); - const selectedFolder = folderSelect.value; - if (!selectedFolder || selectedFolder === "root") { - alert("Please select a valid folder to delete."); - return; - } - - // Only prompt once. - if (!confirm("Are you sure you want to delete folder " + selectedFolder + "?")) { - return; - } - - // Proceed with deletion. - fetch("deleteFolder.php", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ folder: selectedFolder }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - alert("Folder deleted successfully!"); - // Refresh both folder dropdowns. - loadFolderList("root"); - loadCopyMoveFolderList(); - } else { - alert("Error: " + (data.error || "Could not delete folder")); - } - }) - .catch(error => console.error("Error deleting folder:", error)); - } - - - // Updates the copy/move folder dropdown. - export async function loadCopyMoveFolderList() { - try { - const response = await fetch('getFolderList.php'); - const folders = await response.json(); - const folderSelect = document.getElementById('copyMoveFolderSelect'); - folderSelect.innerHTML = ''; // Clear existing options - - // Always add a "Root" option as the default. - const rootOption = document.createElement('option'); - rootOption.value = 'root'; - rootOption.textContent = '(Root)'; - folderSelect.appendChild(rootOption); - - if (Array.isArray(folders) && folders.length > 0) { - folders.forEach(folder => { - const option = document.createElement('option'); - option.value = folder; - option.textContent = folder; - folderSelect.appendChild(option); - }); - } - } catch (error) { - console.error('Error loading folder list:', error); +import { loadFileList } from './fileManager.js'; +import { showToast } from './domUtils.js'; +// ---------------------- +// Helper functions +// ---------------------- + +// Format folder name for display (for copy/move dropdown). +export function formatFolderName(folder) { + if (folder.indexOf("/") !== -1) { + let parts = folder.split("/"); + let indent = ""; + for (let i = 1; i < parts.length; i++) { + indent += "\u00A0\u00A0\u00A0\u00A0"; // 4 non-breaking spaces per level } + return indent + parts[parts.length - 1]; + } else { + return folder; } - - - // Optional helper to load folder lists (alias for loadCopyMoveFolderList). - - export function loadFolderList(selectedFolder) { - const folderSelect = document.getElementById("folderSelect"); - folderSelect.innerHTML = ""; - const rootOption = document.createElement("option"); - rootOption.value = "root"; - rootOption.textContent = "(Root)"; - folderSelect.appendChild(rootOption); - - fetch("getFolderList.php") - .then(response => response.json()) - .then(folders => { - folders.forEach(function (folder) { - let option = document.createElement("option"); - option.value = folder; - option.textContent = folder; - folderSelect.appendChild(option); - }); - - // Set the selected folder if provided, else default to "root" - if (selectedFolder && [...folderSelect.options].some(opt => opt.value === selectedFolder)) { - folderSelect.value = selectedFolder; - } else { - folderSelect.value = "root"; - } - - // Update global currentFolder and title, then load the file list - window.currentFolder = folderSelect.value; - document.getElementById("fileListTitle").textContent = - window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")"; - loadFileList(window.currentFolder); - }) - .catch(error => console.error("Error loading folder list:", error)); +} + +// Build a tree structure from a flat array of folder paths. +function buildFolderTree(folders) { + const tree = {}; + folders.forEach(folderPath => { + const parts = folderPath.split('/'); + let current = tree; + parts.forEach(part => { + if (!current[part]) { + current[part] = {}; + } + current = current[part]; + }); + }); + return tree; +} + +/** + * Render the folder tree as nested