validation, toast, modal, folder tree

This commit is contained in:
Ryan
2025-03-08 22:20:15 -05:00
committed by GitHub
parent 2d49d6eddf
commit 5cc20dfb39
25 changed files with 1229 additions and 525 deletions

View File

@@ -111,7 +111,14 @@ This project is a lightweight, secure web application for uploading, editing, an
--- ---
## Changelog ## changes 3/8/2025
- Validation was added in endpoints.
- Toast notifications were implemented in domUtils.js and integrated throughout the app.
- Modals replaced inline prompts and confirms for rename, create, delete, copy, and move actions.
- Folder tree UI was added and improved to be interactive plus reflect the current state after actions.
## changes 3/7/2025
- **Module Refactoring:** - **Module Refactoring:**
- Split the original `utils.js` into multiple ES6 modules for network requests, DOM utilities, file management, folder management, uploads, and authentication. - Split the original `utils.js` into multiple ES6 modules for network requests, DOM utilities, file management, folder management, uploads, and authentication.

View File

@@ -41,6 +41,12 @@ if (!$newUsername || !$newPassword) {
exit; exit;
} }
// Validate username using preg_match (allow letters, numbers, underscores, dashes, and spaces).
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) {
echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
exit;
}
// Ensure users.txt exists // Ensure users.txt exists
if (!file_exists($usersFile)) { if (!file_exists($usersFile)) {
file_put_contents($usersFile, ''); file_put_contents($usersFile, '');

40
auth.js
View File

@@ -1,7 +1,7 @@
// auth.js // auth.js
import { sendRequest } from './networkUtils.js'; import { sendRequest } from './networkUtils.js';
import { toggleVisibility } from './domUtils.js'; import { toggleVisibility, showToast } from './domUtils.js';
// Import loadFileList from fileManager.js to refresh the file list upon login. // Import loadFileList from fileManager.js to refresh the file list upon login.
import { loadFileList } from './fileManager.js'; import { loadFileList } from './fileManager.js';
@@ -23,33 +23,15 @@ export function initAuth() {
if (data.success) { if (data.success) {
console.log("✅ Login successful. Reloading page."); console.log("✅ Login successful. Reloading page.");
window.location.reload(); window.location.reload();
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
} else { } else {
alert("Login failed: " + (data.error || "Unknown error")); showToast("Login failed: " + (data.error || "Unknown error"));
} }
}) })
.catch(error => console.error("❌ Error logging in:", error)); .catch(error => console.error("❌ Error logging in:", error));
}); });
} }
// Helper function to update UI based on authentication.
function updateUIOnLogin(isAdmin) {
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", true);
toggleVisibility("uploadFileForm", true);
toggleVisibility("fileListContainer", true);
if (isAdmin) {
document.getElementById("addUserBtn").style.display = "block";
document.getElementById("removeUserBtn").style.display = "block";
} else {
document.getElementById("addUserBtn").style.display = "none";
document.getElementById("removeUserBtn").style.display = "none";
}
document.querySelector(".header-buttons").style.visibility = "visible";
loadFileList(window.currentFolder || "root");
}
// Set up the logout button. // Set up the logout button.
document.getElementById("logoutBtn").addEventListener("click", function () { document.getElementById("logoutBtn").addEventListener("click", function () {
fetch("logout.php", { method: "POST" }) fetch("logout.php", { method: "POST" })
@@ -68,7 +50,7 @@ function updateUIOnLogin(isAdmin) {
const newPassword = document.getElementById("newPassword").value.trim(); const newPassword = document.getElementById("newPassword").value.trim();
const isAdmin = document.getElementById("isAdmin").checked; const isAdmin = document.getElementById("isAdmin").checked;
if (!newUsername || !newPassword) { if (!newUsername || !newPassword) {
alert("Username and password are required!"); showToast("Username and password are required!");
return; return;
} }
let url = "addUser.php"; let url = "addUser.php";
@@ -83,11 +65,11 @@ function updateUIOnLogin(isAdmin) {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
alert("User added successfully!"); showToast("User added successfully!");
closeAddUserModal(); closeAddUserModal();
checkAuthentication(); checkAuthentication();
} else { } else {
alert("Error: " + (data.error || "Could not add user")); showToast("Error: " + (data.error || "Could not add user"));
} }
}) })
.catch(error => console.error("Error adding user:", error)); .catch(error => console.error("Error adding user:", error));
@@ -107,7 +89,7 @@ function updateUIOnLogin(isAdmin) {
const selectElem = document.getElementById("removeUsernameSelect"); const selectElem = document.getElementById("removeUsernameSelect");
const usernameToRemove = selectElem.value; const usernameToRemove = selectElem.value;
if (!usernameToRemove) { if (!usernameToRemove) {
alert("Please select a user to remove."); showToast("Please select a user to remove.");
return; return;
} }
if (!confirm("Are you sure you want to delete user " + usernameToRemove + "?")) { if (!confirm("Are you sure you want to delete user " + usernameToRemove + "?")) {
@@ -121,11 +103,11 @@ function updateUIOnLogin(isAdmin) {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
alert("User removed successfully!"); showToast("User removed successfully!");
closeRemoveUserModal(); closeRemoveUserModal();
loadUserList(); loadUserList();
} else { } else {
alert("Error: " + (data.error || "Could not remove user")); showToast("Error: " + (data.error || "Could not remove user"));
} }
}) })
.catch(error => console.error("Error removing user:", error)); .catch(error => console.error("Error removing user:", error));
@@ -140,6 +122,7 @@ export function checkAuthentication() {
.then(data => { .then(data => {
if (data.setup) { if (data.setup) {
window.setupMode = true; window.setupMode = true;
showToast("Setup mode: No users found. Please add an admin user.");
// In setup mode, hide login and main operations; show Add User modal. // In setup mode, hide login and main operations; show Add User modal.
toggleVisibility("loginForm", false); toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", false); toggleVisibility("mainOperations", false);
@@ -168,6 +151,7 @@ export function checkAuthentication() {
} }
document.querySelector(".header-buttons").style.visibility = "visible"; document.querySelector(".header-buttons").style.visibility = "visible";
} else { } else {
showToast("Please log in to continue.");
toggleVisibility("loginForm", true); toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false); toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false); toggleVisibility("uploadFileForm", false);
@@ -213,7 +197,7 @@ function loadUserList() {
selectElem.appendChild(option); selectElem.appendChild(option);
}); });
if (selectElem.options.length === 0) { if (selectElem.options.length === 0) {
alert("No other users found to remove."); showToast("No other users found to remove.");
closeRemoveUserModal(); closeRemoveUserModal();
} }
}) })

View File

@@ -16,27 +16,38 @@ function authenticate($username, $password) {
foreach ($lines as $line) { foreach ($lines as $line) {
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3); list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
if ($username === $storedUser && password_verify($password, $storedPass)) { if ($username === $storedUser && password_verify($password, $storedPass)) {
return $storedRole; // return $storedRole; // Return the user's role
} }
} }
return false; return false;
} }
// Get JSON input // Get JSON input
$data = json_decode(file_get_contents("php://input"), true); $data = json_decode(file_get_contents("php://input"), true);
$username = $data["username"] ?? ""; $username = trim($data["username"] ?? "");
$password = $data["password"] ?? ""; $password = trim($data["password"] ?? "");
// Validate input: ensure both fields are provided.
if (!$username || !$password) {
echo json_encode(["error" => "Username and password are required"]);
exit;
}
// Validate username format: allow only letters, numbers, underscores, dashes, and spaces.
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
exit;
}
// Authenticate user // Authenticate user
$userRole = authenticate($username, $password); $userRole = authenticate($username, $password);
if ($userRole !== false) { if ($userRole !== false) {
$_SESSION["authenticated"] = true; $_SESSION["authenticated"] = true;
$_SESSION["username"] = $username; $_SESSION["username"] = $username;
$_SESSION["isAdmin"] = ($userRole === "1"); // correctly recognize admin status $_SESSION["isAdmin"] = ($userRole === "1"); // "1" indicates admin
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]); echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
} else { } else {
echo json_encode(["error" => "Invalid credentials"]); echo json_encode(["error" => "Invalid credentials"]);
} }
?> ?>

View File

@@ -19,4 +19,4 @@ echo json_encode([
"authenticated" => true, "authenticated" => true,
"isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false "isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false
]); ]);
?> ?>

View File

@@ -10,7 +10,12 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
} }
$data = json_decode(file_get_contents("php://input"), true); $data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['source']) || !isset($data['destination']) || !isset($data['files'])) { if (
!$data ||
!isset($data['source']) ||
!isset($data['destination']) ||
!isset($data['files'])
) {
echo json_encode(["error" => "Invalid request"]); echo json_encode(["error" => "Invalid request"]);
exit; exit;
} }
@@ -19,9 +24,29 @@ $sourceFolder = trim($data['source']);
$destinationFolder = trim($data['destination']); $destinationFolder = trim($data['destination']);
$files = $data['files']; $files = $data['files'];
// Validate folder names: allow letters, numbers, underscores, dashes, spaces, and forward slashes.
$folderPattern = '/^[A-Za-z0-9_\- \/]+$/';
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
echo json_encode(["error" => "Invalid source folder name."]);
exit;
}
if ($destinationFolder !== 'root' && !preg_match($folderPattern, $destinationFolder)) {
echo json_encode(["error" => "Invalid destination folder name."]);
exit;
}
// Trim any leading/trailing slashes and spaces.
$sourceFolder = trim($sourceFolder, "/\\ ");
$destinationFolder = trim($destinationFolder, "/\\ ");
// Build the source and destination directories. // Build the source and destination directories.
$sourceDir = ($sourceFolder === 'root') ? UPLOAD_DIR : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR; $baseDir = rtrim(UPLOAD_DIR, '/\\');
$destDir = ($destinationFolder === 'root') ? UPLOAD_DIR : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR; $sourceDir = ($sourceFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR;
$destDir = ($destinationFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
// Load metadata. // Load metadata.
$metadataFile = META_DIR . META_FILE; $metadataFile = META_DIR . META_FILE;
@@ -36,10 +61,21 @@ if (!is_dir($destDir)) {
} }
$errors = []; $errors = [];
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, and spaces.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
foreach ($files as $fileName) { foreach ($files as $fileName) {
$basename = basename($fileName); $basename = basename(trim($fileName));
// Validate the file name.
if (!preg_match($safeFileNamePattern, $basename)) {
$errors[] = "$basename has an invalid name.";
continue;
}
$srcPath = $sourceDir . $basename; $srcPath = $sourceDir . $basename;
$destPath = $destDir . $basename; $destPath = $destDir . $basename;
// Build metadata keys. // Build metadata keys.
$srcKey = ($sourceFolder === 'root') ? $basename : $sourceFolder . "/" . $basename; $srcKey = ($sourceFolder === 'root') ? $basename : $sourceFolder . "/" . $basename;
$destKey = ($destinationFolder === 'root') ? $basename : $destinationFolder . "/" . $basename; $destKey = ($destinationFolder === 'root') ? $basename : $destinationFolder . "/" . $basename;
@@ -67,4 +103,4 @@ if (empty($errors)) {
} else { } else {
echo json_encode(["error" => implode("; ", $errors)]); echo json_encode(["error" => implode("; ", $errors)]);
} }
?> ?>

View File

@@ -17,32 +17,44 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
// Get the JSON input and decode it // Get the JSON input and decode it
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
if (!isset($input['folder'])) { if (!isset($input['folderName'])) {
echo json_encode(['success' => false, 'error' => 'Folder name not provided.']); echo json_encode(['success' => false, 'error' => 'Folder name not provided.']);
exit; exit;
} }
$folderName = trim($input['folder']); $folderName = trim($input['folderName']);
$parent = isset($input['parent']) ? trim($input['parent']) : "";
// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces // Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces in folderName
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $folderName)) { if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $folderName)) {
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']); echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
exit; exit;
} }
// Build the folder path (assuming UPLOAD_DIR is defined in config.php) // Optionally, sanitize the parent folder if needed.
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folderName; if ($parent && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $parent)) {
echo json_encode(['success' => false, 'error' => 'Invalid parent folder name.']);
exit;
}
// Check if the folder already exists // Build the full folder path.
if (file_exists($folderPath)) { $baseDir = rtrim(UPLOAD_DIR, '/\\');
if ($parent && strtolower($parent) !== "root") {
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName;
} else {
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName;
}
// Check if the folder already exists.
if (file_exists($fullPath)) {
echo json_encode(['success' => false, 'error' => 'Folder already exists.']); echo json_encode(['success' => false, 'error' => 'Folder already exists.']);
exit; exit;
} }
// Attempt to create the folder // Attempt to create the folder.
if (mkdir($folderPath, 0755, true)) { if (mkdir($fullPath, 0755, true)) {
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
} else { } else {
echo json_encode(['success' => false, 'error' => 'Failed to create folder.']); echo json_encode(['success' => false, 'error' => 'Failed to create folder.']);
} }
?> ?>

View File

@@ -20,6 +20,16 @@ if (!isset($data['files']) || !is_array($data['files'])) {
// Determine folder default to 'root' // Determine folder default to 'root'
$folder = isset($data['folder']) ? trim($data['folder']) : 'root'; $folder = isset($data['folder']) ? trim($data['folder']) : 'root';
// Validate folder: allow letters, numbers, underscores, dashes, spaces, and forward slashes
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
// Trim any leading/trailing slashes and spaces.
$folder = trim($folder, "/\\ ");
// Build the upload directory.
if ($folder !== 'root') { if ($folder !== 'root') {
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
} else { } else {
@@ -29,8 +39,19 @@ if ($folder !== 'root') {
$deletedFiles = []; $deletedFiles = [];
$errors = []; $errors = [];
// Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
foreach ($data['files'] as $fileName) { foreach ($data['files'] as $fileName) {
$filePath = $uploadDir . basename($fileName); $basename = basename(trim($fileName));
// Validate the file name.
if (!preg_match($safeFileNamePattern, $basename)) {
$errors[] = "$basename has an invalid name.";
continue;
}
$filePath = $uploadDir . $basename;
if (file_exists($filePath)) { if (file_exists($filePath)) {
if (unlink($filePath)) { if (unlink($filePath)) {
@@ -39,7 +60,7 @@ foreach ($data['files'] as $fileName) {
$errors[] = "Failed to delete $fileName"; $errors[] = "Failed to delete $fileName";
} }
} else { } else {
// If file not found, consider it already deleted. // Consider file already deleted.
$deletedFiles[] = $fileName; $deletedFiles[] = $fileName;
} }
} }
@@ -49,4 +70,4 @@ if (empty($errors)) {
} else { } else {
echo json_encode(["error" => implode("; ", $errors) . ". Files deleted: " . implode(", ", $deletedFiles)]); echo json_encode(["error" => implode("; ", $errors) . ". Files deleted: " . implode(", ", $deletedFiles)]);
} }
?> ?>

View File

@@ -24,12 +24,19 @@ if (!isset($input['folder'])) {
$folderName = trim($input['folder']); $folderName = trim($input['folder']);
// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces // Prevent deletion of root.
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $folderName)) { if ($folderName === 'root') {
echo json_encode(['success' => false, 'error' => 'Cannot delete root folder.']);
exit;
}
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
if (!preg_match('/^[A-Za-z0-9_\- \/]+$/', $folderName)) {
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']); echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
exit; exit;
} }
// Build the folder path (supports subfolder paths like "FolderTest/FolderTestSub")
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folderName; $folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folderName;
// Check if the folder exists and is a directory // Check if the folder exists and is a directory
@@ -50,4 +57,4 @@ if (rmdir($folderPath)) {
} else { } else {
echo json_encode(['success' => false, 'error' => 'Failed to delete folder.']); echo json_encode(['success' => false, 'error' => 'Failed to delete folder.']);
} }
?> ?>

View File

@@ -1,70 +1,83 @@
// domUtils.js // domUtils.js
export function toggleVisibility(elementId, shouldShow) { export function toggleVisibility(elementId, shouldShow) {
const element = document.getElementById(elementId); const element = document.getElementById(elementId);
if (element) { if (element) {
element.style.display = shouldShow ? "block" : "none"; element.style.display = shouldShow ? "block" : "none";
} else { } else {
console.error(`Element with id "${elementId}" not found.`); console.error(`Element with id "${elementId}" not found.`);
}
} }
}
export function escapeHTML(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Toggle all checkboxes (assumes checkboxes have class 'file-checkbox')
export function toggleAllCheckboxes(masterCheckbox) {
const checkboxes = document.querySelectorAll(".file-checkbox");
checkboxes.forEach(chk => {
chk.checked = masterCheckbox.checked;
});
updateFileActionButtons(); // call the updated function
}
// This updateFileActionButtons function checks for checkboxes inside the file list container.
export function updateFileActionButtons() {
const fileListContainer = document.getElementById("fileList");
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
const copyBtn = document.getElementById("copySelectedBtn");
const moveBtn = document.getElementById("moveSelectedBtn");
const deleteBtn = document.getElementById("deleteSelectedBtn");
const folderDropdown = document.getElementById("copyMoveFolderSelect");
export function escapeHTML(str) { // Hide the buttons and dropdown if no files exist.
return String(str) if (fileCheckboxes.length === 0) {
.replace(/&/g, "&amp;") copyBtn.style.display = "none";
.replace(/</g, "&lt;") moveBtn.style.display = "none";
.replace(/>/g, "&gt;") deleteBtn.style.display = "none";
.replace(/"/g, "&quot;") folderDropdown.style.display = "none";
.replace(/'/g, "&#039;"); } else {
} // Otherwise, show the buttons and dropdown.
copyBtn.style.display = "inline-block";
// Toggle all checkboxes (assumes checkboxes have class 'file-checkbox') moveBtn.style.display = "inline-block";
export function toggleAllCheckboxes(masterCheckbox) { deleteBtn.style.display = "inline-block";
const checkboxes = document.querySelectorAll(".file-checkbox"); folderDropdown.style.display = "none";
checkboxes.forEach(chk => {
chk.checked = masterCheckbox.checked;
});
updateFileActionButtons(); // call the updated function
}
// This updateFileActionButtons function checks for checkboxes inside the file list container.
export function updateFileActionButtons() {
const fileListContainer = document.getElementById("fileList");
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
const copyBtn = document.getElementById("copySelectedBtn");
const moveBtn = document.getElementById("moveSelectedBtn");
const deleteBtn = document.getElementById("deleteSelectedBtn");
const folderDropdown = document.getElementById("copyMoveFolderSelect");
// Hide the buttons and dropdown if no files exist. // Enable the buttons if at least one file is selected; otherwise disable.
if (fileCheckboxes.length === 0) { if (selectedCheckboxes.length > 0) {
copyBtn.style.display = "none"; copyBtn.disabled = false;
moveBtn.style.display = "none"; moveBtn.disabled = false;
deleteBtn.style.display = "none"; deleteBtn.disabled = false;
folderDropdown.style.display = "none";
} else { } else {
// Otherwise, show the buttons and dropdown. copyBtn.disabled = true;
copyBtn.style.display = "inline-block"; moveBtn.disabled = true;
moveBtn.style.display = "inline-block"; deleteBtn.disabled = true;
deleteBtn.style.display = "inline-block";
folderDropdown.style.display = "inline-block";
// Enable the buttons if at least one file is selected; otherwise disable.
if (selectedCheckboxes.length > 0) {
copyBtn.disabled = false;
moveBtn.disabled = false;
deleteBtn.disabled = false;
} else {
copyBtn.disabled = true;
moveBtn.disabled = true;
deleteBtn.disabled = true;
}
} }
} }
}
export function showToast(message, duration = 3000) {
const toast = document.getElementById("customToast");
if (!toast) {
console.error("Toast element not found");
return;
}
toast.textContent = message;
toast.style.display = "block";
// Force reflow so the transition works.
void toast.offsetWidth;
toast.classList.add("show");
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => {
toast.style.display = "none";
}, 500); // Wait for the opacity transition to finish.
}, duration);
}

View File

@@ -1,5 +1,6 @@
// fileManager.js // fileManager.js
import { escapeHTML, updateFileActionButtons } from './domUtils.js'; import { escapeHTML, updateFileActionButtons, showToast } from './domUtils.js';
import { formatFolderName } from './folderManager.js';
export let fileData = []; export let fileData = [];
export let sortOrder = { column: "uploaded", ascending: true }; export let sortOrder = { column: "uploaded", ascending: true };
@@ -337,114 +338,218 @@ export function handleDeleteSelected(e) {
e.stopImmediatePropagation(); e.stopImmediatePropagation();
const checkboxes = document.querySelectorAll(".file-checkbox:checked"); const checkboxes = document.querySelectorAll(".file-checkbox:checked");
if (checkboxes.length === 0) { if (checkboxes.length === 0) {
alert("No files selected."); showToast("No files selected.");
return; return;
} }
if (!confirm("Are you sure you want to delete the selected files?")) { // Save selected file names in a global variable for use in the modal.
return; window.filesToDelete = Array.from(checkboxes).map(chk => chk.value);
} // Update modal message (optional)
const filesToDelete = Array.from(checkboxes).map(chk => chk.value); document.getElementById("deleteFilesMessage").textContent =
fetch("deleteFiles.php", { "Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?";
method: "POST", // Show the delete modal.
headers: { "Content-Type": "application/json" }, document.getElementById("deleteFilesModal").style.display = "block";
body: JSON.stringify({ folder: window.currentFolder, files: filesToDelete })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("Selected files deleted successfully!");
loadFileList(window.currentFolder);
} else {
alert("Error: " + (data.error || "Could not delete files"));
}
})
.catch(error => console.error("Error deleting files:", error));
} }
// Attach event listeners for delete modal buttons (wrap in DOMContentLoaded):
document.addEventListener("DOMContentLoaded", function () {
const cancelDelete = document.getElementById("cancelDeleteFiles");
if (cancelDelete) {
cancelDelete.addEventListener("click", function () {
document.getElementById("deleteFilesModal").style.display = "none";
window.filesToDelete = [];
});
}
const confirmDelete = document.getElementById("confirmDeleteFiles");
if (confirmDelete) {
confirmDelete.addEventListener("click", function () {
// Proceed with deletion
fetch("deleteFiles.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ folder: window.currentFolder, files: window.filesToDelete })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Selected files deleted successfully!");
loadFileList(window.currentFolder);
} else {
showToast("Error: " + (data.error || "Could not delete files"));
}
})
.catch(error => console.error("Error deleting files:", error))
.finally(() => {
document.getElementById("deleteFilesModal").style.display = "none";
window.filesToDelete = [];
});
});
}
});
// Copy selected files. // Copy selected files.
export function handleCopySelected(e) { export function handleCopySelected(e) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
const checkboxes = document.querySelectorAll(".file-checkbox:checked"); const checkboxes = document.querySelectorAll(".file-checkbox:checked");
if (checkboxes.length === 0) { if (checkboxes.length === 0) {
alert("No files selected for copying."); showToast("No files selected for copying.", 5000);
return; return;
} }
const targetFolder = document.getElementById("copyMoveFolderSelect").value; window.filesToCopy = Array.from(checkboxes).map(chk => chk.value);
if (!targetFolder) { // Open the Copy modal.
alert("Please select a target folder for copying."); document.getElementById("copyFilesModal").style.display = "block";
return; // Populate target folder dropdown.
} loadCopyMoveFolderListForModal("copyTargetFolder");
const filesToCopy = Array.from(checkboxes).map(chk => chk.value);
fetch("copyFiles.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ source: window.currentFolder, files: filesToCopy, destination: targetFolder })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("Selected files copied successfully!");
loadFileList(window.currentFolder);
} else {
alert("Error: " + (data.error || "Could not copy files"));
}
})
.catch(error => console.error("Error copying files:", error));
} }
// In your loadCopyMoveFolderListForModal function, target the dropdown by its ID.
export async function loadCopyMoveFolderListForModal(dropdownId) {
try {
const response = await fetch('getFolderList.php');
const folders = await response.json();
console.log('Folders fetched for modal:', folders);
const folderSelect = document.getElementById(dropdownId);
folderSelect.innerHTML = '';
const rootOption = document.createElement('option');
rootOption.value = 'root';
rootOption.textContent = '(Root)';
folderSelect.appendChild(rootOption);
if (Array.isArray(folders) && folders.length > 0) {
folders.forEach(folder => {
const option = document.createElement('option');
option.value = folder;
option.textContent = formatFolderName(folder);
folderSelect.appendChild(option);
});
}
} catch (error) {
console.error('Error loading folder list for modal:', error);
}
}
// Attach event listeners for copy modal buttons.
document.addEventListener("DOMContentLoaded", function () {
const cancelCopy = document.getElementById("cancelCopyFiles");
if (cancelCopy) {
cancelCopy.addEventListener("click", function () {
document.getElementById("copyFilesModal").style.display = "none";
window.filesToCopy = [];
});
}
const confirmCopy = document.getElementById("confirmCopyFiles");
if (confirmCopy) {
confirmCopy.addEventListener("click", function () {
const targetFolder = document.getElementById("copyTargetFolder").value;
if (!targetFolder) {
showToast("Please select a target folder for copying.!", 5000);
return;
}
if (targetFolder === window.currentFolder) {
showToast("Error: Cannot move files to the same folder.");
return;
}
fetch("copyFiles.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ source: window.currentFolder, files: window.filesToCopy, destination: targetFolder })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Selected files copied successfully!", 5000);
loadFileList(window.currentFolder);
} else {
showToast("Error: " + (data.error || "Could not copy files"), 5000);
}
})
.catch(error => console.error("Error copying files:", error))
.finally(() => {
document.getElementById("copyFilesModal").style.display = "none";
window.filesToCopy = [];
});
});
}
});
// Move selected files. // Move selected files.
export function handleMoveSelected(e) { export function handleMoveSelected(e) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
const checkboxes = document.querySelectorAll(".file-checkbox:checked"); const checkboxes = document.querySelectorAll(".file-checkbox:checked");
if (checkboxes.length === 0) { if (checkboxes.length === 0) {
alert("No files selected for moving."); showToast("No files selected for moving.");
return; return;
} }
const targetFolder = document.getElementById("copyMoveFolderSelect").value; window.filesToMove = Array.from(checkboxes).map(chk => chk.value);
if (!targetFolder) { // Open the Move modal.
alert("Please select a target folder for moving."); document.getElementById("moveFilesModal").style.display = "block";
return; // Populate target folder dropdown.
} loadCopyMoveFolderListForModal("moveTargetFolder");
if (targetFolder === window.currentFolder) {
alert("Error: Cannot move files to the same folder.");
return;
}
const filesToMove = Array.from(checkboxes).map(chk => chk.value);
fetch("moveFiles.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ source: window.currentFolder, files: filesToMove, destination: targetFolder })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("Selected files moved successfully!");
loadFileList(window.currentFolder);
} else {
alert("Error: " + (data.error || "Could not move files"));
}
})
.catch(error => console.error("Error moving files:", error));
} }
document.addEventListener("DOMContentLoaded", function () {
const cancelMove = document.getElementById("cancelMoveFiles");
if (cancelMove) {
cancelMove.addEventListener("click", function () {
document.getElementById("moveFilesModal").style.display = "none";
window.filesToMove = [];
});
}
const confirmMove = document.getElementById("confirmMoveFiles");
if (confirmMove) {
confirmMove.addEventListener("click", function () {
const targetFolder = document.getElementById("moveTargetFolder").value;
if (!targetFolder) {
showToast("Please select a target folder for moving.");
return;
}
if (targetFolder === window.currentFolder) {
showToast("Error: Cannot move files to the same folder.");
return;
}
fetch("moveFiles.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ source: window.currentFolder, files: window.filesToMove, destination: targetFolder })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Selected files moved successfully!");
loadFileList(window.currentFolder);
} else {
showToast("Error: " + (data.error || "Could not move files"));
}
})
.catch(error => console.error("Error moving files:", error))
.finally(() => {
document.getElementById("moveFilesModal").style.display = "none";
window.filesToMove = [];
});
});
}
});
// File Editing Functions. // File Editing Functions.
export function editFile(fileName, folder) { export function editFile(fileName, folder) {
console.log("Edit button clicked for:", fileName); console.log("Edit button clicked for:", fileName);
let existingEditor = document.getElementById("editorContainer"); let existingEditor = document.getElementById("editorContainer");
if (existingEditor) { existingEditor.remove(); } if (existingEditor) { existingEditor.remove(); }
const folderUsed = folder || window.currentFolder || "root"; const folderUsed = folder || window.currentFolder || "root";
// For subfolders, encode each segment separately to preserve slashes.
const folderPath = (folderUsed === "root") const folderPath = (folderUsed === "root")
? "uploads/" ? "uploads/"
: "uploads/" + encodeURIComponent(folderUsed) + "/"; : "uploads/" + folderUsed.split("/").map(encodeURIComponent).join("/") + "/";
const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime(); const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime();
fetch(fileUrl, { method: "HEAD" }) fetch(fileUrl, { method: "HEAD" })
.then(response => { .then(response => {
const contentLength = response.headers.get("Content-Length"); const contentLength = response.headers.get("Content-Length");
if (contentLength && parseInt(contentLength) > 10485760) { if (contentLength && parseInt(contentLength) > 10485760) {
alert("This file is larger than 10 MB and cannot be edited in the browser."); showToast("This file is larger than 10 MB and cannot be edited in the browser.");
throw new Error("File too large."); throw new Error("File too large.");
} }
return fetch(fileUrl); return fetch(fileUrl);
@@ -492,7 +597,7 @@ export function saveFile(fileName, folder) {
}) })
.then(response => response.json()) .then(response => response.json())
.then(result => { .then(result => {
alert(result.success || result.error); showToast(result.success || result.error);
document.getElementById("editorContainer")?.remove(); document.getElementById("editorContainer")?.remove();
loadFileList(folderUsed); loadFileList(folderUsed);
}) })
@@ -546,32 +651,67 @@ export function initFileActions() {
// Rename function: always available. // Rename function: always available.
// Expose renameFile to global scope.
export function renameFile(oldName, folder) { export function renameFile(oldName, folder) {
const newName = prompt(`Enter new name for file "${oldName}":`, oldName); // Store the file name and folder globally for use in the modal.
if (!newName || newName === oldName) { window.fileToRename = oldName;
return; // No change. window.fileFolder = folder || window.currentFolder || "root";
}
const folderUsed = folder || window.currentFolder || "root"; // Pre-fill the input with the current file name.
fetch("renameFile.php", { document.getElementById("newFileName").value = oldName;
method: "POST",
headers: { "Content-Type": "application/json" }, // Show the rename file modal.
body: JSON.stringify({ folder: folderUsed, oldName: oldName, newName: newName }) document.getElementById("renameFileModal").style.display = "block";
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("File renamed successfully!");
loadFileList(folderUsed);
} else {
alert("Error renaming file: " + (data.error || "Unknown error"));
}
})
.catch(error => {
console.error("Error renaming file:", error);
alert("Error renaming file");
});
} }
// Attach event listeners after DOM content is loaded.
document.addEventListener("DOMContentLoaded", () => {
// Cancel button: hide modal and clear input.
const cancelBtn = document.getElementById("cancelRenameFile");
if (cancelBtn) {
cancelBtn.addEventListener("click", function() {
document.getElementById("renameFileModal").style.display = "none";
document.getElementById("newFileName").value = "";
});
}
// Submit button: send rename request.
const submitBtn = document.getElementById("submitRenameFile");
if (submitBtn) {
submitBtn.addEventListener("click", function() {
const newName = document.getElementById("newFileName").value.trim();
if (!newName || newName === window.fileToRename) {
// No change; just hide the modal.
document.getElementById("renameFileModal").style.display = "none";
return;
}
const folderUsed = window.fileFolder;
fetch("renameFile.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ folder: folderUsed, oldName: window.fileToRename, newName: newName })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("File renamed successfully!");
loadFileList(folderUsed);
} else {
showToast("Error renaming file: " + (data.error || "Unknown error"));
}
})
.catch(error => {
console.error("Error renaming file:", error);
showToast("Error renaming file");
})
.finally(() => {
document.getElementById("renameFileModal").style.display = "none";
document.getElementById("newFileName").value = "";
});
});
}
});
// Expose renameFile to global scope. // Expose renameFile to global scope.
window.renameFile = renameFile; window.renameFile = renameFile;

View File

@@ -1,215 +1,381 @@
// folderManager.js import { loadFileList } from './fileManager.js';
import { import { showToast } from './domUtils.js';
loadFileList // ----------------------
} from './fileManager.js'; // Helper functions
// ----------------------
export function renameFolder() { // Format folder name for display (for copy/move dropdown).
const folderSelect = document.getElementById("folderSelect"); export function formatFolderName(folder) {
const selectedFolder = folderSelect.value; if (folder.indexOf("/") !== -1) {
const newFolderName = prompt("Enter the new folder name:", selectedFolder); let parts = folder.split("/");
if (newFolderName && newFolderName !== selectedFolder) { let indent = "";
fetch("renameFolder.php", { for (let i = 1; i < parts.length; i++) {
method: "POST", indent += "\u00A0\u00A0\u00A0\u00A0"; // 4 non-breaking spaces per level
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ oldName: selectedFolder, newName: newFolderName })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("Folder renamed successfully!");
loadFolderList("root");
} else {
alert("Error: " + (data.error || "Could not rename folder"));
}
})
.catch(error => console.error("Error renaming folder:", error));
}
}
export function deleteFolder() {
const folderSelect = document.getElementById("folderSelect");
const selectedFolder = folderSelect.value;
if (!selectedFolder || selectedFolder === "root") {
alert("Please select a valid folder to delete.");
return;
}
// Only prompt once.
if (!confirm("Are you sure you want to delete folder " + selectedFolder + "?")) {
return;
}
// Proceed with deletion.
fetch("deleteFolder.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ folder: selectedFolder })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("Folder deleted successfully!");
// Refresh both folder dropdowns.
loadFolderList("root");
loadCopyMoveFolderList();
} else {
alert("Error: " + (data.error || "Could not delete folder"));
}
})
.catch(error => console.error("Error deleting folder:", error));
}
// Updates the copy/move folder dropdown.
export async function loadCopyMoveFolderList() {
try {
const response = await fetch('getFolderList.php');
const folders = await response.json();
const folderSelect = document.getElementById('copyMoveFolderSelect');
folderSelect.innerHTML = ''; // Clear existing options
// Always add a "Root" option as the default.
const rootOption = document.createElement('option');
rootOption.value = 'root';
rootOption.textContent = '(Root)';
folderSelect.appendChild(rootOption);
if (Array.isArray(folders) && folders.length > 0) {
folders.forEach(folder => {
const option = document.createElement('option');
option.value = folder;
option.textContent = folder;
folderSelect.appendChild(option);
});
}
} catch (error) {
console.error('Error loading folder list:', error);
} }
return indent + parts[parts.length - 1];
} else {
return folder;
} }
}
// Optional helper to load folder lists (alias for loadCopyMoveFolderList). // Build a tree structure from a flat array of folder paths.
function buildFolderTree(folders) {
export function loadFolderList(selectedFolder) { const tree = {};
const folderSelect = document.getElementById("folderSelect"); folders.forEach(folderPath => {
folderSelect.innerHTML = ""; const parts = folderPath.split('/');
const rootOption = document.createElement("option"); let current = tree;
rootOption.value = "root"; parts.forEach(part => {
rootOption.textContent = "(Root)"; if (!current[part]) {
folderSelect.appendChild(rootOption); current[part] = {};
}
fetch("getFolderList.php") current = current[part];
.then(response => response.json()) });
.then(folders => { });
folders.forEach(function (folder) { return tree;
let option = document.createElement("option"); }
option.value = folder;
option.textContent = folder; /**
folderSelect.appendChild(option); * Render the folder tree as nested <ul> elements with toggle icons.
}); * @param {object} tree - The tree object.
* @param {string} parentPath - The path prefix.
// Set the selected folder if provided, else default to "root" * @param {string} defaultDisplay - "block" (open) or "none" (collapsed)
if (selectedFolder && [...folderSelect.options].some(opt => opt.value === selectedFolder)) { */
folderSelect.value = selectedFolder; function renderFolderTree(tree, parentPath = "", defaultDisplay = "none") {
} else { let html = `<ul style="list-style-type:none; padding-left:20px; margin:0; display:${defaultDisplay};">`;
folderSelect.value = "root"; for (const folder in tree) {
} const fullPath = parentPath ? parentPath + "/" + folder : folder;
const hasChildren = Object.keys(tree[folder]).length > 0;
// Update global currentFolder and title, then load the file list html += `<li style="margin:4px 0; display:block;">`;
window.currentFolder = folderSelect.value; if (hasChildren) {
document.getElementById("fileListTitle").textContent = // For nested levels (below root) default to collapsed: toggle label "[+]"
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")"; html += `<span class="folder-toggle" style="cursor:pointer; margin-right:5px;">[+]</span>`;
loadFileList(window.currentFolder); } else {
}) html += `<span style="display:inline-block; width:18px;"></span>`;
.catch(error => console.error("Error loading folder list:", error)); }
html += `<span class="folder-option" data-folder="${fullPath}" style="cursor:pointer;">${folder}</span>`;
if (hasChildren) {
// Nested children always collapse by default.
html += renderFolderTree(tree[folder], fullPath, "none");
}
html += `</li>`;
}
html += `</ul>`;
return html;
}
/**
* Expand the tree path for the given folder.
* This function splits the folder path and, for each level, finds the parent li and forces its nested ul to be open.
*/
function expandTreePath(path) {
const parts = path.split("/");
let cumulative = "";
parts.forEach((part, index) => {
cumulative = index === 0 ? part : cumulative + "/" + part;
const option = document.querySelector(`.folder-option[data-folder="${cumulative}"]`);
if (option) {
const li = option.parentNode;
const nestedUl = li.querySelector("ul");
if (nestedUl && (nestedUl.style.display === "none" || nestedUl.style.display === "")) {
nestedUl.style.display = "block";
const toggle = li.querySelector(".folder-toggle");
if (toggle) {
toggle.textContent = "[-]";
}
}
}
});
}
// ----------------------
// Main Interactive Tree
// ----------------------
export async function loadFolderTree(selectedFolder) {
try {
const response = await fetch('getFolderList.php');
// Check for Unauthorized status
if (response.status === 401) {
console.error("Unauthorized: Please log in to view folders.");
// Optionally, redirect to the login page:
// window.location.href = "/login.html";
return;
}
const folders = await response.json();
if (!Array.isArray(folders)) {
console.error("Folder list response is not an array:", folders);
return;
} }
// Event listener for folder dropdown changes const container = document.getElementById("folderTreeContainer");
document.getElementById("folderSelect").addEventListener("change", function () { if (!container) return;
window.currentFolder = this.value;
document.getElementById("fileListTitle").textContent = const tree = buildFolderTree(folders);
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")";
loadFileList(window.currentFolder); // Build the root row.
let html = `<div id="rootRow" style="margin-bottom:10px; display:flex; align-items:center;">`;
html += `<span class="folder-toggle" style="cursor:pointer; margin-right:5px;">[-]</span>`;
html += `<span class="folder-option" data-folder="root" style="cursor:pointer; font-weight:bold;">(Root)</span>`;
html += `</div>`;
// Append the nested tree for root. Force its display to "block".
html += renderFolderTree(tree, "", "block");
container.innerHTML = html;
if (selectedFolder) {
window.currentFolder = selectedFolder;
} else if (!window.currentFolder) {
window.currentFolder = "root";
}
document.getElementById("fileListTitle").textContent =
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")";
loadFileList(window.currentFolder);
if (window.currentFolder !== "root") {
expandTreePath(window.currentFolder);
}
// --- Attach events ---
container.querySelectorAll(".folder-option").forEach(el => {
el.addEventListener("click", function(e) {
e.stopPropagation();
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
this.classList.add("selected");
const selected = this.getAttribute("data-folder");
window.currentFolder = selected;
document.getElementById("fileListTitle").textContent =
selected === "root" ? "Files in (Root)" : "Files in (" + selected + ")";
loadFileList(selected);
});
}); });
// Event listener for creating a folder const rootToggle = container.querySelector("#rootRow .folder-toggle");
document.getElementById("createFolderBtn").addEventListener("click", function () { if (rootToggle) {
let folderName = prompt("Enter folder name:"); rootToggle.addEventListener("click", function(e) {
if (folderName) { e.stopPropagation();
fetch("createFolder.php", { const nestedUl = container.querySelector("#rootRow + ul");
method: "POST", if (nestedUl) {
headers: { "Content-Type": "application/json" }, if (nestedUl.style.display === "none" || nestedUl.style.display === "") {
body: JSON.stringify({ folder: folderName }) nestedUl.style.display = "block";
}) this.textContent = "[-]";
.then(response => response.json()) } else {
.then(data => { nestedUl.style.display = "none";
if (data.success) { this.textContent = "[+]";
alert("Folder created successfully!"); }
loadFolderList(folderName); }
loadCopyMoveFolderList(); });
} else { }
alert("Error: " + (data.error || "Could not create folder"));
}
})
.catch(error => console.error("Error creating folder:", error));
}
});
// Event listener for renaming a folder container.querySelectorAll(".folder-toggle").forEach(toggle => {
document.getElementById("renameFolderBtn").addEventListener("click", function () { toggle.addEventListener("click", function(e) {
const folderSelect = document.getElementById("folderSelect"); e.stopPropagation();
const selectedFolder = folderSelect.value; const siblingUl = this.parentNode.querySelector("ul");
if (!selectedFolder || selectedFolder === "root") { if (siblingUl) {
alert("Please select a valid folder to rename."); if (siblingUl.style.display === "none" || siblingUl.style.display === "") {
return; siblingUl.style.display = "block";
} this.textContent = "[-]";
let newFolderName = prompt("Enter new folder name for '" + selectedFolder + "':", selectedFolder); } else {
if (newFolderName && newFolderName !== selectedFolder) { siblingUl.style.display = "none";
fetch("renameFolder.php", { this.textContent = "[+]";
method: "POST", }
headers: { "Content-Type": "application/json" }, }
body: JSON.stringify({ oldFolder: selectedFolder, newFolder: newFolderName }) });
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("Folder renamed successfully!");
loadCopyMoveFolderList()
loadFolderList(newFolderName);
} else {
alert("Error: " + (data.error || "Could not rename folder"));
}
})
.catch(error => console.error("Error renaming folder:", error));
}
}); });
} catch (error) {
// Event listener for deleting a folder console.error("Error loading folder tree:", error);
document.getElementById("deleteFolderBtn").addEventListener("click", function () { }
const folderSelect = document.getElementById("folderSelect"); }
const selectedFolder = folderSelect.value;
if (!selectedFolder || selectedFolder === "root") { // For backward compatibility.
alert("Please select a valid folder to delete."); export function loadFolderList(selectedFolder) {
return; loadFolderTree(selectedFolder);
} }
if (confirm("Are you sure you want to delete folder " + selectedFolder + "?")) {
fetch("deleteFolder.php", {
method: "POST", // ----------------------
headers: { "Content-Type": "application/json" }, // Folder Management Functions
body: JSON.stringify({ folder: selectedFolder }) // ----------------------
})
.then(response => response.json()) // Attach event listeners for Rename and Delete buttons.
.then(data => { document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
if (data.success) { document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
alert("Folder deleted successfully!");
loadFolderList("root"); function openRenameFolderModal() {
} else { const selectedFolder = window.currentFolder || "root";
alert("Error: " + (data.error || "Could not delete folder")); if (!selectedFolder || selectedFolder === "root") {
} showToast("Please select a valid folder to rename.");
}) return;
.catch(error => console.error("Error deleting folder:", error)); }
// Pre-fill the input with the current folder name (optional)
document.getElementById("newRenameFolderName").value = selectedFolder;
// Show the modal
document.getElementById("renameFolderModal").style.display = "block";
}
// Attach event listener for Cancel button in the rename modal
document.getElementById("cancelRenameFolder").addEventListener("click", function () {
document.getElementById("renameFolderModal").style.display = "none";
document.getElementById("newRenameFolderName").value = "";
});
// Attach event listener for the Rename (Submit) button in the rename modal
document.getElementById("submitRenameFolder").addEventListener("click", function () {
const selectedFolder = window.currentFolder || "root";
const newFolderName = document.getElementById("newRenameFolderName").value.trim();
if (!newFolderName || newFolderName === selectedFolder) {
showToast("Please enter a valid new folder name.");
return;
}
fetch("renameFolder.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ oldFolder: selectedFolder, newFolder: newFolderName })
})
.then(response => response.json())
.then(data => {
console.log("Rename response:", data);
if (data.success) {
showToast("Folder renamed successfully!");
window.currentFolder = newFolderName;
loadFolderList(newFolderName);
loadCopyMoveFolderList();
} else {
showToast("Error: " + (data.error || "Could not rename folder"));
} }
})
.catch(error => console.error("Error renaming folder:", error))
.finally(() => {
// Hide the modal and clear the input
document.getElementById("renameFolderModal").style.display = "none";
document.getElementById("newRenameFolderName").value = "";
}); });
});
function openDeleteFolderModal() {
const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to delete.");
return;
}
// Update the modal message to include the folder name.
document.getElementById("deleteFolderMessage").textContent =
"Are you sure you want to delete folder " + selectedFolder + "?";
// Show the modal.
document.getElementById("deleteFolderModal").style.display = "block";
}
// Attach event for Cancel button in the delete modal.
document.getElementById("cancelDeleteFolder").addEventListener("click", function () {
document.getElementById("deleteFolderModal").style.display = "none";
});
// Attach event for Confirm/Delete button.
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
const selectedFolder = window.currentFolder || "root";
fetch("deleteFolder.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ folder: selectedFolder })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Folder deleted successfully!");
if (window.currentFolder === selectedFolder) {
window.currentFolder = "root";
}
loadFolderList("root");
loadCopyMoveFolderList();
} else {
showToast("Error: " + (data.error || "Could not delete folder"));
}
})
.catch(error => console.error("Error deleting folder:", error))
.finally(() => {
// Hide the modal after the request completes.
document.getElementById("deleteFolderModal").style.display = "none";
});
});
// Instead of using prompt, show the modal.
document.getElementById("createFolderBtn").addEventListener("click", function () {
document.getElementById("createFolderModal").style.display = "block";
});
// Attach event for the Cancel button.
document.getElementById("cancelCreateFolder").addEventListener("click", function () {
document.getElementById("createFolderModal").style.display = "none";
document.getElementById("newFolderName").value = "";
});
// Attach event for the Submit (Create) button.
document.getElementById("submitCreateFolder").addEventListener("click", function () {
const folderInput = document.getElementById("newFolderName").value.trim();
if (!folderInput) {
showToast("Please enter a folder name.");
return;
}
let selectedFolder = window.currentFolder || "root";
let fullFolderName = folderInput;
if (selectedFolder && selectedFolder !== "root") {
fullFolderName = selectedFolder + "/" + folderInput;
}
console.log("Create folder payload:", { folderName: folderInput, parent: selectedFolder === "root" ? "" : selectedFolder });
fetch("createFolder.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ folderName: folderInput, parent: selectedFolder === "root" ? "" : selectedFolder })
})
.then(response => response.json())
.then(data => {
console.log("Create folder response:", data);
if (data.success) {
showToast("Folder created successfully!");
window.currentFolder = fullFolderName;
loadFolderList(fullFolderName);
loadCopyMoveFolderList();
} else {
showToast("Error: " + (data.error || "Could not create folder"));
}
// Hide modal and clear input.
document.getElementById("createFolderModal").style.display = "none";
document.getElementById("newFolderName").value = "";
})
.catch(error => {
console.error("Error creating folder:", error);
document.getElementById("createFolderModal").style.display = "none";
});
});
// For copy/move folder dropdown.
export async function loadCopyMoveFolderList() {
try {
const response = await fetch('getFolderList.php');
const folders = await response.json();
if (!Array.isArray(folders)) {
console.error("Folder list response is not an array:", folders);
return;
}
const folderSelect = document.getElementById('copyMoveFolderSelect').style.display = "none";
folderSelect.innerHTML = '';
const rootOption = document.createElement('option');
rootOption.value = 'root';
rootOption.textContent = '(Root)';
folderSelect.appendChild(rootOption);
if (Array.isArray(folders) && folders.length > 0) {
folders.forEach(folder => {
const option = document.createElement('option');
option.value = folder;
option.textContent = formatFolderName(folder);
folderSelect.appendChild(option);
});
}
} catch (error) {
console.error('Error loading folder list:', error);
}
}

View File

@@ -13,6 +13,14 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
} }
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root'; $folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
// Allow only safe characters in the folder parameter (letters, numbers, underscores, dashes, spaces, and forward slashes).
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
// Determine the directory based on the folder parameter.
if ($folder !== 'root') { if ($folder !== 'root') {
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; $directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
} else { } else {
@@ -30,12 +38,20 @@ if (!is_dir($directory)) {
$files = array_values(array_diff(scandir($directory), array('.', '..'))); $files = array_values(array_diff(scandir($directory), array('.', '..')));
$fileList = []; $fileList = [];
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, and spaces.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
foreach ($files as $file) { foreach ($files as $file) {
$filePath = $directory . DIRECTORY_SEPARATOR . $file; $filePath = $directory . DIRECTORY_SEPARATOR . $file;
// Only include files (skip directories) // Only include files (skip directories)
if (!is_file($filePath)) continue; if (!is_file($filePath)) continue;
// Build the metadata key. // Optionally, skip files with unsafe names.
if (!preg_match($safeFileNamePattern, $file)) {
continue;
}
// Build the metadata key; if not in root, include the folder path.
$metaKey = ($folder !== 'root') ? $folder . "/" . $file : $file; $metaKey = ($folder !== 'root') ? $folder . "/" . $file : $file;
$fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown"; $fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown";
@@ -63,4 +79,4 @@ foreach ($files as $file) {
} }
echo json_encode(["files" => $fileList]); echo json_encode(["files" => $fileList]);
?> ?>

View File

@@ -9,17 +9,43 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
exit; exit;
} }
$folderList = []; /**
$dir = rtrim(UPLOAD_DIR, '/\\'); * Recursively scan a directory for subfolders.
if (is_dir($dir)) { *
foreach (scandir($dir) as $item) { * @param string $dir The full path to the directory.
* @param string $relative The relative path from the base upload directory.
* @return array An array of folder paths (relative to the base).
*/
function getSubfolders($dir, $relative = '') {
$folders = [];
$items = scandir($dir);
// Allow letters, numbers, underscores, dashes, and spaces in folder names.
$safeFolderNamePattern = '/^[A-Za-z0-9_\- ]+$/';
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue; if ($item === '.' || $item === '..') continue;
// Only process folder names that match the safe pattern.
if (!preg_match($safeFolderNamePattern, $item)) {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $item; $path = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($path)) { if (is_dir($path)) {
$folderList[] = $item; // Build the relative path.
$folderPath = ($relative ? $relative . '/' : '') . $item;
$folders[] = $folderPath;
// Recursively get subfolders.
$subFolders = getSubfolders($path, $folderPath);
$folders = array_merge($folders, $subFolders);
} }
} }
return $folders;
}
$baseDir = rtrim(UPLOAD_DIR, '/\\');
$folderList = [];
if (is_dir($baseDir)) {
$folderList = getSubfolders($baseDir);
} }
echo json_encode($folderList); echo json_encode($folderList);
?> ?>

View File

@@ -13,7 +13,10 @@ if (file_exists($usersFile)) {
foreach ($lines as $line) { foreach ($lines as $line) {
$parts = explode(':', trim($line)); $parts = explode(':', trim($line));
if (count($parts) >= 3) { if (count($parts) >= 3) {
$users[] = ["username" => $parts[0]]; // Optionally, validate username format:
if (preg_match('/^[A-Za-z0-9_\- ]+$/', $parts[0])) {
$users[] = ["username" => $parts[0]];
}
} }
} }
} }

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -14,6 +15,7 @@
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head> </head>
<body> <body>
<!-- Header --> <!-- Header -->
<header> <header>
@@ -29,7 +31,8 @@
<button id="removeUserBtn" title="Remove User"><i class="material-icons">person_remove</i></button> <button id="removeUserBtn" title="Remove User"><i class="material-icons">person_remove</i></button>
</div> </div>
</header> </header>
<!-- Custom Toast Container -->
<div id="customToast"></div>
<div class="container"> <div class="container">
<!-- Login Form --> <!-- Login Form -->
<div class="row" id="loginForm"> <div class="row" id="loginForm">
@@ -47,7 +50,7 @@
</form> </form>
</div> </div>
</div> </div>
<!-- Main Operations: Upload and Folder Management --> <!-- Main Operations: Upload and Folder Management -->
<div id="mainOperations" style="display: none;"> <div id="mainOperations" style="display: none;">
<div class="row" id="uploadFolderRow"> <div class="row" id="uploadFolderRow">
@@ -58,10 +61,12 @@
<div class="card-body"> <div class="card-body">
<form id="uploadFileForm" method="post" enctype="multipart/form-data"> <form id="uploadFileForm" method="post" enctype="multipart/form-data">
<div class="form-group"> <div class="form-group">
<div id="uploadDropArea" style="border:2px dashed #ccc; padding:20px; text-align:center; cursor:pointer;"> <div id="uploadDropArea"
style="border:2px dashed #ccc; padding:20px; text-align:center; cursor:pointer;">
<span>Drop files here or click 'Choose files'</span> <span>Drop files here or click 'Choose files'</span>
<br> <br>
<input type="file" id="file" name="file[]" class="form-control-file" multiple required style="display:none;"> <input type="file" id="file" name="file[]" class="form-control-file" multiple required
style="display:none;">
</div> </div>
</div> </div>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button> <button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
@@ -73,65 +78,167 @@
<!-- Folder Management Card: 40% width --> <!-- Folder Management Card: 40% width -->
<div class="col-md-5 d-flex"> <div class="col-md-5 d-flex">
<div class="card flex-fill"> <div class="card flex-fill">
<div class="card-header">Folder Management</div> <div class="card-header">Folder Navigation & Management</div>
<div class="card-body"> <div class="card-body custom-folder-card-body">
<div class="form-group d-flex align-items-center" style="padding-top:15px;"> <div class="form-group d-flex align-items-top" style="padding-top:0px; margin-bottom:0;">
<select id="folderSelect" class="form-control"></select> <div id="folderTreeContainer"></div>
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
<i class="material-icons">edit</i>
</button>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
<i class="material-icons">delete</i>
</button>
</div> </div>
<button id="createFolderBtn" class="btn btn-primary mt-3">Create Folder</button> <button id="createFolderBtn" class="btn btn-primary mt-3">Create Folder</button>
<!-- Create Folder Modal -->
<div id="createFolderModal" class="modal">
<div class="modal-content">
<h4>Create Folder</h4>
<input type="text" id="newFolderName" class="form-control" placeholder="Enter folder name"
style="margin-top:10px;">
<div style="margin-top:15px; text-align:right;">
<button id="cancelCreateFolder" class="btn btn-secondary">Cancel</button>
<button id="submitCreateFolder" class="btn btn-primary">Create</button>
</div>
</div>
</div>
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
<i class="material-icons">edit</i>
</button>
<!-- Rename Folder Modal -->
<div id="renameFolderModal" class="modal">
<div class="modal-content">
<h4>Rename Folder</h4>
<input type="text" id="newRenameFolderName" class="form-control" placeholder="Enter new folder name"
style="margin-top:10px;">
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFolder" class="btn btn-secondary">Cancel</button>
<button id="submitRenameFolder" class="btn btn-primary">Rename</button>
</div>
</div>
</div>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
<i class="material-icons">delete</i>
</button>
<!-- Delete Folder Modal -->
<div id="deleteFolderModal" class="modal">
<div class="modal-content">
<h4>Delete Folder</h4>
<p id="deleteFolderMessage">Are you sure you want to delete this folder?</p>
<div style="margin-top:15px; text-align:right;">
<button id="cancelDeleteFolder" class="btn btn-secondary">Cancel</button>
<button id="confirmDeleteFolder" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
<div id="folderExplanation" style="
margin-top:15px;
font-size:12px;
color:#555;
background-color:#f9f9f9;
border:1px solid #ddd;
border-radius:4px;
padding:10px;
">
<ul style="margin: 0; padding-left:20px;">
<li>To view files in a folder, click on the folder name in the tree.</li>
<li>To create a subfolder, select a folder from the tree above and click "Create Folder".</li>
<li>To rename or delete a folder, first select it from the tree, then click "Rename Folder" or "Delete
Folder" respectively.</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- File List Section --> <!-- File List Section -->
<div id="fileListContainer"> <div id="fileListContainer">
<h2 id="fileListTitle">Files in (Root)</h2> <h2 id="fileListTitle">Files in (Root)</h2>
<div id="fileListActions" class="file-list-actions"> <div id="fileListActions" class="file-list-actions">
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Selected</button> <button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Selected</button>
<!-- Delete Files Modal -->
<div id="deleteFilesModal" class="modal">
<div class="modal-content">
<h4>Delete Selected Files</h4>
<p id="deleteFilesMessage">Are you sure you want to delete the selected files?</p>
<div style="margin-top:15px; text-align:right;">
<button id="cancelDeleteFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmDeleteFiles" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Selected</button> <button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Selected</button>
<!-- Copy Files Modal -->
<div id="copyFilesModal" class="modal">
<div class="modal-content">
<h4>Copy Selected Files</h4>
<p id="copyFilesMessage">Select a target folder for copying the selected files:</p>
<select id="copyTargetFolder" class="form-control" style="margin-top:10px;"></select>
<div style="margin-top:15px; text-align:right;">
<button id="cancelCopyFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmCopyFiles" class="btn btn-primary">Copy</button>
</div>
</div>
</div>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Selected</button> <button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Selected</button>
<select id="copyMoveFolderSelect" class="form-control folder-dropdown"></select> <!-- Move Files Modal -->
<div id="moveFilesModal" class="modal">
<div class="modal-content">
<h4>Move Selected Files</h4>
<p id="moveFilesMessage">Select a target folder for moving the selected files:</p>
<select id="moveTargetFolder" class="form-control" style="margin-top:10px;"></select>
<div style="margin-top:15px; text-align:right;">
<button id="cancelMoveFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmMoveFiles" class="btn btn-primary">Move</button>
</div>
</div>
</div>
<select id="copyMoveFolderSelect" style="display: none;"></select>
</div> </div>
<div id="fileList"></div> <div id="fileList"></div>
</div> </div>
</div> </div>
<!-- Add User Modal --> <!-- Add User Modal -->
<div id="addUserModal" class="modal"> <div id="addUserModal" class="modal">
<h3>Create New User</h3> <div class="modal-content">
<label for="newUsername">Username:</label> <h3>Create New User</h3>
<input type="text" id="newUsername" class="form-control"> <label for="newUsername">Username:</label>
<label for="newPassword">Password:</label> <input type="text" id="newUsername" class="form-control">
<input type="password" id="newPassword" class="form-control"> <label for="newPassword">Password:</label>
<div id="adminCheckboxContainer"> <input type="password" id="newPassword" class="form-control">
<input type="checkbox" id="isAdmin"> <div id="adminCheckboxContainer">
<label for="isAdmin">Grant Admin Access</label> <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> </div>
<button id="saveUserBtn" class="btn btn-primary">Save User</button>
<button id="cancelUserBtn" class="btn btn-secondary">Cancel</button>
</div> </div>
<!-- Remove User Modal --> <!-- Remove User Modal -->
<div id="removeUserModal" class="modal"> <div id="removeUserModal" class="modal">
<h3>Remove User</h3> <div class="modal-content">
<label for="removeUsernameSelect">Select a user to remove:</label> <h3>Remove User</h3>
<select id="removeUsernameSelect" class="form-control"></select> <label for="removeUsernameSelect">Select a user to remove:</label>
<button id="deleteUserBtn" class="btn btn-danger">Delete User</button> <select id="removeUsernameSelect" class="form-control"></select>
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button> <button id="deleteUserBtn" class="btn btn-danger">Delete User</button>
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button>
</div>
</div> </div>
</div>
<!-- Rename File Modal -->
<!-- JavaScript Files --> <div id="renameFileModal" class="modal">
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> <div class="modal-content">
<script type="module" src="main.js"></script> <h4>Rename File</h4>
<input type="text" id="newFileName" class="form-control" placeholder="Enter new file name"
style="margin-top:10px;">
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFile" class="btn btn-secondary">Cancel</button>
<button id="submitRenameFile" class="btn btn-primary">Rename</button>
</div>
</div>
</div>
<!-- JavaScript Files -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script type="module" src="main.js"></script>
</body> </body>
</html> </html>

34
main.js
View File

@@ -4,7 +4,8 @@ import { sendRequest } from './networkUtils.js';
import { import {
toggleVisibility, toggleVisibility,
toggleAllCheckboxes, toggleAllCheckboxes,
updateFileActionButtons updateFileActionButtons,
showToast
} from './domUtils.js'; } from './domUtils.js';
import { import {
loadFileList, loadFileList,
@@ -12,12 +13,12 @@ import {
editFile, editFile,
saveFile, saveFile,
displayFilePreview, displayFilePreview,
renameFile renameFile
} from './fileManager.js'; } from './fileManager.js';
import { import {
deleteFolder, loadFolderTree,
loadCopyMoveFolderList, loadCopyMoveFolderList,
loadFolderList loadFolderList,
} from './folderManager.js'; } from './folderManager.js';
import { initUpload } from './upload.js'; import { initUpload } from './upload.js';
import { initAuth } from './auth.js'; import { initAuth } from './auth.js';
@@ -35,14 +36,19 @@ window.renameFile = renameFile;
window.currentFolder = "root"; window.currentFolder = "root";
// DOMContentLoaded initialization. // DOMContentLoaded initialization.
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Initialize authentication and user management. // Call initAuth synchronously.
initAuth(); initAuth();
window.currentFolder = window.currentFolder || "root"; const message = sessionStorage.getItem("welcomeMessage");
window.updateFileActionButtons = updateFileActionButtons; if (message) {
loadFileList(window.currentFolder); showToast(message);
loadCopyMoveFolderList(); sessionStorage.removeItem("welcomeMessage");
initFileActions(); }
initUpload(); window.currentFolder = "root";
loadFolderList(); window.updateFileActionButtons = updateFileActionButtons;
}); loadFileList(window.currentFolder);
initFileActions();
initUpload();
loadFolderTree();
});

View File

@@ -10,18 +10,43 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
} }
$data = json_decode(file_get_contents("php://input"), true); $data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['source']) || !isset($data['destination']) || !isset($data['files'])) { if (
!$data ||
!isset($data['source']) ||
!isset($data['destination']) ||
!isset($data['files'])
) {
echo json_encode(["error" => "Invalid request"]); echo json_encode(["error" => "Invalid request"]);
exit; exit;
} }
$sourceFolder = trim($data['source']); // Get and trim folder parameters.
$destinationFolder = trim($data['destination']); $sourceFolder = trim($data['source']) ?: 'root';
$files = $data['files']; $destinationFolder = trim($data['destination']) ?: 'root';
// Allow only letters, numbers, underscores, dashes, spaces, and forward slashes in folder names.
$folderPattern = '/^[A-Za-z0-9_\- \/]+$/';
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
echo json_encode(["error" => "Invalid source folder name."]);
exit;
}
if ($destinationFolder !== 'root' && !preg_match($folderPattern, $destinationFolder)) {
echo json_encode(["error" => "Invalid destination folder name."]);
exit;
}
// Remove any leading/trailing slashes.
$sourceFolder = trim($sourceFolder, "/\\ ");
$destinationFolder = trim($destinationFolder, "/\\ ");
// Build the source and destination directories. // Build the source and destination directories.
$sourceDir = ($sourceFolder === 'root') ? UPLOAD_DIR : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR; $baseDir = rtrim(UPLOAD_DIR, '/\\');
$destDir = ($destinationFolder === 'root') ? UPLOAD_DIR : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR; $sourceDir = ($sourceFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR;
$destDir = ($destinationFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
// Load metadata. // Load metadata.
$metadataFile = META_DIR . META_FILE; $metadataFile = META_DIR . META_FILE;
@@ -36,10 +61,20 @@ if (!is_dir($destDir)) {
} }
$errors = []; $errors = [];
foreach ($files as $fileName) { // Define a safe pattern for file names: letters, numbers, underscores, dashes, dots, and spaces.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
foreach ($data['files'] as $fileName) {
$basename = basename($fileName); $basename = basename($fileName);
// Validate file name.
if (!preg_match($safeFileNamePattern, $basename)) {
$errors[] = "$basename has invalid characters.";
continue;
}
$srcPath = $sourceDir . $basename; $srcPath = $sourceDir . $basename;
$destPath = $destDir . $basename; $destPath = $destDir . $basename;
// Build metadata keys. // Build metadata keys.
$srcKey = ($sourceFolder === 'root') ? $basename : $sourceFolder . "/" . $basename; $srcKey = ($sourceFolder === 'root') ? $basename : $sourceFolder . "/" . $basename;
$destKey = ($destinationFolder === 'root') ? $basename : $destinationFolder . "/" . $basename; $destKey = ($destinationFolder === 'root') ? $basename : $destinationFolder . "/" . $basename;
@@ -52,7 +87,7 @@ foreach ($files as $fileName) {
$errors[] = "Failed to move $basename"; $errors[] = "Failed to move $basename";
continue; continue;
} }
// Update metadata: if source key exists, copy it to destination key then remove source key. // Update metadata: copy source metadata to destination key and remove source key.
if (isset($metadata[$srcKey])) { if (isset($metadata[$srcKey])) {
$metadata[$destKey] = $metadata[$srcKey]; $metadata[$destKey] = $metadata[$srcKey];
unset($metadata[$srcKey]); unset($metadata[$srcKey]);
@@ -68,4 +103,4 @@ if (empty($errors)) {
} else { } else {
echo json_encode(["error" => implode("; ", $errors)]); echo json_encode(["error" => implode("; ", $errors)]);
} }
?> ?>

View File

@@ -22,6 +22,12 @@ if (!$usernameToRemove) {
exit; exit;
} }
// Optional: Validate the username format (allow letters, numbers, underscores, dashes, and spaces)
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $usernameToRemove)) {
echo json_encode(["error" => "Invalid username format"]);
exit;
}
// Prevent removal of the currently logged-in user // Prevent removal of the currently logged-in user
if (isset($_SESSION['username']) && $_SESSION['username'] === $usernameToRemove) { if (isset($_SESSION['username']) && $_SESSION['username'] === $usernameToRemove) {
echo json_encode(["error" => "Cannot remove yourself"]); echo json_encode(["error" => "Cannot remove yourself"]);
@@ -60,4 +66,4 @@ if (!$userFound) {
// Write the updated list back to users.txt // Write the updated list back to users.txt
file_put_contents($usersFile, implode(PHP_EOL, $newUsers) . PHP_EOL); file_put_contents($usersFile, implode(PHP_EOL, $newUsers) . PHP_EOL);
echo json_encode(["success" => "User removed successfully"]); echo json_encode(["success" => "User removed successfully"]);
?> ?>

View File

@@ -19,9 +19,22 @@ if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($dat
} }
$folder = trim($data['folder']) ?: 'root'; $folder = trim($data['folder']) ?: 'root';
// For subfolders, allow letters, numbers, underscores, dashes, spaces, and forward slashes.
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
exit;
}
$oldName = basename(trim($data['oldName'])); $oldName = basename(trim($data['oldName']));
$newName = basename(trim($data['newName'])); $newName = basename(trim($data['newName']));
// Validate file names: allow letters, numbers, underscores, dashes, dots, and spaces.
if (!preg_match('/^[A-Za-z0-9_\-\. ]+$/', $oldName) || !preg_match('/^[A-Za-z0-9_\-\. ]+$/', $newName)) {
echo json_encode(["error" => "Invalid file name."]);
exit;
}
// Determine the directory path based on the folder.
if ($folder !== 'root') { if ($folder !== 'root') {
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; $directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
} else { } else {
@@ -47,7 +60,7 @@ if (rename($oldPath, $newPath)) {
// Update metadata. // Update metadata.
if (file_exists($metadataFile)) { if (file_exists($metadataFile)) {
$metadata = json_decode(file_get_contents($metadataFile), true); $metadata = json_decode(file_get_contents($metadataFile), true);
// Build the keys. // Build metadata keys using the folder (if not root).
$oldKey = ($folder !== 'root') ? $folder . "/" . $oldName : $oldName; $oldKey = ($folder !== 'root') ? $folder . "/" . $oldName : $oldName;
$newKey = ($folder !== 'root') ? $folder . "/" . $newName : $newName; $newKey = ($folder !== 'root') ? $folder . "/" . $newName : $newName;
if (isset($metadata[$oldKey])) { if (isset($metadata[$oldKey])) {
@@ -60,4 +73,4 @@ if (rename($oldPath, $newPath)) {
} else { } else {
echo json_encode(["error" => "Error renaming file"]); echo json_encode(["error" => "Error renaming file"]);
} }
?> ?>

View File

@@ -1,6 +1,9 @@
<?php <?php
require 'config.php'; require 'config.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
// Ensure user is authenticated // Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
@@ -25,31 +28,45 @@ if (!isset($input['oldFolder']) || !isset($input['newFolder'])) {
$oldFolder = trim($input['oldFolder']); $oldFolder = trim($input['oldFolder']);
$newFolder = trim($input['newFolder']); $newFolder = trim($input['newFolder']);
// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces // Allow letters, numbers, underscores, dashes, spaces, and forward slashes
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $oldFolder) || !preg_match('/^[A-Za-z0-9_\- ]+$/', $newFolder)) { if (!preg_match('/^[A-Za-z0-9_\- \/]+$/', $oldFolder) || !preg_match('/^[A-Za-z0-9_\- \/]+$/', $newFolder)) {
echo json_encode(['success' => false, 'error' => 'Invalid folder name(s).']); echo json_encode(['success' => false, 'error' => 'Invalid folder name(s).']);
exit; exit;
} }
$oldPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $oldFolder; // Trim any leading/trailing slashes and spaces.
$newPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $newFolder; $oldFolder = trim($oldFolder, "/\\ ");
$newFolder = trim($newFolder, "/\\ ");
// Check if the folder to rename exists // Build full paths relative to UPLOAD_DIR.
$baseDir = rtrim(UPLOAD_DIR, '/\\');
$oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder;
$newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder;
// Security check: ensure both paths are within the base directory.
if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
strpos(realpath($oldPath), realpath($baseDir)) !== 0 ||
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0) {
echo json_encode(['success' => false, 'error' => 'Invalid folder path.']);
exit;
}
// Check if the folder to rename exists.
if (!file_exists($oldPath) || !is_dir($oldPath)) { if (!file_exists($oldPath) || !is_dir($oldPath)) {
echo json_encode(['success' => false, 'error' => 'Folder to rename does not exist.']); echo json_encode(['success' => false, 'error' => 'Folder to rename does not exist.']);
exit; exit;
} }
// Check if the new folder name already exists // Check if the new folder name already exists.
if (file_exists($newPath)) { if (file_exists($newPath)) {
echo json_encode(['success' => false, 'error' => 'New folder name already exists.']); echo json_encode(['success' => false, 'error' => 'New folder name already exists.']);
exit; exit;
} }
// Attempt to rename the folder // Attempt to rename the folder.
if (rename($oldPath, $newPath)) { if (rename($oldPath, $newPath)) {
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
} else { } else {
echo json_encode(['success' => false, 'error' => 'Failed to rename folder.']); echo json_encode(['success' => false, 'error' => 'Failed to rename folder.']);
} }
?> ?>

View File

@@ -26,18 +26,41 @@ $fileName = basename($data["fileName"]);
// Determine the folder. Default to "root" if not provided. // Determine the folder. Default to "root" if not provided.
$folder = isset($data["folder"]) ? trim($data["folder"]) : "root"; $folder = isset($data["folder"]) ? trim($data["folder"]) : "root";
if ($folder !== "root") {
$targetDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; // If a subfolder is provided, validate it.
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
if ($folder !== "root" && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
exit;
}
// Trim any leading/trailing slashes or spaces.
$folder = trim($folder, "/\\ ");
// Determine the target upload directory.
$baseDir = rtrim(UPLOAD_DIR, '/\\');
if ($folder && strtolower($folder) !== "root") {
$targetDir = $baseDir . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
} else { } else {
$targetDir = UPLOAD_DIR; $targetDir = $baseDir . DIRECTORY_SEPARATOR;
}
// (Optional security check: Ensure $targetDir starts with $baseDir)
if (strpos(realpath($targetDir), realpath($baseDir)) !== 0) {
echo json_encode(["error" => "Invalid folder path"]);
exit;
}
if (!is_dir($targetDir)) {
mkdir($targetDir, 0775, true);
} }
$filePath = $targetDir . $fileName; $filePath = $targetDir . $fileName;
// Try to save the file. // Attempt to save the file.
if (file_put_contents($filePath, $data["content"]) !== false) { if (file_put_contents($filePath, $data["content"]) !== false) {
echo json_encode(["success" => "File saved successfully"]); echo json_encode(["success" => "File saved successfully"]);
} else { } else {
echo json_encode(["error" => "Error saving file"]); echo json_encode(["error" => "Error saving file"]);
} }
?> ?>

View File

@@ -88,22 +88,30 @@ header {
/* MODALS & EDITOR MODALS */ /* MODALS & EDITOR MODALS */
.modal { .modal {
display: none; display: none; /* Hidden by default */
position: fixed; position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1050;
}
.modal .modal-content {
position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(75%, 75%); transform: translate(-50%, -50%);
/* centers the modal */ background: #fff;
background: white;
padding: 20px; padding: 20px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 1000; max-width: 400px;
width: 40vw; width: 90%;
max-width: 40vw; max-height: 90vh;
height: 600px; overflow-y: auto;
max-height: 35vh;
} }
.editor-modal { .editor-modal {
@@ -111,6 +119,10 @@ header {
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(5%, 10%); transform: translate(5%, 10%);
background-color: #fff; /* Ensures modal is opaque */
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
/* centers the editor modal */ /* centers the editor modal */
width: 50vw; width: 50vw;
max-width: 90vw; max-width: 90vw;
@@ -119,6 +131,8 @@ header {
max-height: 80vh; max-height: 80vh;
overflow: auto; overflow: auto;
resize: both; resize: both;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1100; /* make sure its on top of any overlay */
} }
/* LOGOUT & USER BUTTON CONTAINER */ /* LOGOUT & USER BUTTON CONTAINER */
@@ -402,3 +416,34 @@ label {
background-color: #d5d5d5; background-color: #d5d5d5;
} }
.folder-option:hover {
background-color: #f0f0f0;
}
.folder-option.selected {
background-color: #d0d0d0;
}
.custom-folder-card-body {
padding-top: 5px !important;
/* You can leave the other padding values as default or specify them if needed */
}
#customToast {
position: fixed;
top: 20px;
right: 20px;
background: #333;
color: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
opacity: 0;
transition: opacity 0.5s ease;
z-index: 9999; /* Increased z-index */
min-width: 250px;
display: none;
}
#customToast.show {
opacity: 1;
}

View File

@@ -1,6 +1,8 @@
// upload.js // upload.js
import { loadFileList, displayFilePreview, initFileActions } from './fileManager.js'; import { loadFileList, displayFilePreview, initFileActions } from './fileManager.js';
import { showToast } from './domUtils.js';
export function initUpload() { export function initUpload() {
const fileInput = document.getElementById("file"); const fileInput = document.getElementById("file");
@@ -108,7 +110,7 @@ export function initUpload() {
list.style.padding = "0"; list.style.padding = "0";
allFiles.forEach((file, index) => { allFiles.forEach((file, index) => {
const li = document.createElement("li"); const li = document.createElement("li");
li.style.paddingTop = "20px"; li.style.paddingTop = "10px";
li.style.marginBottom = "10px"; li.style.marginBottom = "10px";
li.style.display = (index < maxDisplay) ? "flex" : "none"; li.style.display = (index < maxDisplay) ? "flex" : "none";
li.style.alignItems = "center"; li.style.alignItems = "center";
@@ -144,7 +146,7 @@ export function initUpload() {
}); });
if (allFiles.length > maxDisplay) { if (allFiles.length > maxDisplay) {
const extra = document.createElement("li"); const extra = document.createElement("li");
extra.style.paddingTop = "20px"; extra.style.paddingTop = "10px";
extra.style.marginBottom = "10px"; extra.style.marginBottom = "10px";
extra.textContent = `Uploading additional ${allFiles.length - maxDisplay} file(s)...`; extra.textContent = `Uploading additional ${allFiles.length - maxDisplay} file(s)...`;
extra.style.display = "flex"; extra.style.display = "flex";
@@ -161,7 +163,7 @@ export function initUpload() {
e.preventDefault(); e.preventDefault();
const files = fileInput.files; const files = fileInput.files;
if (files.length === 0) { if (files.length === 0) {
alert("No files selected."); showToast("No files selected.");
return; return;
} }
const allFiles = Array.from(files); const allFiles = Array.from(files);
@@ -274,12 +276,12 @@ export function initUpload() {
if (dropArea) setDropAreaDefault(); if (dropArea) setDropAreaDefault();
}, 10000); }, 10000);
if (!allSucceeded) { if (!allSucceeded) {
alert("Some files failed to upload. Please check the list."); showToast("Some files failed to upload. Please check the list.");
} }
}) })
.catch(error => { .catch(error => {
console.error("Error fetching file list:", error); console.error("Error fetching file list:", error);
alert("Some files may have failed to upload. Please check the list."); showToast("Some files may have failed to upload. Please check the list.");
}); });
} }
}); });

View File

@@ -9,9 +9,10 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
exit; exit;
} }
// Validate folder name input. Only allow letters, numbers, underscores, dashes, and spaces. // Validate folder name input. Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root'; $folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- ]+$/', $folder)) { // When folder is not 'root', allow "/" in the folder name to denote subfolders.
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
echo json_encode(["error" => "Invalid folder name"]); echo json_encode(["error" => "Invalid folder name"]);
exit; exit;
} }
@@ -21,6 +22,7 @@ $uploadDir = UPLOAD_DIR;
if ($folder !== 'root') { if ($folder !== 'root') {
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
if (!is_dir($uploadDir)) { if (!is_dir($uploadDir)) {
// Recursively create subfolders as needed.
mkdir($uploadDir, 0775, true); mkdir($uploadDir, 0775, true);
} }
} else { } else {
@@ -71,4 +73,4 @@ if ($metadataChanged) {
} }
echo json_encode(["success" => "Files uploaded successfully"]); echo json_encode(["success" => "Files uploaded successfully"]);
?> ?>