diff --git a/README.md b/README.md index 43f08a2..6cf5ccd 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,26 @@ Multi File Upload Editor is a lightweight, secure web application for uploading, - Direct access to sensitive files (e.g., `users.txt`) is restricted through .htaccess rules. - A proxy download mechanism has been implemented (via endpoints like `download.php` and `downloadZip.php`) so that every file download request goes through a PHP script. This script validates the session and CSRF token before streaming the file, ensuring that even if a file URL is guessed, only authenticated users can access it. - Administrators are advised to deploy the app on a secure internal network or use the proxy download mechanism for public deployments to further protect file content. +- **Trash Management with Restore & Delete:** + - **Trash Storage & Metadata:** + - Deleted files are moved to a designated “Trash” folder rather than being immediately removed. + - Metadata is stored in a JSON file (`trash.json`) that records: + - Original folder and file name + - Timestamp when the file was trashed + - Uploader information (and optionally who deleted it) + - Additional metadata (e.g., file type) + - **Restore Functionality:** + - Admins can view trashed files in a modal. + - They can restore individual files (with conflict checks) or restore all files back to their original location. + - **Delete Functionality:** + - Users can permanently delete trashed files via: + - **Delete Selected:** Remove specific files from the Trash and update `trash.json`. + - **Delete All:** Permanently remove every file from the Trash after confirmation. + - **Auto-Purge Mechanism:** + - The system automatically purges (permanently deletes) any files in the Trash older than three days, helping manage storage and prevent the accumulation of outdated files. + - **User Interface:** + - The trash modal displays details such as file name, uploader/deleter, and the trashed date/time. + - Material icons with tooltips visually represent the restore and delete actions. --- diff --git a/addUser.php b/addUser.php index b11a3fe..6d3cd5d 100644 --- a/addUser.php +++ b/addUser.php @@ -3,13 +3,6 @@ require 'config.php'; header('Content-Type: application/json'); $usersFile = USERS_DIR . USERS_FILE; -$headers = array_change_key_case(getallheaders(), CASE_LOWER); -$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; -if ($receivedToken !== $_SESSION['csrf_token']) { - echo json_encode(["error" => "Invalid CSRF token"]); - http_response_code(403); - exit; -} // Determine if we are in setup mode: // - Query parameter setup=1 is passed @@ -20,7 +13,14 @@ if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile)) $setupMode = true; } else { $setupMode = false; - // Only allow admins to add users normally. + // In non-setup mode, check CSRF token and require admin privileges. + $headers = array_change_key_case(getallheaders(), CASE_LOWER); + $receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; + } if ( !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true || !isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true @@ -83,4 +83,4 @@ if ($setupMode) { } echo json_encode(["success" => "User added successfully"]); -?> +?> \ No newline at end of file diff --git a/auth.js b/auth.js index 142d177..2ed33f1 100644 --- a/auth.js +++ b/auth.js @@ -1,35 +1,107 @@ import { sendRequest } from './networkUtils.js'; import { toggleVisibility, showToast } from './domUtils.js'; -// Import loadFileList and renderFileTable from fileManager.js to refresh the file list upon login. import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js'; import { loadFolderTree } from './folderManager.js'; function initAuth() { // First, check if the user is already authenticated. - checkAuthentication(); + checkAuthentication(false).then(data => { + if (data.setup) { + window.setupMode = true; + showToast("Setup mode: No users found. Please add an admin user."); + toggleVisibility("loginForm", false); + toggleVisibility("mainOperations", false); + document.querySelector(".header-buttons").style.visibility = "hidden"; + toggleVisibility("addUserModal", true); + return; + } + window.setupMode = false; + if (data.authenticated) { + // User is logged in—show the main UI. + toggleVisibility("loginForm", false); + toggleVisibility("mainOperations", true); + toggleVisibility("uploadFileForm", true); + toggleVisibility("fileListContainer", true); + document.querySelector(".header-buttons").style.visibility = "visible"; + // If admin, show admin-only buttons. + if (data.isAdmin) { + const addUserBtn = document.getElementById("addUserBtn"); + const removeUserBtn = document.getElementById("removeUserBtn"); + if (addUserBtn) addUserBtn.style.display = "block"; + if (removeUserBtn) removeUserBtn.style.display = "block"; + // Create and show the restore button. + let restoreBtn = document.getElementById("restoreFilesBtn"); + if (!restoreBtn) { + restoreBtn = document.createElement("button"); + restoreBtn.id = "restoreFilesBtn"; + restoreBtn.classList.add("btn", "btn-warning"); + // Use a material icon. + restoreBtn.innerHTML = 'restore_from_trash'; - // Attach event listener for login. - document.getElementById("authForm").addEventListener("submit", function (event) { - event.preventDefault(); - const formData = { - username: document.getElementById("loginUsername").value.trim(), - password: document.getElementById("loginPassword").value.trim() - }; - // Include CSRF token header with login - sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken }) - .then(data => { - if (data.success) { - console.log("✅ Login successful. Reloading page."); - sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!"); - window.location.reload(); - } else { - showToast("Login failed: " + (data.error || "Unknown error")); + const headerButtons = document.querySelector(".header-buttons"); + if (headerButtons) { + // Insert after the third child if available. + if (headerButtons.children.length >= 4) { + headerButtons.insertBefore(restoreBtn, headerButtons.children[4]); + } else { + headerButtons.appendChild(restoreBtn); + } + } } - }) - .catch(error => console.error("❌ Error logging in:", error)); + restoreBtn.style.display = "block"; + } else { + const addUserBtn = document.getElementById("addUserBtn"); + const removeUserBtn = document.getElementById("removeUserBtn"); + if (addUserBtn) addUserBtn.style.display = "none"; + if (removeUserBtn) removeUserBtn.style.display = "none"; + // If not admin, hide the restore button. + const restoreBtn = document.getElementById("restoreFilesBtn"); + if (restoreBtn) { + restoreBtn.style.display = "none"; + } + } + // Set items-per-page. + const selectElem = document.querySelector(".form-control.bottom-select"); + if (selectElem) { + const stored = localStorage.getItem("itemsPerPage") || "10"; + selectElem.value = stored; + } + } else { + // Do not show a toast message repeatedly during initial check. + toggleVisibility("loginForm", true); + toggleVisibility("mainOperations", false); + toggleVisibility("uploadFileForm", false); + toggleVisibility("fileListContainer", false); + document.querySelector(".header-buttons").style.visibility = "hidden"; + } + }).catch(error => { + console.error("Error checking authentication:", error); }); - // Set up the logout button. + // Attach login event listener once. + const authForm = document.getElementById("authForm"); + if (authForm) { + authForm.addEventListener("submit", function (event) { + event.preventDefault(); + const formData = { + username: document.getElementById("loginUsername").value.trim(), + password: document.getElementById("loginPassword").value.trim() + }; + sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken }) + .then(data => { + if (data.success) { + console.log("✅ Login successful. Reloading page."); + sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!"); + window.location.reload(); + } else { + showToast("Login failed: " + (data.error || "Unknown error")); + } + }) + .catch(error => console.error("❌ Error logging in:", error)); + }); + } + + // Attach logout event listener. document.getElementById("logoutBtn").addEventListener("click", function () { fetch("logout.php", { method: "POST", @@ -40,12 +112,11 @@ function initAuth() { .catch(error => console.error("Logout error:", error)); }); - // Set up Add User functionality. + // Add User functionality. document.getElementById("addUserBtn").addEventListener("click", function () { resetUserForm(); toggleVisibility("addUserModal", true); }); - document.getElementById("saveUserBtn").addEventListener("click", function () { const newUsername = document.getElementById("newUsername").value.trim(); const newPassword = document.getElementById("newPassword").value.trim(); @@ -72,24 +143,22 @@ function initAuth() { if (data.success) { showToast("User added successfully!"); closeAddUserModal(); - checkAuthentication(); + checkAuthentication(false); // Re-check without showing toast } else { showToast("Error: " + (data.error || "Could not add user")); } }) .catch(error => console.error("Error adding user:", error)); }); - document.getElementById("cancelUserBtn").addEventListener("click", function () { closeAddUserModal(); }); - // Set up Remove User functionality. + // Remove User functionality. document.getElementById("removeUserBtn").addEventListener("click", function () { loadUserList(); toggleVisibility("removeUserModal", true); }); - document.getElementById("deleteUserBtn").addEventListener("click", function () { const selectElem = document.getElementById("removeUsernameSelect"); const usernameToRemove = selectElem.value; @@ -121,52 +190,29 @@ function initAuth() { }) .catch(error => console.error("Error removing user:", error)); }); - document.getElementById("cancelRemoveUserBtn").addEventListener("click", function () { closeRemoveUserModal(); }); } -function checkAuthentication() { - // Return the promise from sendRequest +function checkAuthentication(showLoginToast = true) { + // Optionally pass a flag so we don't show a toast every time. return sendRequest("checkAuth.php") .then(data => { if (data.setup) { window.setupMode = true; - showToast("Setup mode: No users found. Please add an admin user."); + if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user."); toggleVisibility("loginForm", false); toggleVisibility("mainOperations", false); document.querySelector(".header-buttons").style.visibility = "hidden"; toggleVisibility("addUserModal", true); return false; - } else { - window.setupMode = false; } + window.setupMode = false; if (data.authenticated) { - toggleVisibility("loginForm", false); - toggleVisibility("mainOperations", true); - toggleVisibility("uploadFileForm", true); - toggleVisibility("fileListContainer", true); - if (data.isAdmin) { - const addUserBtn = document.getElementById("addUserBtn"); - const removeUserBtn = document.getElementById("removeUserBtn"); - if (addUserBtn) addUserBtn.style.display = "block"; - if (removeUserBtn) removeUserBtn.style.display = "block"; - } else { - const addUserBtn = document.getElementById("addUserBtn"); - const removeUserBtn = document.getElementById("removeUserBtn"); - if (addUserBtn) addUserBtn.style.display = "none"; - if (removeUserBtn) removeUserBtn.style.display = "none"; - } - document.querySelector(".header-buttons").style.visibility = "visible"; - const selectElem = document.querySelector(".form-control.bottom-select"); - if (selectElem) { - const stored = localStorage.getItem("itemsPerPage") || "10"; - selectElem.value = stored; - } - return true; + return data; } else { - showToast("Please log in to continue."); + if (showLoginToast) showToast("Please log in to continue."); toggleVisibility("loginForm", true); toggleVisibility("mainOperations", false); toggleVisibility("uploadFileForm", false); @@ -182,11 +228,7 @@ function checkAuthentication() { } window.checkAuthentication = checkAuthentication; -/* ------------------------------ - Persistent Items-Per-Page Setting - ------------------------------ */ window.changeItemsPerPage = function (value) { - console.log("Saving itemsPerPage:", value); localStorage.setItem("itemsPerPage", value); const folder = window.currentFolder || "root"; if (typeof renderFileTable === "function") { @@ -198,14 +240,10 @@ document.addEventListener("DOMContentLoaded", function () { const selectElem = document.querySelector(".form-control.bottom-select"); if (selectElem) { const stored = localStorage.getItem("itemsPerPage") || "10"; - console.log("Loaded itemsPerPage from localStorage:", stored); selectElem.value = stored; } }); -/* ------------------------------ - Helper functions for modals and user list - ------------------------------ */ function resetUserForm() { document.getElementById("newUsername").value = ""; document.getElementById("newPassword").value = ""; @@ -226,10 +264,6 @@ function loadUserList() { .then(response => response.json()) .then(data => { const users = Array.isArray(data) ? data : (data.users || []); - if (!users || !Array.isArray(users)) { - console.error("Invalid users data:", data); - return; - } const selectElem = document.getElementById("removeUsernameSelect"); selectElem.innerHTML = ""; users.forEach(user => { diff --git a/config.php b/config.php index 03447b1..1cc3889 100644 --- a/config.php +++ b/config.php @@ -50,6 +50,6 @@ define('USERS_DIR', '/var/www/users/'); define('USERS_FILE', 'users.txt'); define('META_DIR','/var/www/metadata/'); define('META_FILE','file_metadata.json'); - +define('TRASH_DIR', UPLOAD_DIR . 'trash/'); date_default_timezone_set(TIMEZONE); ?> \ No newline at end of file diff --git a/deleteFiles.php b/deleteFiles.php index 6921f93..a6f338f 100644 --- a/deleteFiles.php +++ b/deleteFiles.php @@ -19,8 +19,22 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { exit; } +// --- Setup Trash Folder & Metadata --- +$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; +if (!file_exists($trashDir)) { + mkdir($trashDir, 0755, true); +} +$trashMetadataFile = $trashDir . "trash.json"; +$trashData = []; +if (file_exists($trashMetadataFile)) { + $json = file_get_contents($trashMetadataFile); + $trashData = json_decode($json, true); + if (!is_array($trashData)) { + $trashData = []; + } +} + // Helper: Generate the metadata file path for a given folder. -// For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json". function getMetadataFilePath($folder) { if (strtolower($folder) === 'root' || $folder === '') { return META_DIR . "root_metadata.json"; @@ -45,7 +59,6 @@ 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. @@ -55,7 +68,17 @@ if ($folder !== 'root') { $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR; } -$deletedFiles = []; +// Load folder metadata (if exists) to retrieve uploader and upload date. +$metadataFile = getMetadataFilePath($folder); +$folderMetadata = []; +if (file_exists($metadataFile)) { + $folderMetadata = json_decode(file_get_contents($metadataFile), true); + if (!is_array($folderMetadata)) { + $folderMetadata = []; + } +} + +$movedFiles = []; $errors = []; // Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces. @@ -73,23 +96,41 @@ foreach ($data['files'] as $fileName) { $filePath = $uploadDir . $basename; if (file_exists($filePath)) { - if (unlink($filePath)) { - $deletedFiles[] = $basename; + // Append a timestamp to the file name in trash to avoid collisions. + $timestamp = time(); + $trashFileName = $basename . "_" . $timestamp; + if (rename($filePath, $trashDir . $trashFileName)) { + $movedFiles[] = $basename; + // Record trash metadata for possible restoration. + $trashData[] = [ + 'type' => 'file', + 'originalFolder' => $uploadDir, // You could also store a relative path here. + 'originalName' => $basename, + 'trashName' => $trashFileName, + 'trashedAt' => $timestamp, + // Enrich trash record with uploader and upload date from folder metadata (if available) + 'uploaded' => isset($folderMetadata[$basename]['uploaded']) ? $folderMetadata[$basename]['uploaded'] : "Unknown", + 'uploader' => isset($folderMetadata[$basename]['uploader']) ? $folderMetadata[$basename]['uploader'] : "Unknown", + // NEW: Record the username of the user who deleted the file. + 'deletedBy' => isset($_SESSION['username']) ? $_SESSION['username'] : "Unknown" + ]; } else { - $errors[] = "Failed to delete $basename"; + $errors[] = "Failed to move $basename to Trash."; } } else { // Consider file already deleted. - $deletedFiles[] = $basename; + $movedFiles[] = $basename; } } +// Write back the updated trash metadata. +file_put_contents($trashMetadataFile, json_encode($trashData, JSON_PRETTY_PRINT)); + // Update folder-specific metadata file by removing deleted files. -$metadataFile = getMetadataFilePath($folder); if (file_exists($metadataFile)) { $metadata = json_decode(file_get_contents($metadataFile), true); if (is_array($metadata)) { - foreach ($deletedFiles as $delFile) { + foreach ($movedFiles as $delFile) { if (isset($metadata[$delFile])) { unset($metadata[$delFile]); } @@ -99,8 +140,8 @@ if (file_exists($metadataFile)) { } if (empty($errors)) { - echo json_encode(["success" => "Files deleted: " . implode(", ", $deletedFiles)]); + echo json_encode(["success" => "Files moved to Trash: " . implode(", ", $movedFiles)]); } else { - echo json_encode(["error" => implode("; ", $errors) . ". Files deleted: " . implode(", ", $deletedFiles)]); + echo json_encode(["error" => implode("; ", $errors) . ". Files moved to Trash: " . implode(", ", $movedFiles)]); } ?> \ No newline at end of file diff --git a/deleteTrashFiles.php b/deleteTrashFiles.php new file mode 100644 index 0000000..800c0b6 --- /dev/null +++ b/deleteTrashFiles.php @@ -0,0 +1,105 @@ + "Invalid CSRF token"]); + http_response_code(403); + exit; +} + +// Ensure user is authenticated +if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + echo json_encode(["error" => "Unauthorized"]); + http_response_code(401); + exit; +} + +// --- Setup Trash Folder & Metadata --- +$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; +if (!file_exists($trashDir)) { + mkdir($trashDir, 0755, true); +} +$trashMetadataFile = $trashDir . "trash.json"; + +// Load trash metadata into an associative array keyed by trashName. +$trashData = []; +if (file_exists($trashMetadataFile)) { + $json = file_get_contents($trashMetadataFile); + $tempData = json_decode($json, true); + if (is_array($tempData)) { + foreach ($tempData as $item) { + if (isset($item['trashName'])) { + $trashData[$item['trashName']] = $item; + } + } + } +} + +// Read request body. +$data = json_decode(file_get_contents("php://input"), true); +if (!$data) { + echo json_encode(["error" => "Invalid input"]); + exit; +} + +// Determine deletion mode: if "deleteAll" is true, delete all trash items; otherwise, use provided "files" array. +$filesToDelete = []; +if (isset($data['deleteAll']) && $data['deleteAll'] === true) { + $filesToDelete = array_keys($trashData); +} elseif (isset($data['files']) && is_array($data['files'])) { + $filesToDelete = $data['files']; +} else { + echo json_encode(["error" => "No trash file identifiers provided"]); + exit; +} + +$deletedFiles = []; +$errors = []; + +// Define a safe file name pattern. +$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/'; + +foreach ($filesToDelete as $trashName) { + $trashName = trim($trashName); + if (!preg_match($safeFileNamePattern, $trashName)) { + $errors[] = "$trashName has an invalid format."; + continue; + } + + if (!isset($trashData[$trashName])) { + $errors[] = "Trash item $trashName not found."; + continue; + } + + $filePath = $trashDir . $trashName; + + if (file_exists($filePath)) { + if (unlink($filePath)) { + $deletedFiles[] = $trashName; + unset($trashData[$trashName]); + } else { + $errors[] = "Failed to delete $trashName."; + } + } else { + // If the file doesn't exist, remove its metadata entry. + unset($trashData[$trashName]); + $deletedFiles[] = $trashName; + } +} + +// Write the updated trash metadata back (as an indexed array). +file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT)); + +if (empty($errors)) { + echo json_encode(["success" => "Trash items deleted: " . implode(", ", $deletedFiles)]); +} else { + echo json_encode(["error" => implode("; ", $errors) . ". Trash items deleted: " . implode(", ", $deletedFiles)]); +} +exit; +?> \ No newline at end of file diff --git a/domUtils.js b/domUtils.js index 6bb65ea..1ee5cfe 100644 --- a/domUtils.js +++ b/domUtils.js @@ -164,7 +164,7 @@ export function buildFileTableRow(file, folderPath) { file_download ${file.editable ? ` - edit diff --git a/fileManager.js b/fileManager.js index 7cc671e..e08be2f 100644 --- a/fileManager.js +++ b/fileManager.js @@ -457,9 +457,15 @@ export function renderFileTable(folder) { window.currentSearchTerm = newSearchInput.value; window.currentPage = 1; renderFileTable(folder); + // After re‑render, re-select the input element and set focus. setTimeout(() => { - newSearchInput.focus(); - newSearchInput.setSelectionRange(newSearchInput.value.length, newSearchInput.value.length); + const freshInput = document.getElementById("searchInput"); + if (freshInput) { + freshInput.focus(); + // Place the caret at the end of the text. + const len = freshInput.value.length; + freshInput.setSelectionRange(len, len); + } }, 0); }, 300)); } @@ -528,7 +534,7 @@ export function renderGalleryView(folder) { file_download ${file.editable ? ` - + edit ` : ""} @@ -999,9 +1005,11 @@ function getModeForFile(fileName) { function adjustEditorSize() { const modal = document.querySelector(".editor-modal"); if (modal && window.currentEditor) { - const modalHeight = modal.getBoundingClientRect().height || 600; - const newEditorHeight = Math.max(modalHeight * 0.8, 5) + "px"; - window.currentEditor.setSize("100%", newEditorHeight); + // Calculate available height for the editor. + // If you have a header or footer inside the modal, subtract their heights. + const headerHeight = 60; + const availableHeight = modal.clientHeight - headerHeight; + window.currentEditor.setSize("100%", availableHeight + "px"); } } diff --git a/folderManager.js b/folderManager.js index 9deeba7..2982759 100644 --- a/folderManager.js +++ b/folderManager.js @@ -68,6 +68,11 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") { const state = loadFolderTreeState(); let html = ``; for (const folder in tree) { + // Skip the trash folder (case-insensitive) + if (folder.toLowerCase() === "trash") { + continue; + } + const fullPath = parentPath ? parentPath + "/" + folder : folder; const hasChildren = Object.keys(tree[folder]).length > 0; const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay; diff --git a/getTrashItems.php b/getTrashItems.php new file mode 100644 index 0000000..f3ef7f0 --- /dev/null +++ b/getTrashItems.php @@ -0,0 +1,68 @@ + "Unauthorized"]); + http_response_code(401); + exit; +} + +// Define the trash directory and trash metadata file. +$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; +$trashMetadataFile = $trashDir . "trash.json"; + +// Helper: Generate the metadata file path for a given folder. +// For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json". +function getMetadataFilePath($folder) { + if (strtolower($folder) === 'root' || $folder === '') { + return META_DIR . "root_metadata.json"; + } + return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; +} + +// Read the trash metadata. +$trashItems = []; +if (file_exists($trashMetadataFile)) { + $json = file_get_contents($trashMetadataFile); + $trashItems = json_decode($json, true); + if (!is_array($trashItems)) { + $trashItems = []; + } +} + +// Enrich each trash record. +foreach ($trashItems as &$item) { + // Ensure deletedBy is set and not empty. + if (empty($item['deletedBy'])) { + $item['deletedBy'] = "Unknown"; + } + // Enrich with uploader and uploaded date if not already present. + if (empty($item['uploaded']) || empty($item['uploader'])) { + if (isset($item['originalFolder']) && isset($item['originalName'])) { + $metadataFile = getMetadataFilePath($item['originalFolder']); + if (file_exists($metadataFile)) { + $metadata = json_decode(file_get_contents($metadataFile), true); + if (is_array($metadata) && isset($metadata[$item['originalName']])) { + $item['uploaded'] = !empty($metadata[$item['originalName']]['uploaded']) ? $metadata[$item['originalName']]['uploaded'] : "Unknown"; + $item['uploader'] = !empty($metadata[$item['originalName']]['uploader']) ? $metadata[$item['originalName']]['uploader'] : "Unknown"; + } else { + $item['uploaded'] = "Unknown"; + $item['uploader'] = "Unknown"; + } + } else { + $item['uploaded'] = "Unknown"; + $item['uploader'] = "Unknown"; + } + } else { + $item['uploaded'] = "Unknown"; + $item['uploader'] = "Unknown"; + } + } +} +unset($item); + +echo json_encode($trashItems); +exit; +?> \ No newline at end of file diff --git a/index.html b/index.html index 59e30cd..3b29e1b 100644 --- a/index.html +++ b/index.html @@ -9,8 +9,6 @@ - - @@ -22,7 +20,7 @@ - + @@ -93,6 +91,28 @@ exit_to_app + + + + + restore_from_trash + Restore or + delete_for_ever + Delete Trash Items + + + + + + Restore Selected + Restore All + Delete Selected + Delete All + Close + + + person_add @@ -119,7 +139,7 @@ Password: - Login + Login @@ -151,14 +171,15 @@ - + Folder Navigation & Management - + info @@ -302,8 +323,10 @@ Grant Admin Access - Save User - Cancel + + Cancel + Save User + @@ -313,8 +336,10 @@ Remove User Select a user to remove: - Delete User - Cancel + + Cancel + Delete User + @@ -331,6 +356,17 @@ + + + + + + Yes + No + + + + diff --git a/main.js b/main.js index d5eead0..6460dbb 100644 --- a/main.js +++ b/main.js @@ -16,6 +16,7 @@ import { import { loadFolderTree } from './folderManager.js'; import { initUpload } from './upload.js'; import { initAuth, checkAuthentication } from './auth.js'; +import { setupTrashRestoreDelete } from './trashRestoreDelete.js'; function loadCsrfToken() { fetch('token.php', { credentials: 'include' }) @@ -123,6 +124,7 @@ document.addEventListener("DOMContentLoaded", function () { initFileActions(); initUpload(); loadFolderTree(); + setupTrashRestoreDelete(); const helpBtn = document.getElementById("folderHelpBtn"); const helpTooltip = document.getElementById("folderHelpTooltip"); helpBtn.addEventListener("click", function () { @@ -132,11 +134,6 @@ document.addEventListener("DOMContentLoaded", function () { } else { helpTooltip.style.display = "none"; } - // Set the icon color based on dark mode. - const helpIcon = document.querySelector("#folderHelpBtn > i.material-icons.folder-help-icon"); - if (helpIcon) { - helpIcon.style.color = document.body.classList.contains("dark-mode") ? "#ffa500" : "orange"; - } }); } else { console.warn("User not authenticated. Data loading deferred."); diff --git a/restoreFiles.php b/restoreFiles.php new file mode 100644 index 0000000..d4b454d --- /dev/null +++ b/restoreFiles.php @@ -0,0 +1,175 @@ + "Invalid CSRF token"]); + http_response_code(403); + exit; +} + +// Ensure user is authenticated +if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + echo json_encode(["error" => "Unauthorized"]); + http_response_code(401); + exit; +} + +// Define the trash directory and trash metadata file. +$trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR; +if (!file_exists($trashDir)) { + mkdir($trashDir, 0755, true); +} +$trashMetadataFile = $trashDir . "trash.json"; +$trashData = []; +if (file_exists($trashMetadataFile)) { + $json = file_get_contents($trashMetadataFile); + $trashData = json_decode($json, true); + if (!is_array($trashData)) { + $trashData = []; + } +} + +// Helper: Generate the metadata file path for a given folder. +// For "root", returns "root_metadata.json". Otherwise, replaces slashes, backslashes, and spaces with dashes and appends "_metadata.json". +function getMetadataFilePath($folder) { + if (strtolower($folder) === 'root' || $folder === '') { + return META_DIR . "root_metadata.json"; + } + return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; +} + +// Read request body. +$data = json_decode(file_get_contents("php://input"), true); + +// Validate request. +if (!isset($data['files']) || !is_array($data['files'])) { + echo json_encode(["error" => "No file or folder identifiers provided"]); + exit; +} + +// Define a safe file name pattern. +$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/'; + +$restoredItems = []; +$errors = []; + +foreach ($data['files'] as $trashFileName) { + $trashFileName = trim($trashFileName); + if (!preg_match($safeFileNamePattern, $trashFileName)) { + $errors[] = "$trashFileName has an invalid format."; + continue; + } + + // Find the matching trash record. + $recordKey = null; + foreach ($trashData as $key => $record) { + if (isset($record['trashName']) && $record['trashName'] === $trashFileName) { + $recordKey = $key; + break; + } + } + if ($recordKey === null) { + $errors[] = "No trash record found for $trashFileName."; + continue; + } + + $record = $trashData[$recordKey]; + if (!isset($record['originalFolder']) || !isset($record['originalName'])) { + $errors[] = "Incomplete trash record for $trashFileName."; + continue; + } + $originalFolder = $record['originalFolder']; + $originalName = $record['originalName']; + + // Convert the absolute original folder to a relative folder. + $relativeFolder = 'root'; + if (strpos($originalFolder, UPLOAD_DIR) === 0) { + $relativeFolder = trim(substr($originalFolder, strlen(UPLOAD_DIR)), '/\\'); + if ($relativeFolder === '') { + $relativeFolder = 'root'; + } + } + + // Build destination path. + if ($relativeFolder !== 'root') { + $destinationPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $relativeFolder . DIRECTORY_SEPARATOR . $originalName; + } else { + $destinationPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $originalName; + } + + // If the record is for a folder, recreate the folder. + if (isset($record['type']) && $record['type'] === 'folder') { + if (!file_exists($destinationPath)) { + if (mkdir($destinationPath, 0755, true)) { + $restoredItems[] = $originalName . " (folder restored)"; + } else { + $errors[] = "Failed to restore folder $originalName."; + continue; + } + } else { + $errors[] = "Folder already exists at destination: $originalName."; + continue; + } + // Remove the trash record and continue. + unset($trashData[$recordKey]); + continue; + } + + // For files: Ensure the destination directory exists. + $destinationDir = dirname($destinationPath); + if (!file_exists($destinationDir)) { + if (!mkdir($destinationDir, 0755, true)) { + $errors[] = "Failed to create destination folder for $originalName."; + continue; + } + } + + if (file_exists($destinationPath)) { + $errors[] = "File already exists at destination: $originalName."; + continue; + } + + // Move the file from trash to its original location. + $sourcePath = $trashDir . $trashFileName; + if (file_exists($sourcePath)) { + if (rename($sourcePath, $destinationPath)) { + $restoredItems[] = $originalName; + // Update metadata for the restored file. + $metadataFile = getMetadataFilePath($relativeFolder); + $metadata = []; + if (file_exists($metadataFile)) { + $metadata = json_decode(file_get_contents($metadataFile), true); + if (!is_array($metadata)) { + $metadata = []; + } + } + $restoredMeta = [ + "uploaded" => isset($record['uploaded']) ? $record['uploaded'] : date(DATE_TIME_FORMAT), + "uploader" => isset($record['uploader']) ? $record['uploader'] : "Unknown" + ]; + $metadata[$originalName] = $restoredMeta; + file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT)); + unset($trashData[$recordKey]); + } else { + $errors[] = "Failed to restore $originalName."; + } + } else { + $errors[] = "Trash file not found: $trashFileName."; + } +} + +// Write back updated trash metadata. +file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT)); + +if (empty($errors)) { + echo json_encode(["success" => "Items restored: " . implode(", ", $restoredItems)]); +} else { + echo json_encode(["error" => implode("; ", $errors) . ". Items restored: " . implode(", ", $restoredItems)]); +} +exit; +?> \ No newline at end of file diff --git a/styles.css b/styles.css index 94eb93a..08abd13 100644 --- a/styles.css +++ b/styles.css @@ -10,6 +10,10 @@ body { transition: background-color 0.3s, color 0.3s; } +body { + letter-spacing: 0.2px; +} + /* CONTAINER */ .container { margin-top: 20px; @@ -44,6 +48,25 @@ body { /* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */ /************************************************************/ +.btn-login { + margin-top: 10px; +} + +/* Color overrides */ +.orange-icon { + color: #2196F3 !important; + font-size: 34px !important; + transform: translateY(-3px) !important; +} + +.red-icon { + width: 34px !important; + display: inline-block !important; + font-size: 34px !important; + color: red !important; + transform: translateY(-3px) !important; +} + .header-container { display: flex; align-items: center; @@ -190,7 +213,7 @@ body.dark-mode header { border: 1px solid #ccc; border-radius: 4px; padding: 10px; - box-shadow: 2px 2px 6px rgba(0,0,0,0.2); + box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2); } /* Folder Help Tooltip - Dark Mode */ @@ -199,19 +222,21 @@ body.dark-mode .folder-help-tooltip { color: #eee !important; border: 1px solid #555 !important; } + #folderHelpBtn i.material-icons.folder-help-icon { -webkit-text-fill-color: orange !important; color: inherit !important; } body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon { - -webkit-text-fill-color: #ffa500 !important; /* or another color for dark mode */ + -webkit-text-fill-color: #ffa500 !important; } + /************************************************************/ /* RESPONSIVE HEADER FIXES */ /************************************************************/ -@media (max-width: 900px) { +@media (max-width: 970px) { .header-container { flex-wrap: wrap; height: auto; @@ -353,6 +378,103 @@ body.dark-mode .card { border: 1px solid #444; } +#restoreFilesModal .modal-content { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + margin: 0 !important; + z-index: 10000 !important; + width: 95% !important; + max-width: 800px !important; + background: transparent !important; +} + +/* Ensure the inner modal content still has a white background */ +#restoreFilesModal .modal-content { + background: #fff !important; + padding: 20px; + border: 1px solid #ccc; + border-radius: 4px; +} + +/* Override modal content for dark mode */ +body.dark-mode #restoreFilesModal .modal-content { + background: #2c2c2c !important; + border: 1px solid #555 !important; + color: #f0f0f0; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6) !important; +} + +/* Custom styling for restore modal buttons */ +#restoreSelectedBtn, +#restoreAllBtn, +#deleteTrashSelectedBtn, +#deleteAllBtn, +#closeRestoreModal { + padding: 10px 20px !important; + font-size: 16px !important; + border-radius: 4px !important; + transition: background-color 0.3s ease !important; + border: none !important; + margin-bottom: 10px !important; +} + +/* Primary button - Restore Selected */ +#restoreSelectedBtn { + background-color: #007bff !important; + color: #ffffff !important; +} + +#restoreSelectedBtn:hover { + background-color: #0056b3 !important; + color: #ffffff !important; +} + +/* Secondary button - Restore All */ +#restoreAllBtn { + background-color: #6c757d !important; + color: #ffffff !important; +} + +#restoreAllBtn:hover { + background-color: #5a6268 !important; + color: #ffffff !important; +} + +/* Warning button - Delete Selected */ +#deleteTrashSelectedBtn { + background-color: #ffc107 !important; + color: #212529 !important; +} + +#deleteTrashSelectedBtn:hover { + background-color: #e0a800 !important; + color: #212529 !important; +} + +/* Danger button - Delete All */ +#deleteAllBtn { + background-color: #dc3545 !important; + color: #ffffff !important; +} + +#deleteAllBtn:hover { + background-color: #c82333 !important; + color: #ffffff !important; +} + +/* Dark button - Close Restore Modal */ +#closeRestoreModal { + background-color: #343a40 !important; + color: #ffffff !important; +} + +#closeRestoreModal:hover { + background-color: #23272b !important; + color: #ffffff !important; +} + .modal { display: none; position: fixed; @@ -446,26 +568,31 @@ body.dark-mode .editor-close-btn:hover { color: #000; } +/* Editor Modal */ .editor-modal { position: fixed; - top: 50%; - left: 50%; - transform: translate(5%, 5%); + top: 5%; + left: 5%; + width: 90vw; + height: 90vh; background-color: #fff; padding: 20px; border: 1px solid #ccc; - border-radius: 4px; - width: 50vw; - max-width: 90vw; - min-width: 400px; - height: 600px; - resize: both; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - z-index: 1100; - display: flex; - flex-direction: column; - max-height: 90vh; - overflow: hidden; + border-radius: 4px !important; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important; + z-index: 1100 !important; + display: flex !important; + flex-direction: column !important; + overflow: hidden !important; + resize: both !important; +} + +/* Editor Textarea */ +.editor-textarea { + flex-grow: 1 !important; + width: 100% !important; + resize: none !important; + overflow: auto !important; } body.dark-mode .editor-modal { @@ -507,15 +634,6 @@ body.dark-mode .editor-modal { margin-bottom: 5px; } -.editor-textarea { - flex-grow: 1; - min-height: 5px !important; - height: auto !important; - max-height: 100vh !important; - resize: none; - overflow: auto; -} - .editor-footer { margin-top: 5px; text-align: right; @@ -524,6 +642,13 @@ body.dark-mode .editor-modal { /* =========================================================== LOGOUT & USER CONTROLS =========================================================== */ +.modal-content .button-container { + display: flex !important; + justify-content: flex-end; + gap: 5px; + margin-top: 20px; +} + .logout-container { position: absolute; top: 10px; @@ -535,7 +660,8 @@ body.dark-mode .editor-modal { } #uploadBtn { - font-size: 16px; + margin-top: 20px; + font-size: 20px; padding: 10px 22px; align-items: center; } @@ -698,14 +824,10 @@ body.dark-mode .editor-modal { } #fileList button.edit-btn { - background-color: #4CAF50; + background-color: #007bff; color: white; } -#fileList button.edit-btn:hover { - background-color: #43A047; -} - .rename-btn .material-icons { color: black !important; } @@ -764,10 +886,8 @@ body.dark-mode #fileList table tr { #fileList table td { border: none !important; white-space: nowrap; - /* Prevents wrapping for all other columns */ } -/* Ensure only File Name column wraps */ #fileList table th[data-column="name"], #fileList table td:nth-child(2) { white-space: normal !important; @@ -777,7 +897,7 @@ body.dark-mode #fileList table tr { text-align: left !important; line-height: 1.2 !important; padding: 8px 10px !important; - max-width: 200px !important; + max-width: 250px !important; min-width: 120px !important; } @@ -795,7 +915,7 @@ body.dark-mode #fileList table tr { #fileList table th[data-column="name"], #fileList table td:nth-child(2) { max-width: 280px !important; - min-width: 180px !important; + min-width: 120px !important; } } @@ -803,8 +923,8 @@ body.dark-mode #fileList table tr { #fileList table th[data-column="name"], #fileList table td:nth-child(2) { - max-width: 380px !important; - min-width: 180px !important; + max-width: 510px !important; + min-width: 240px !important; } } @@ -880,6 +1000,8 @@ label { #createFolderBtn { margin-top: 0px !important; + height: 40px !important; + font-size: 1rem; } .folder-actions { @@ -1129,7 +1251,7 @@ body.dark-mode #fileListContainer { cursor: pointer; margin-right: 5px; display: inline-block; - width: 25px; + width: 25px; text-align: right; } @@ -1192,18 +1314,17 @@ body.dark-mode .image-preview-modal-content { border-color: #444; } -.preview-btn { +.preview-btn, +.download-btn, +.rename-btn, +.share-btn, +.edit-btn { display: flex; align-items: center; + padding: 8px 12px; justify-content: center; } -.preview-btn i.material-icons { - vertical-align: middle; - margin: 0; - margin-top: -2px; -} - .share-btn { /* Your custom styles here */ border: none; @@ -1252,7 +1373,8 @@ body.dark-mode .image-preview-modal-content { .share-modal-content { width: 600px !important; - max-width: 90vw !important; /* ensures it doesn't exceed the viewport width */ + max-width: 90vw !important; + /* ensures it doesn't exceed the viewport width */ } body.dark-mode .close-image-modal { @@ -1450,10 +1572,18 @@ body.dark-mode .btn-secondary { border-radius: 4px; font-size: 16px; cursor: pointer; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* More subtle drop shadow */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); transition: background 0.3s ease, box-shadow 0.3s ease; } +@media (max-width: 600px) { + #toggleViewBtn { + margin-left: auto !important; + margin-right: auto !important; + display: block !important; + } +} + #toggleViewBtn:hover { background: rgba(0, 0, 0, 0.8); box-shadow: 0 3px 5px rgba(0, 0, 0, 0.4); @@ -1504,7 +1634,6 @@ body.dark-mode #uploadProgressContainer .progress-bar { .dark-mode-toggle:hover { background-color: rgba(255, 255, 255, 0.15) !important; - /* Slight highlight */ } .dark-mode-toggle:active { @@ -1547,7 +1676,7 @@ body.dark-mode .dark-mode-toggle:hover { .folder-help-icon { vertical-align: middle; color: #d96601; - font-size: 20px !important; + font-size: 24px !important; } .folder-help-list { @@ -1569,6 +1698,7 @@ body.dark-mode .folder-help-summary { body.dark-mode .folder-help-icon { color: #f6a72c; font-size: 20px; + } body.dark-mode #searchIcon { @@ -1614,7 +1744,7 @@ body.dark-mode .CodeMirror-matchingbracket { } .gallery-nav-btn { - background: rgba(80, 80, 80, 0.6) !important; /* More translucent dark grey */ + background: rgba(80, 80, 80, 0.6) !important; border: none !important; color: white !important; font-size: 48px !important; @@ -1626,11 +1756,10 @@ body.dark-mode .CodeMirror-matchingbracket { } .gallery-nav-btn:hover { - background: rgba(80, 80, 80, 0.8) !important; /* Slightly less translucent on hover */ + background: rgba(80, 80, 80, 0.8) !important; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4) !important; } -/* If you need distinct positioning for left and right buttons */ .gallery-nav-btn.left { left: 10px; right: auto; @@ -1649,4 +1778,19 @@ body.dark-mode .CodeMirror-matchingbracket { body.dark-mode .drop-hover { background-color: rgba(255, 255, 255, 0.1) !important; border-bottom: 1px dashed #ffffff !important; +} + +#restoreFilesList li { + display: flex !important; + align-items: center !important; + margin-bottom: 5px; +} + +#restoreFilesList li input[type="checkbox"] { + margin: 0 !important; + transform: translateY(-3px) !important; +} + +#restoreFilesList li label { + margin-left: 8px !important; } \ No newline at end of file diff --git a/trashRestoreDelete.js b/trashRestoreDelete.js new file mode 100644 index 0000000..28b7184 --- /dev/null +++ b/trashRestoreDelete.js @@ -0,0 +1,320 @@ +// trashRestoreDelete.js +import { sendRequest } from './networkUtils.js'; +import { toggleVisibility, showToast } from './domUtils.js'; +import { loadFileList } from './fileManager.js'; +import { loadFolderTree } from './folderManager.js'; + +/** + * Displays a custom confirmation modal with the given message. + * Calls onConfirm() if the user confirms. + */ +function showConfirm(message, onConfirm) { + // Assume your custom confirm modal exists with id "customConfirmModal" + // and has elements "confirmMessage", "confirmYesBtn", and "confirmNoBtn". + const modal = document.getElementById("customConfirmModal"); + const messageElem = document.getElementById("confirmMessage"); + const yesBtn = document.getElementById("confirmYesBtn"); + const noBtn = document.getElementById("confirmNoBtn"); + + if (!modal || !messageElem || !yesBtn || !noBtn) { + // Fallback to browser confirm if custom modal is not found. + if (confirm(message)) { + onConfirm(); + } + return; + } + + messageElem.textContent = message; + modal.style.display = "block"; + + // Clear any previous event listeners by cloning the node. + const yesBtnClone = yesBtn.cloneNode(true); + yesBtn.parentNode.replaceChild(yesBtnClone, yesBtn); + const noBtnClone = noBtn.cloneNode(true); + noBtn.parentNode.replaceChild(noBtnClone, noBtn); + + yesBtnClone.addEventListener("click", () => { + modal.style.display = "none"; + onConfirm(); + }); + noBtnClone.addEventListener("click", () => { + modal.style.display = "none"; + }); +} + +/** + * Sets up event listeners for trash restore and delete operations. + * This function should be called from main.js after authentication. + */ +export function setupTrashRestoreDelete() { + console.log("Setting up trash restore/delete listeners."); + + // --- Attach listener to the restore button (created in auth.js) to open the modal. + const restoreBtn = document.getElementById("restoreFilesBtn"); + if (restoreBtn) { + restoreBtn.addEventListener("click", () => { + toggleVisibility("restoreFilesModal", true); + loadTrashItems(); + }); + } else { + console.warn("restoreFilesBtn not found. It may not be available for the current user."); + setTimeout(() => { + const retryBtn = document.getElementById("restoreFilesBtn"); + if (retryBtn) { + retryBtn.addEventListener("click", () => { + toggleVisibility("restoreFilesModal", true); + loadTrashItems(); + }); + } + }, 500); + } + + + // --- Restore Selected: Restore only the selected trash items. + const restoreSelectedBtn = document.getElementById("restoreSelectedBtn"); + if (restoreSelectedBtn) { + restoreSelectedBtn.addEventListener("click", () => { + const selected = document.querySelectorAll("#restoreFilesList input[type='checkbox']:checked"); + const files = Array.from(selected).map(chk => chk.value); + console.log("Restore Selected clicked, files:", files); + if (files.length === 0) { + showToast("No trash items selected for restore."); + return; + } + fetch("restoreFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ files }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(data.success); + toggleVisibility("restoreFilesModal", false); + loadFileList(window.currentFolder); + loadFolderTree(window.currentFolder); + } else { + showToast(data.error); + } + }) + .catch(err => { + console.error("Error restoring files:", err); + showToast("Error restoring files."); + }); + }); + } else { + console.error("restoreSelectedBtn not found."); + } + + // --- Restore All: Restore all trash items. + const restoreAllBtn = document.getElementById("restoreAllBtn"); + if (restoreAllBtn) { + restoreAllBtn.addEventListener("click", () => { + const allChk = document.querySelectorAll("#restoreFilesList input[type='checkbox']"); + const files = Array.from(allChk).map(chk => chk.value); + console.log("Restore All clicked, files:", files); + if (files.length === 0) { + showToast("Trash is empty."); + return; + } + fetch("restoreFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ files }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(data.success); + toggleVisibility("restoreFilesModal", false); + loadFileList(window.currentFolder); + loadFolderTree(window.currentFolder); + + } else { + showToast(data.error); + } + }) + .catch(err => { + console.error("Error restoring files:", err); + showToast("Error restoring files."); + }); + }); + } else { + console.error("restoreAllBtn not found."); + } + + // --- Delete Selected: Permanently delete selected trash items with confirmation. + const deleteTrashSelectedBtn = document.getElementById("deleteTrashSelectedBtn"); + if (deleteTrashSelectedBtn) { + deleteTrashSelectedBtn.addEventListener("click", () => { + const selected = document.querySelectorAll("#restoreFilesList input[type='checkbox']:checked"); + const files = Array.from(selected).map(chk => chk.value); + console.log("Delete Selected clicked, files:", files); + if (files.length === 0) { + showToast("No trash items selected for deletion."); + return; + } + showConfirm("Are you sure you want to permanently delete the selected trash items?", () => { + fetch("deleteTrashFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ files }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(data.success); + loadTrashItems(); + loadFileList(window.currentFolder); + loadFolderTree(window.currentFolder); + } else { + showToast(data.error); + } + }) + .catch(err => { + console.error("Error deleting trash files:", err); + showToast("Error deleting trash files."); + }); + }); + }); + } else { + console.error("deleteTrashSelectedBtn not found."); + } + + // --- Delete All: Permanently delete all trash items with confirmation. + const deleteAllBtn = document.getElementById("deleteAllBtn"); + if (deleteAllBtn) { + deleteAllBtn.addEventListener("click", () => { + showConfirm("Are you sure you want to permanently delete all trash items? This action cannot be undone.", () => { + fetch("deleteTrashFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ deleteAll: true }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(data.success); + toggleVisibility("restoreFilesModal", false); + loadFileList(window.currentFolder); + loadFolderTree(window.currentFolder); + } else { + showToast(data.error); + } + }) + .catch(err => { + console.error("Error deleting all trash files:", err); + showToast("Error deleting all trash files."); + }); + }); + }); + } else { + console.error("deleteAllBtn not found."); + } + + // --- Close the Restore Modal --- + const closeRestoreModal = document.getElementById("closeRestoreModal"); + if (closeRestoreModal) { + closeRestoreModal.addEventListener("click", () => { + toggleVisibility("restoreFilesModal", false); + }); + } else { + console.error("closeRestoreModal not found."); + } + + // --- Auto-purge old trash items (older than 3 days) --- + autoPurgeOldTrash(); +} + +/** + * Loads trash items from the server and updates the restore modal list. + */ +export function loadTrashItems() { + fetch("getTrashItems.php", { credentials: "include" }) + .then(response => response.json()) + .then(trashItems => { + const listContainer = document.getElementById("restoreFilesList"); + if (listContainer) { + listContainer.innerHTML = ""; + trashItems.forEach(item => { + const li = document.createElement("li"); + li.style.listStyle = "none"; + li.style.marginBottom = "5px"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.value = item.trashName; + li.appendChild(checkbox); + + const label = document.createElement("label"); + label.style.marginLeft = "8px"; + // Include the deletedBy username in the label text. + const deletedBy = item.deletedBy ? item.deletedBy : "Unknown"; + label.textContent = `${item.originalName} (${deletedBy} trashed on ${new Date(item.trashedAt * 1000).toLocaleString()})`; + li.appendChild(label); + + listContainer.appendChild(li); + }); + } + }) + .catch(err => { + console.error("Error loading trash items:", err); + showToast("Error loading trash items."); + }); +} + +/** + * Automatically purges (permanently deletes) trash items older than 3 days. + */ +function autoPurgeOldTrash() { + fetch("getTrashItems.php", { credentials: "include" }) + .then(response => response.json()) + .then(trashItems => { + const now = Date.now(); + const threeDays = 3 * 24 * 60 * 60 * 1000; + const oldItems = trashItems.filter(item => (now - (item.trashedAt * 1000)) > threeDays); + if (oldItems.length > 0) { + const files = oldItems.map(item => item.trashName); + fetch("deleteTrashFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ files }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log("Auto-purged old trash items:", data.success); + loadTrashItems(); + } else { + console.warn("Auto-purge warning:", data.error); + } + }) + .catch(err => { + console.error("Error auto-purging old trash items:", err); + }); + } + }) + .catch(err => { + console.error("Error retrieving trash items for auto-purge:", err); + }); +} \ No newline at end of file diff --git a/upload.php b/upload.php index c327955..bedff9e 100644 --- a/upload.php +++ b/upload.php @@ -43,7 +43,7 @@ if ($folder !== 'root') { $metadataCollection = []; // key: folder path, value: metadata array $metadataChanged = []; // key: folder path, value: boolean -$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/'; +$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/'; foreach ($_FILES["file"]["name"] as $index => $fileName) { $safeFileName = basename($fileName);