Trash with Restore & Delete plus more changes/fixes
This commit is contained in:
20
README.md
20
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
18
addUser.php
18
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"]);
|
||||
?>
|
||||
?>
|
||||
170
auth.js
170
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 = '<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 => {
|
||||
|
||||
@@ -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);
|
||||
?>
|
||||
@@ -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
105
deleteTrashFiles.php
Normal 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;
|
||||
?>
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
<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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
68
getTrashItems.php
Normal 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;
|
||||
?>
|
||||
56
index.html
56
index.html
@@ -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 & 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>
|
||||
|
||||
7
main.js
7
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.");
|
||||
|
||||
175
restoreFiles.php
Normal file
175
restoreFiles.php
Normal 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;
|
||||
?>
|
||||
252
styles.css
252
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;
|
||||
}
|
||||
320
trashRestoreDelete.js
Normal file
320
trashRestoreDelete.js
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user