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

40
auth.js
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,215 +1,381 @@
// folderManager.js
import {
loadFileList
} from './fileManager.js';
export function renameFolder() {
const folderSelect = document.getElementById("folderSelect");
const selectedFolder = folderSelect.value;
const newFolderName = prompt("Enter the new folder name:", selectedFolder);
if (newFolderName && newFolderName !== selectedFolder) {
fetch("renameFolder.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ oldName: selectedFolder, newName: newFolderName })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("Folder renamed successfully!");
loadFolderList("root");
} else {
alert("Error: " + (data.error || "Could not rename folder"));
}
})
.catch(error => console.error("Error renaming folder:", error));
}
}
export function deleteFolder() {
const folderSelect = document.getElementById("folderSelect");
const selectedFolder = folderSelect.value;
if (!selectedFolder || selectedFolder === "root") {
alert("Please select a valid folder to delete.");
return;
}
// Only prompt once.
if (!confirm("Are you sure you want to delete folder " + selectedFolder + "?")) {
return;
}
// Proceed with deletion.
fetch("deleteFolder.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ folder: selectedFolder })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("Folder deleted successfully!");
// Refresh both folder dropdowns.
loadFolderList("root");
loadCopyMoveFolderList();
} else {
alert("Error: " + (data.error || "Could not delete folder"));
}
})
.catch(error => console.error("Error deleting folder:", error));
}
// Updates the copy/move folder dropdown.
export async function loadCopyMoveFolderList() {
try {
const response = await fetch('getFolderList.php');
const folders = await response.json();
const folderSelect = document.getElementById('copyMoveFolderSelect');
folderSelect.innerHTML = ''; // Clear existing options
// Always add a "Root" option as the default.
const rootOption = document.createElement('option');
rootOption.value = 'root';
rootOption.textContent = '(Root)';
folderSelect.appendChild(rootOption);
if (Array.isArray(folders) && folders.length > 0) {
folders.forEach(folder => {
const option = document.createElement('option');
option.value = folder;
option.textContent = folder;
folderSelect.appendChild(option);
});
}
} catch (error) {
console.error('Error loading folder list:', error);
import { loadFileList } from './fileManager.js';
import { showToast } from './domUtils.js';
// ----------------------
// Helper functions
// ----------------------
// Format folder name for display (for copy/move dropdown).
export function formatFolderName(folder) {
if (folder.indexOf("/") !== -1) {
let parts = folder.split("/");
let indent = "";
for (let i = 1; i < parts.length; i++) {
indent += "\u00A0\u00A0\u00A0\u00A0"; // 4 non-breaking spaces per level
}
return indent + parts[parts.length - 1];
} else {
return folder;
}
// Optional helper to load folder lists (alias for loadCopyMoveFolderList).
export function loadFolderList(selectedFolder) {
const folderSelect = document.getElementById("folderSelect");
folderSelect.innerHTML = "";
const rootOption = document.createElement("option");
rootOption.value = "root";
rootOption.textContent = "(Root)";
folderSelect.appendChild(rootOption);
fetch("getFolderList.php")
.then(response => response.json())
.then(folders => {
folders.forEach(function (folder) {
let option = document.createElement("option");
option.value = folder;
option.textContent = folder;
folderSelect.appendChild(option);
});
// Set the selected folder if provided, else default to "root"
if (selectedFolder && [...folderSelect.options].some(opt => opt.value === selectedFolder)) {
folderSelect.value = selectedFolder;
} else {
folderSelect.value = "root";
}
// Update global currentFolder and title, then load the file list
window.currentFolder = folderSelect.value;
document.getElementById("fileListTitle").textContent =
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")";
loadFileList(window.currentFolder);
})
.catch(error => console.error("Error loading folder list:", error));
}
// Build a tree structure from a flat array of folder paths.
function buildFolderTree(folders) {
const tree = {};
folders.forEach(folderPath => {
const parts = folderPath.split('/');
let current = tree;
parts.forEach(part => {
if (!current[part]) {
current[part] = {};
}
current = current[part];
});
});
return tree;
}
/**
* Render the folder tree as nested <ul> elements with toggle icons.
* @param {object} tree - The tree object.
* @param {string} parentPath - The path prefix.
* @param {string} defaultDisplay - "block" (open) or "none" (collapsed)
*/
function renderFolderTree(tree, parentPath = "", defaultDisplay = "none") {
let html = `<ul style="list-style-type:none; padding-left:20px; margin:0; display:${defaultDisplay};">`;
for (const folder in tree) {
const fullPath = parentPath ? parentPath + "/" + folder : folder;
const hasChildren = Object.keys(tree[folder]).length > 0;
html += `<li style="margin:4px 0; display:block;">`;
if (hasChildren) {
// For nested levels (below root) default to collapsed: toggle label "[+]"
html += `<span class="folder-toggle" style="cursor:pointer; margin-right:5px;">[+]</span>`;
} else {
html += `<span style="display:inline-block; width:18px;"></span>`;
}
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
document.getElementById("folderSelect").addEventListener("change", function () {
window.currentFolder = this.value;
document.getElementById("fileListTitle").textContent =
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")";
loadFileList(window.currentFolder);
const container = document.getElementById("folderTreeContainer");
if (!container) return;
const tree = buildFolderTree(folders);
// 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
document.getElementById("createFolderBtn").addEventListener("click", function () {
let folderName = prompt("Enter folder name:");
if (folderName) {
fetch("createFolder.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ folder: folderName })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("Folder created successfully!");
loadFolderList(folderName);
loadCopyMoveFolderList();
} else {
alert("Error: " + (data.error || "Could not create folder"));
}
})
.catch(error => console.error("Error creating folder:", error));
}
});
const rootToggle = container.querySelector("#rootRow .folder-toggle");
if (rootToggle) {
rootToggle.addEventListener("click", function(e) {
e.stopPropagation();
const nestedUl = container.querySelector("#rootRow + ul");
if (nestedUl) {
if (nestedUl.style.display === "none" || nestedUl.style.display === "") {
nestedUl.style.display = "block";
this.textContent = "[-]";
} else {
nestedUl.style.display = "none";
this.textContent = "[+]";
}
}
});
}
// Event listener for renaming a folder
document.getElementById("renameFolderBtn").addEventListener("click", function () {
const folderSelect = document.getElementById("folderSelect");
const selectedFolder = folderSelect.value;
if (!selectedFolder || selectedFolder === "root") {
alert("Please select a valid folder to rename.");
return;
}
let newFolderName = prompt("Enter new folder name for '" + selectedFolder + "':", selectedFolder);
if (newFolderName && newFolderName !== selectedFolder) {
fetch("renameFolder.php", {
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));
}
container.querySelectorAll(".folder-toggle").forEach(toggle => {
toggle.addEventListener("click", function(e) {
e.stopPropagation();
const siblingUl = this.parentNode.querySelector("ul");
if (siblingUl) {
if (siblingUl.style.display === "none" || siblingUl.style.display === "") {
siblingUl.style.display = "block";
this.textContent = "[-]";
} else {
siblingUl.style.display = "none";
this.textContent = "[+]";
}
}
});
});
// Event listener for deleting a folder
document.getElementById("deleteFolderBtn").addEventListener("click", function () {
const folderSelect = document.getElementById("folderSelect");
const selectedFolder = folderSelect.value;
if (!selectedFolder || selectedFolder === "root") {
alert("Please select a valid folder to delete.");
return;
}
if (confirm("Are you sure you want to delete folder " + selectedFolder + "?")) {
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!");
loadFolderList("root");
} else {
alert("Error: " + (data.error || "Could not delete folder"));
}
})
.catch(error => console.error("Error deleting folder:", error));
} catch (error) {
console.error("Error loading folder tree:", error);
}
}
// For backward compatibility.
export function loadFolderList(selectedFolder) {
loadFolderTree(selectedFolder);
}
// ----------------------
// Folder Management Functions
// ----------------------
// Attach event listeners for Rename and Delete buttons.
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
function openRenameFolderModal() {
const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to rename.");
return;
}
// 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';
// 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') {
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
} else {
@@ -30,12 +38,20 @@ if (!is_dir($directory)) {
$files = array_values(array_diff(scandir($directory), array('.', '..')));
$fileList = [];
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, and spaces.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
foreach ($files as $file) {
$filePath = $directory . DIRECTORY_SEPARATOR . $file;
// Only include files (skip directories)
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;
$fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown";
@@ -63,4 +79,4 @@ foreach ($files as $file) {
}
echo json_encode(["files" => $fileList]);
?>
?>

View File

@@ -9,17 +9,43 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
exit;
}
$folderList = [];
$dir = rtrim(UPLOAD_DIR, '/\\');
if (is_dir($dir)) {
foreach (scandir($dir) as $item) {
/**
* Recursively scan a directory for subfolders.
*
* @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;
// Only process folder names that match the safe pattern.
if (!preg_match($safeFolderNamePattern, $item)) {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $item;
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);
?>
?>

View File

@@ -13,7 +13,10 @@ if (file_exists($usersFile)) {
foreach ($lines as $line) {
$parts = explode(':', trim($line));
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>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -14,6 +15,7 @@
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<!-- Header -->
<header>
@@ -29,7 +31,8 @@
<button id="removeUserBtn" title="Remove User"><i class="material-icons">person_remove</i></button>
</div>
</header>
<!-- Custom Toast Container -->
<div id="customToast"></div>
<div class="container">
<!-- Login Form -->
<div class="row" id="loginForm">
@@ -47,7 +50,7 @@
</form>
</div>
</div>
<!-- Main Operations: Upload and Folder Management -->
<div id="mainOperations" style="display: none;">
<div class="row" id="uploadFolderRow">
@@ -58,10 +61,12 @@
<div class="card-body">
<form id="uploadFileForm" method="post" enctype="multipart/form-data">
<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>
<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>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
@@ -73,65 +78,167 @@
<!-- Folder Management Card: 40% width -->
<div class="col-md-5 d-flex">
<div class="card flex-fill">
<div class="card-header">Folder Management</div>
<div class="card-body">
<div class="form-group d-flex align-items-center" style="padding-top:15px;">
<select id="folderSelect" class="form-control"></select>
<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 class="card-header">Folder Navigation & Management</div>
<div class="card-body custom-folder-card-body">
<div class="form-group d-flex align-items-top" style="padding-top:0px; margin-bottom:0;">
<div id="folderTreeContainer"></div>
</div>
<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>
<!-- File List Section -->
<div id="fileListContainer">
<h2 id="fileListTitle">Files in (Root)</h2>
<div id="fileListActions" class="file-list-actions">
<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>
<!-- 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>
<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 id="fileList"></div>
</div>
</div>
<!-- Add User Modal -->
<div id="addUserModal" class="modal">
<h3>Create New User</h3>
<label for="newUsername">Username:</label>
<input type="text" id="newUsername" class="form-control">
<label for="newPassword">Password:</label>
<input type="password" id="newPassword" class="form-control">
<div id="adminCheckboxContainer">
<input type="checkbox" id="isAdmin">
<label for="isAdmin">Grant Admin Access</label>
<div class="modal-content">
<h3>Create New User</h3>
<label for="newUsername">Username:</label>
<input type="text" id="newUsername" class="form-control">
<label for="newPassword">Password:</label>
<input type="password" id="newPassword" class="form-control">
<div id="adminCheckboxContainer">
<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>
<button id="saveUserBtn" class="btn btn-primary">Save User</button>
<button id="cancelUserBtn" class="btn btn-secondary">Cancel</button>
</div>
<!-- Remove User Modal -->
<div id="removeUserModal" class="modal">
<h3>Remove User</h3>
<label for="removeUsernameSelect">Select a user to remove:</label>
<select id="removeUsernameSelect" class="form-control"></select>
<button id="deleteUserBtn" class="btn btn-danger">Delete User</button>
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button>
<div class="modal-content">
<h3>Remove User</h3>
<label for="removeUsernameSelect">Select a user to remove:</label>
<select id="removeUsernameSelect" class="form-control"></select>
<button id="deleteUserBtn" class="btn btn-danger">Delete User</button>
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
<!-- JavaScript Files -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script type="module" src="main.js"></script>
<!-- Rename File Modal -->
<div id="renameFileModal" class="modal">
<div class="modal-content">
<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>
</html>
</html>

34
main.js
View File

@@ -4,7 +4,8 @@ import { sendRequest } from './networkUtils.js';
import {
toggleVisibility,
toggleAllCheckboxes,
updateFileActionButtons
updateFileActionButtons,
showToast
} from './domUtils.js';
import {
loadFileList,
@@ -12,12 +13,12 @@ import {
editFile,
saveFile,
displayFilePreview,
renameFile
renameFile
} from './fileManager.js';
import {
deleteFolder,
loadFolderTree,
loadCopyMoveFolderList,
loadFolderList
loadFolderList,
} from './folderManager.js';
import { initUpload } from './upload.js';
import { initAuth } from './auth.js';
@@ -35,14 +36,19 @@ window.renameFile = renameFile;
window.currentFolder = "root";
// DOMContentLoaded initialization.
document.addEventListener("DOMContentLoaded", function () {
// Initialize authentication and user management.
initAuth();
window.currentFolder = window.currentFolder || "root";
window.updateFileActionButtons = updateFileActionButtons;
loadFileList(window.currentFolder);
loadCopyMoveFolderList();
initFileActions();
initUpload();
loadFolderList();
});
// Call initAuth synchronously.
initAuth();
const message = sessionStorage.getItem("welcomeMessage");
if (message) {
showToast(message);
sessionStorage.removeItem("welcomeMessage");
}
window.currentFolder = "root";
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);
if (!$data || !isset($data['source']) || !isset($data['destination']) || !isset($data['files'])) {
if (
!$data ||
!isset($data['source']) ||
!isset($data['destination']) ||
!isset($data['files'])
) {
echo json_encode(["error" => "Invalid request"]);
exit;
}
$sourceFolder = trim($data['source']);
$destinationFolder = trim($data['destination']);
$files = $data['files'];
// Get and trim folder parameters.
$sourceFolder = trim($data['source']) ?: 'root';
$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.
$sourceDir = ($sourceFolder === 'root') ? UPLOAD_DIR : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR;
$destDir = ($destinationFolder === 'root') ? UPLOAD_DIR : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
$baseDir = rtrim(UPLOAD_DIR, '/\\');
$sourceDir = ($sourceFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $sourceFolder . DIRECTORY_SEPARATOR;
$destDir = ($destinationFolder === 'root')
? $baseDir . DIRECTORY_SEPARATOR
: $baseDir . DIRECTORY_SEPARATOR . $destinationFolder . DIRECTORY_SEPARATOR;
// Load metadata.
$metadataFile = META_DIR . META_FILE;
@@ -36,10 +61,20 @@ if (!is_dir($destDir)) {
}
$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);
// Validate file name.
if (!preg_match($safeFileNamePattern, $basename)) {
$errors[] = "$basename has invalid characters.";
continue;
}
$srcPath = $sourceDir . $basename;
$destPath = $destDir . $basename;
// Build metadata keys.
$srcKey = ($sourceFolder === 'root') ? $basename : $sourceFolder . "/" . $basename;
$destKey = ($destinationFolder === 'root') ? $basename : $destinationFolder . "/" . $basename;
@@ -52,7 +87,7 @@ foreach ($files as $fileName) {
$errors[] = "Failed to move $basename";
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])) {
$metadata[$destKey] = $metadata[$srcKey];
unset($metadata[$srcKey]);
@@ -68,4 +103,4 @@ if (empty($errors)) {
} else {
echo json_encode(["error" => implode("; ", $errors)]);
}
?>
?>

View File

@@ -22,6 +22,12 @@ if (!$usernameToRemove) {
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
if (isset($_SESSION['username']) && $_SESSION['username'] === $usernameToRemove) {
echo json_encode(["error" => "Cannot remove yourself"]);
@@ -60,4 +66,4 @@ if (!$userFound) {
// Write the updated list back to users.txt
file_put_contents($usersFile, implode(PHP_EOL, $newUsers) . PHP_EOL);
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';
// 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']));
$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') {
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
} else {
@@ -47,7 +60,7 @@ if (rename($oldPath, $newPath)) {
// Update metadata.
if (file_exists($metadataFile)) {
$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;
$newKey = ($folder !== 'root') ? $folder . "/" . $newName : $newName;
if (isset($metadata[$oldKey])) {
@@ -60,4 +73,4 @@ if (rename($oldPath, $newPath)) {
} else {
echo json_encode(["error" => "Error renaming file"]);
}
?>
?>

View File

@@ -1,6 +1,9 @@
<?php
require 'config.php';
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
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
@@ -25,31 +28,45 @@ if (!isset($input['oldFolder']) || !isset($input['newFolder'])) {
$oldFolder = trim($input['oldFolder']);
$newFolder = trim($input['newFolder']);
// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $oldFolder) || !preg_match('/^[A-Za-z0-9_\- ]+$/', $newFolder)) {
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes
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).']);
exit;
}
$oldPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $oldFolder;
$newPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $newFolder;
// Trim any leading/trailing slashes and spaces.
$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)) {
echo json_encode(['success' => false, 'error' => 'Folder to rename does not exist.']);
exit;
}
// Check if the new folder name already exists
// Check if the new folder name already exists.
if (file_exists($newPath)) {
echo json_encode(['success' => false, 'error' => 'New folder name already exists.']);
exit;
}
// Attempt to rename the folder
// Attempt to rename the folder.
if (rename($oldPath, $newPath)) {
echo json_encode(['success' => true]);
} else {
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.
$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 {
$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;
// Try to save the file.
// Attempt to save the file.
if (file_put_contents($filePath, $data["content"]) !== false) {
echo json_encode(["success" => "File saved successfully"]);
} else {
echo json_encode(["error" => "Error saving file"]);
}
?>
?>

View File

@@ -88,22 +88,30 @@ header {
/* MODALS & EDITOR MODALS */
.modal {
display: none;
display: none; /* Hidden by default */
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%;
left: 50%;
transform: translate(75%, 75%);
/* centers the modal */
background: white;
transform: translate(-50%, -50%);
background: #fff;
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
width: 40vw;
max-width: 40vw;
height: 600px;
max-height: 35vh;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
max-width: 400px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.editor-modal {
@@ -111,6 +119,10 @@ header {
top: 50%;
left: 50%;
transform: translate(5%, 10%);
background-color: #fff; /* Ensures modal is opaque */
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
/* centers the editor modal */
width: 50vw;
max-width: 90vw;
@@ -119,6 +131,8 @@ header {
max-height: 80vh;
overflow: auto;
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 */
@@ -402,3 +416,34 @@ label {
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
import { loadFileList, displayFilePreview, initFileActions } from './fileManager.js';
import { showToast } from './domUtils.js';
export function initUpload() {
const fileInput = document.getElementById("file");
@@ -108,7 +110,7 @@ export function initUpload() {
list.style.padding = "0";
allFiles.forEach((file, index) => {
const li = document.createElement("li");
li.style.paddingTop = "20px";
li.style.paddingTop = "10px";
li.style.marginBottom = "10px";
li.style.display = (index < maxDisplay) ? "flex" : "none";
li.style.alignItems = "center";
@@ -144,7 +146,7 @@ export function initUpload() {
});
if (allFiles.length > maxDisplay) {
const extra = document.createElement("li");
extra.style.paddingTop = "20px";
extra.style.paddingTop = "10px";
extra.style.marginBottom = "10px";
extra.textContent = `Uploading additional ${allFiles.length - maxDisplay} file(s)...`;
extra.style.display = "flex";
@@ -161,7 +163,7 @@ export function initUpload() {
e.preventDefault();
const files = fileInput.files;
if (files.length === 0) {
alert("No files selected.");
showToast("No files selected.");
return;
}
const allFiles = Array.from(files);
@@ -274,12 +276,12 @@ export function initUpload() {
if (dropArea) setDropAreaDefault();
}, 10000);
if (!allSucceeded) {
alert("Some files failed to upload. Please check the list.");
showToast("Some files failed to upload. Please check the list.");
}
})
.catch(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;
}
// 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';
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"]);
exit;
}
@@ -21,6 +22,7 @@ $uploadDir = UPLOAD_DIR;
if ($folder !== 'root') {
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
if (!is_dir($uploadDir)) {
// Recursively create subfolders as needed.
mkdir($uploadDir, 0775, true);
}
} else {
@@ -71,4 +73,4 @@ if ($metadataChanged) {
}
echo json_encode(["success" => "Files uploaded successfully"]);
?>
?>