validation, toast, modal, folder tree
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
40
auth.js
@@ -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();
|
||||
}
|
||||
})
|
||||
|
||||
23
auth.php
23
auth.php
@@ -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"]);
|
||||
}
|
||||
?>
|
||||
?>
|
||||
@@ -19,4 +19,4 @@ echo json_encode([
|
||||
"authenticated" => true,
|
||||
"isAdmin" => isset($_SESSION["isAdmin"]) ? $_SESSION["isAdmin"] : false
|
||||
]);
|
||||
?>
|
||||
?>
|
||||
@@ -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)]);
|
||||
}
|
||||
?>
|
||||
?>
|
||||
@@ -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.']);
|
||||
}
|
||||
?>
|
||||
?>
|
||||
@@ -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)]);
|
||||
}
|
||||
?>
|
||||
?>
|
||||
@@ -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.']);
|
||||
}
|
||||
?>
|
||||
?>
|
||||
135
domUtils.js
135
domUtils.js
@@ -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, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
330
fileManager.js
330
fileManager.js
@@ -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;
|
||||
|
||||
|
||||
576
folderManager.js
576
folderManager.js
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
?>
|
||||
?>
|
||||
@@ -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);
|
||||
?>
|
||||
?>
|
||||
@@ -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]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
187
index.html
187
index.html
@@ -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
34
main.js
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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)]);
|
||||
}
|
||||
?>
|
||||
?>
|
||||
@@ -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"]);
|
||||
?>
|
||||
?>
|
||||
@@ -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"]);
|
||||
}
|
||||
?>
|
||||
?>
|
||||
@@ -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.']);
|
||||
}
|
||||
?>
|
||||
?>
|
||||
33
saveFile.php
33
saveFile.php
@@ -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"]);
|
||||
}
|
||||
?>
|
||||
?>
|
||||
65
styles.css
65
styles.css
@@ -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 it’s 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;
|
||||
}
|
||||
12
upload.js
12
upload.js
@@ -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.");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"]);
|
||||
?>
|
||||
?>
|
||||
Reference in New Issue
Block a user