Trash with Restore & Delete plus more changes/fixes

This commit is contained in:
Ryan
2025-03-21 00:10:08 -04:00
committed by GitHub
parent 8a7dcbe7bf
commit dcd976cdc5
16 changed files with 1119 additions and 166 deletions

View File

@@ -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.
---

View File

@@ -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"]);
?>
?>

170
auth.js
View File

@@ -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 = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
// 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 => {

View File

@@ -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);
?>

View File

@@ -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)]);
}
?>

105
deleteTrashFiles.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
session_start();
require_once 'config.php';
header('Content-Type: application/json');
// --- CSRF Protection ---
$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;
}
// 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;
?>

View File

@@ -164,7 +164,7 @@ export function buildFileTableRow(file, folderPath) {
<i class="material-icons">file_download</i>
</a>
${file.editable ? `
<button class="btn btn-sm btn-primary"
<button class="btn btn-sm edit-btn"
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
title="Edit">
<i class="material-icons">edit</i>

View File

@@ -457,9 +457,15 @@ export function renderFileTable(folder) {
window.currentSearchTerm = newSearchInput.value;
window.currentPage = 1;
renderFileTable(folder);
// After rerender, 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) {
<i class="material-icons">file_download</i>
</a>
${file.editable ? `
<button class="btn btn-sm btn-primary" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="Edit">
<button class="btn btn-sm edit-btn" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="Edit">
<i class="material-icons">edit</i>
</button>
` : ""}
@@ -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");
}
}

View File

@@ -68,6 +68,11 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
const state = loadFolderTreeState();
let html = `<ul class="folder-tree ${defaultDisplay === 'none' ? 'collapsed' : 'expanded'}">`;
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;

68
getTrashItems.php Normal file
View File

@@ -0,0 +1,68 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// 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;
$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;
?>

View File

@@ -9,8 +9,6 @@
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<meta name="csrf-token" content="">
<meta name="share-url" content="">
<!-- External CSS -->
<link rel="stylesheet" href="styles.css" />
<!-- Google Fonts and Material Icons -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
@@ -22,7 +20,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"></script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
@@ -93,6 +91,28 @@
<button id="logoutBtn" title="Logout">
<i class="material-icons">exit_to_app</i>
</button>
<!-- Restore Files Modal (Admin Only) -->
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
<div class="modal-content">
<h4 class="custom-restore-header">
<i class="material-icons orange-icon">restore_from_trash</i>
<span>Restore or</span>
<i class="material-icons red-icon">delete_for_ever</i>
<span>Delete Trash Items</span>
</h4>
<div id="restoreFilesList"
style="max-height:300px; overflow-y:auto; border:1px solid #ccc; padding:10px; margin-bottom:10px;">
<!-- Trash items will be loaded here -->
</div>
<div style="text-align: right;">
<button id="restoreSelectedBtn" class="btn btn-primary">Restore Selected</button>
<button id="restoreAllBtn" class="btn btn-secondary">Restore All</button>
<button id="deleteTrashSelectedBtn" class="btn btn-warning">Delete Selected</button>
<button id="deleteAllBtn" class="btn btn-danger">Delete All</button>
<button id="closeRestoreModal" class="btn btn-dark">Close</button>
</div>
</div>
</div>
<button id="addUserBtn" title="Add User">
<i class="material-icons">person_add</i>
</button>
@@ -119,7 +139,7 @@
<label for="loginPassword">Password:</label>
<input type="password" class="form-control" id="loginPassword" name="password" required />
</div>
<button type="submit" class="btn btn-primary btn-block">Login</button>
<button type="submit" class="btn btn-primary btn-block btn-login">Login</button>
</form>
</div>
</div>
@@ -151,14 +171,15 @@
</div>
</div>
</div>
<!-- Folder Management Card -->
<div class="col-md-6 col-lg-5 d-flex">
<div class="card flex-fill" style="max-width: 900px; width: 100%; position: relative;">
<!-- Card header with folder management title and help icon -->
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<span>Folder Navigation &amp; Management</span>
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help" style="padding: 0; border: none; background: none;">
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help"
style="padding: 0; border: none; background: none;">
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
</button>
</div>
@@ -302,8 +323,10 @@
<input type="checkbox" id="isAdmin" />
<label for="isAdmin">Grant Admin Access</label>
</div>
<button id="saveUserBtn" class="btn btn-primary">Save User</button>
<button id="cancelUserBtn" class="btn btn-secondary">Cancel</button>
<div class="button-container">
<button id="cancelUserBtn" class="btn btn-secondary">Cancel</button>
<button id="saveUserBtn" class="btn btn-primary">Save User</button>
</div>
</div>
</div>
@@ -313,8 +336,10 @@
<h3>Remove User</h3>
<label for="removeUsernameSelect">Select a user to remove:</label>
<select id="removeUsernameSelect" class="form-control"></select>
<button id="deleteUserBtn" class="btn btn-danger">Delete User</button>
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button>
<div class="button-container">
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button>
<button id="deleteUserBtn" class="btn btn-danger">Delete User</button>
</div>
</div>
</div>
@@ -331,6 +356,17 @@
</div>
</div>
<!-- Custom Confirm Modal -->
<div id="customConfirmModal" class="modal" style="display:none;">
<div class="modal-content">
<p id="confirmMessage"></p>
<div class="modal-actions">
<button id="confirmYesBtn" class="btn btn-primary">Yes</button>
<button id="confirmNoBtn" class="btn btn-secondary">No</button>
</div>
</div>
</div>
<!-- JavaScript Files -->
<script type="module" src="main.js"></script>
</body>

View File

@@ -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.");

175
restoreFiles.php Normal file
View File

@@ -0,0 +1,175 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// --- CSRF Protection ---
$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;
}
// 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;
?>

View File

@@ -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;
}

320
trashRestoreDelete.js Normal file
View File

@@ -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);
});
}

View File

@@ -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);