diff --git a/README.md b/README.md index fb2bbbc..959a3bd 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Multi File Upload Editor is a lightweight, secure web application for uploading, - The file list can be sorted by name, last modified date, upload date, size, or uploader. For easier browsing, the interface supports pagination with selectable page sizes (10, 20, 50, or 100 items per page) and navigation controls (“Prev”, “Next”, specific page numbers). - **User Authentication & Management:** - Secure, session-based authentication protects the editor. An admin user can add or remove users through the interface. Passwords are hashed using PHP’s password_hash() for security, and session checks prevent unauthorized access to backend endpoints. + - **CSRF Protection:** All state-changing endpoints (such as those for folder and file operations) include CSRF token validation to ensure that only legitimate requests from authenticated users are processed. - **Responsive, Dynamic & Persistent UI:** - The interface is mobile-friendly and adjusts to different screen sizes (hiding non-critical columns on small devices to avoid clutter). Updates to the file list, folder tree, and upload progress happen asynchronously (via Fetch API and XMLHttpRequest), so the page never needs to fully reload. Users receive immediate feedback through toast notifications and modal dialogs for actions like confirmations and error messages, creating a smooth user experience. Persistent UI elements Items Per Page, Dark/Light Mode, folder tree view & last open folder. - **Dark Mode/Light Mode** diff --git a/addUser.php b/addUser.php index 7ced597..b11a3fe 100644 --- a/addUser.php +++ b/addUser.php @@ -3,6 +3,13 @@ require 'config.php'; header('Content-Type: application/json'); $usersFile = USERS_DIR . USERS_FILE; +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; +} // Determine if we are in setup mode: // - Query parameter setup=1 is passed diff --git a/auth.js b/auth.js index e20eda9..62a3ba6 100644 --- a/auth.js +++ b/auth.js @@ -16,8 +16,8 @@ function initAuth() { password: document.getElementById("loginPassword").value.trim() }; console.log("Sending login data:", formData); - // sendRequest already handles credentials if configured in networkUtils.js. - sendRequest("auth.php", "POST", formData) + // Include CSRF token header with login + sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken }) .then(data => { console.log("Login response:", data); if (data.success) { @@ -35,7 +35,8 @@ function initAuth() { document.getElementById("logoutBtn").addEventListener("click", function () { fetch("logout.php", { method: "POST", - credentials: "include" // Ensure the session cookie is sent. + credentials: "include", + headers: { "X-CSRF-Token": window.csrfToken } }) .then(() => window.location.reload(true)) .catch(error => console.error("Logout error:", error)); @@ -62,7 +63,10 @@ function initAuth() { fetch(url, { method: "POST", credentials: "include", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin }) }) .then(response => response.json()) @@ -101,7 +105,10 @@ function initAuth() { fetch("removeUser.php", { method: "POST", credentials: "include", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, body: JSON.stringify({ username: usernameToRemove }) }) .then(response => response.json()) @@ -129,7 +136,6 @@ function checkAuthentication() { 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); document.querySelector(".header-buttons").style.visibility = "hidden"; @@ -143,7 +149,6 @@ function checkAuthentication() { toggleVisibility("mainOperations", true); toggleVisibility("uploadFileForm", true); toggleVisibility("fileListContainer", true); - // Show Add/Remove User buttons if admin. if (data.isAdmin) { const addUserBtn = document.getElementById("addUserBtn"); const removeUserBtn = document.getElementById("removeUserBtn"); @@ -156,7 +161,6 @@ function checkAuthentication() { if (removeUserBtn) removeUserBtn.style.display = "none"; } document.querySelector(".header-buttons").style.visibility = "visible"; - // Update persistent items-per-page select once main operations are visible. const selectElem = document.querySelector(".form-control.bottom-select"); if (selectElem) { const stored = localStorage.getItem("itemsPerPage") || "10"; @@ -183,18 +187,15 @@ window.checkAuthentication = checkAuthentication; /* ------------------------------ Persistent Items-Per-Page Setting ------------------------------ */ -// When the select value changes, save it to localStorage and refresh the file list. window.changeItemsPerPage = function (value) { console.log("Saving itemsPerPage:", value); localStorage.setItem("itemsPerPage", value); - // Refresh the file list automatically. const folder = window.currentFolder || "root"; if (typeof renderFileTable === "function") { renderFileTable(folder); } }; -// On DOMContentLoaded, set the select to the persisted value. document.addEventListener("DOMContentLoaded", function () { const selectElem = document.querySelector(".form-control.bottom-select"); if (selectElem) { @@ -207,7 +208,6 @@ document.addEventListener("DOMContentLoaded", function () { /* ------------------------------ Helper functions for modals and user list ------------------------------ */ - function resetUserForm() { document.getElementById("newUsername").value = ""; document.getElementById("newPassword").value = ""; diff --git a/auth.php b/auth.php index 4f699c5..5d6e4d6 100644 --- a/auth.php +++ b/auth.php @@ -3,6 +3,13 @@ require 'config.php'; header('Content-Type: application/json'); $usersFile = USERS_DIR . USERS_FILE; +/*$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; +}*/ // Function to authenticate user function authenticate($username, $password) { diff --git a/config.php b/config.php index bbb66e2..f140de0 100644 --- a/config.php +++ b/config.php @@ -2,6 +2,9 @@ session_set_cookie_params(7200); // 2 hours in seconds ini_set('session.gc_maxlifetime', 7200); session_start(); +if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); +} // config.php define('UPLOAD_DIR', '/var/www/uploads/'); define('BASE_URL', 'http://yourwebsite/uploads/'); diff --git a/copyFiles.php b/copyFiles.php index ee06428..158057a 100644 --- a/copyFiles.php +++ b/copyFiles.php @@ -2,6 +2,16 @@ require_once 'config.php'; header('Content-Type: application/json'); +// --- CSRF Protection --- +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; +} + // Ensure user is authenticated if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { echo json_encode(["error" => "Unauthorized"]); diff --git a/createFolder.php b/createFolder.php index e9251b3..4ce739d 100644 --- a/createFolder.php +++ b/createFolder.php @@ -15,6 +15,15 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { exit; } +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']); + http_response_code(403); + exit; +} + // Get the JSON input and decode it $input = json_decode(file_get_contents('php://input'), true); if (!isset($input['folderName'])) { diff --git a/deleteFiles.php b/deleteFiles.php index e0b5398..8e272c8 100644 --- a/deleteFiles.php +++ b/deleteFiles.php @@ -2,6 +2,16 @@ require_once 'config.php'; header('Content-Type: application/json'); +// --- CSRF Protection --- +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; +} + // Ensure user is authenticated if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { echo json_encode(["error" => "Unauthorized"]); diff --git a/deleteFolder.php b/deleteFolder.php index dedcd03..d34792a 100644 --- a/deleteFolder.php +++ b/deleteFolder.php @@ -15,6 +15,15 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { exit; } +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']); + http_response_code(403); + exit; +} + // Get the JSON input and decode it $input = json_decode(file_get_contents('php://input'), true); if (!isset($input['folder'])) { diff --git a/downloadZip.php b/downloadZip.php index df059bd..684cbb3 100644 --- a/downloadZip.php +++ b/downloadZip.php @@ -1,6 +1,16 @@ "Invalid CSRF token"]); + http_response_code(403); + exit; +} + // Check if the user is authenticated. if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { http_response_code(401); diff --git a/fileManager.js b/fileManager.js index 04ddeae..ba47ed9 100644 --- a/fileManager.js +++ b/fileManager.js @@ -1,5 +1,3 @@ -// fileManager.js - import { escapeHTML, debounce, @@ -21,11 +19,8 @@ window.itemsPerPage = window.itemsPerPage || 10; window.currentPage = window.currentPage || 1; // --- Define formatFolderName --- -// This helper formats folder names for display. Adjust as needed. function formatFolderName(folder) { - // Example: If folder is "root", return "(Root)" if (folder === "root") return "(Root)"; - // Replace underscores/dashes with spaces and capitalize each word. return folder .replace(/[_-]+/g, " ") .replace(/\b\w/g, char => char.toUpperCase()); @@ -247,7 +242,11 @@ document.addEventListener("DOMContentLoaded", function () { confirmDelete.addEventListener("click", function () { fetch("deleteFiles.php", { method: "POST", - headers: { "Content-Type": "application/json" }, + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, body: JSON.stringify({ folder: window.currentFolder, files: window.filesToDelete }) }) .then(response => response.json()) @@ -303,7 +302,10 @@ document.addEventListener("DOMContentLoaded", function () { fetch("downloadZip.php", { method: "POST", credentials: "include", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, body: JSON.stringify({ folder: folder, files: window.filesToDownload }) }) .then(response => { @@ -396,7 +398,11 @@ document.addEventListener("DOMContentLoaded", function () { } fetch("copyFiles.php", { method: "POST", - headers: { "Content-Type": "application/json" }, + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, body: JSON.stringify({ source: window.currentFolder, files: window.filesToCopy, destination: targetFolder }) }) .then(response => response.json()) @@ -452,7 +458,11 @@ document.addEventListener("DOMContentLoaded", function () { } fetch("moveFiles.php", { method: "POST", - headers: { "Content-Type": "application/json" }, + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, body: JSON.stringify({ source: window.currentFolder, files: window.filesToMove, destination: targetFolder }) }) .then(response => response.json()) @@ -632,7 +642,11 @@ export function saveFile(fileName, folder) { }; fetch("saveFile.php", { method: "POST", - headers: { "Content-Type": "application/json" }, + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, body: JSON.stringify(fileDataObj) }) .then(response => response.json()) @@ -708,7 +722,11 @@ document.addEventListener("DOMContentLoaded", () => { const folderUsed = window.fileFolder; fetch("renameFile.php", { method: "POST", - headers: { "Content-Type": "application/json" }, + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, body: JSON.stringify({ folder: folderUsed, oldName: window.fileToRename, newName: newName }) }) .then(response => response.json()) diff --git a/folderManager.js b/folderManager.js index 43fde17..e4b11a3 100644 --- a/folderManager.js +++ b/folderManager.js @@ -267,7 +267,9 @@ document.getElementById("cancelRenameFolder").addEventListener("click", function document.getElementById("newRenameFolderName").value = ""; }); -document.getElementById("submitRenameFolder").addEventListener("click", function () { +document.getElementById("submitRenameFolder").addEventListener("click", function (event) { + event.preventDefault(); // Prevent default form submission + const selectedFolder = window.currentFolder || "root"; const newNameBasename = document.getElementById("newRenameFolderName").value.trim(); if (!newNameBasename || newNameBasename === selectedFolder.split("/").pop()) { @@ -276,9 +278,22 @@ document.getElementById("submitRenameFolder").addEventListener("click", function } const parentPath = getParentFolder(selectedFolder); const newFolderFull = parentPath === "root" ? newNameBasename : parentPath + "/" + newNameBasename; + + // Read the CSRF token from the meta tag + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + if (!csrfToken) { + showToast("CSRF token not loaded yet! Please try again."); + return; + } + + // Send the rename request with the CSRF token in a custom header fetch("renameFolder.php", { method: "POST", - headers: { "Content-Type": "application/json" }, + credentials: "include", // ensure cookies (and session) are sent + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken + }, body: JSON.stringify({ oldFolder: selectedFolder, newFolder: newFolderFull }) }) .then(response => response.json()) @@ -316,9 +331,15 @@ document.getElementById("cancelDeleteFolder").addEventListener("click", function document.getElementById("confirmDeleteFolder").addEventListener("click", function () { const selectedFolder = window.currentFolder || "root"; + // Read CSRF token from the meta tag + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + fetch("deleteFolder.php", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken + }, body: JSON.stringify({ folder: selectedFolder }) }) .then(response => response.json()) @@ -358,10 +379,19 @@ document.getElementById("submitCreateFolder").addEventListener("click", function if (selectedFolder && selectedFolder !== "root") { fullFolderName = selectedFolder + "/" + folderInput; } + // Read CSRF token from the meta tag + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + fetch("createFolder.php", { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ folderName: folderInput, parent: selectedFolder === "root" ? "" : selectedFolder }) + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken + }, + body: JSON.stringify({ + folderName: folderInput, + parent: selectedFolder === "root" ? "" : selectedFolder + }) }) .then(response => response.json()) .then(data => { diff --git a/index.html b/index.html index 5b0285e..f11b5fb 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,7 @@ Multi File Upload Editor + diff --git a/logout.php b/logout.php index fa8ebe8..b18646b 100644 --- a/logout.php +++ b/logout.php @@ -1,5 +1,12 @@ "Invalid CSRF token"]); + http_response_code(403); + exit; +} $_SESSION = []; // Clear session data session_destroy(); // Destroy session diff --git a/main.js b/main.js index 88a96ac..668a9bf 100644 --- a/main.js +++ b/main.js @@ -17,6 +17,25 @@ import { loadFolderTree } from './folderManager.js'; import { initUpload } from './upload.js'; import { initAuth, checkAuthentication } from './auth.js'; +function loadCsrfToken() { + fetch('token.php', { credentials: 'include' }) + .then(response => response.json()) + .then(data => { + // Assign to global variable + window.csrfToken = data.csrf_token; + // Also update the meta tag + let meta = document.querySelector('meta[name="csrf-token"]'); + if (!meta) { + meta = document.createElement('meta'); + meta.name = 'csrf-token'; + document.head.appendChild(meta); + } + meta.setAttribute('content', data.csrf_token); + }) + .catch(error => console.error("Error loading CSRF token:", error)); +} + +document.addEventListener("DOMContentLoaded", loadCsrfToken); // Expose functions for inline handlers. window.sendRequest = sendRequest; window.toggleVisibility = toggleVisibility; diff --git a/moveFiles.php b/moveFiles.php index 52b0161..afc2ffe 100644 --- a/moveFiles.php +++ b/moveFiles.php @@ -2,6 +2,16 @@ require_once 'config.php'; header('Content-Type: application/json'); +// --- CSRF Protection --- +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; +} + // Ensure user is authenticated if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { echo json_encode(["error" => "Unauthorized"]); diff --git a/removeUser.php b/removeUser.php index 27f6065..30f7893 100644 --- a/removeUser.php +++ b/removeUser.php @@ -3,6 +3,13 @@ require 'config.php'; header('Content-Type: application/json'); $usersFile = USERS_DIR . USERS_FILE; +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; +} // Only allow admins to remove users if ( diff --git a/renameFile.php b/renameFile.php index 39b60c5..a1c6e0b 100644 --- a/renameFile.php +++ b/renameFile.php @@ -5,6 +5,16 @@ header("Cache-Control: no-cache, no-store, must-revalidate"); header("Pragma: no-cache"); header("Expires: 0"); +// --- CSRF Protection --- +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; +} + // Ensure user is authenticated if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { echo json_encode(["error" => "Unauthorized"]); diff --git a/renameFolder.php b/renameFolder.php index 8c8c04d..be9829d 100644 --- a/renameFolder.php +++ b/renameFolder.php @@ -18,6 +18,16 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { exit; } +// CSRF Protection: Read token from the custom header "X-CSRF-Token" +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']); + http_response_code(403); + exit; +} + // Get the JSON input and decode it $input = json_decode(file_get_contents('php://input'), true); if (!isset($input['oldFolder']) || !isset($input['newFolder'])) { @@ -28,22 +38,19 @@ if (!isset($input['oldFolder']) || !isset($input['newFolder'])) { $oldFolder = trim($input['oldFolder']); $newFolder = trim($input['newFolder']); -// Allow letters, numbers, underscores, dashes, spaces, and forward slashes +// Validate folder names 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; } -// Trim any leading/trailing slashes and spaces. $oldFolder = trim($oldFolder, "/\\ "); $newFolder = trim($newFolder, "/\\ "); -// 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) { @@ -51,13 +58,11 @@ if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) || 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. if (file_exists($newPath)) { echo json_encode(['success' => false, 'error' => 'New folder name already exists.']); exit; diff --git a/saveFile.php b/saveFile.php index 89b3b70..324df79 100644 --- a/saveFile.php +++ b/saveFile.php @@ -2,6 +2,16 @@ require_once 'config.php'; header('Content-Type: application/json'); +// --- CSRF Protection --- +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; +} + // Ensure user is authenticated if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { echo json_encode(["error" => "Unauthorized"]); diff --git a/token.php b/token.php new file mode 100644 index 0000000..60facb8 --- /dev/null +++ b/token.php @@ -0,0 +1,5 @@ + $_SESSION['csrf_token']]); +?> \ No newline at end of file diff --git a/upload.js b/upload.js index 40b0960..69e1b28 100644 --- a/upload.js +++ b/upload.js @@ -7,8 +7,6 @@ function traverseFileTreePromise(item, path = "") { return new Promise((resolve, reject) => { if (item.isFile) { item.file(file => { - // Instead of modifying file.webkitRelativePath (read-only), - // define a new property called "customRelativePath" Object.defineProperty(file, 'customRelativePath', { value: path + file.name, writable: true, @@ -23,9 +21,7 @@ function traverseFileTreePromise(item, path = "") { for (let i = 0; i < entries.length; i++) { promises.push(traverseFileTreePromise(entries[i], path + item.name + "/")); } - Promise.all(promises).then(results => { - resolve(results.flat()); - }); + Promise.all(promises).then(results => resolve(results.flat())); }); } else { resolve([]); @@ -46,7 +42,6 @@ function getFilesFromDataTransferItems(items) { } // Helper: Set default drop area content. -// Moved to module scope so it is available globally in this module. function setDropAreaDefault() { const dropArea = document.getElementById("uploadDropArea"); if (dropArea) { @@ -104,7 +99,6 @@ function updateFileInfoCount() { ${window.selectedFiles.length} files selected `; } - // Show preview of first file. const previewContainer = document.getElementById("filePreviewContainer"); if (previewContainer && window.selectedFiles.length > 0) { previewContainer.innerHTML = ""; @@ -120,19 +114,16 @@ function createFileEntry(file) { li.style.display = "flex"; li.dataset.uploadIndex = file.uploadIndex; - // Create remove button positioned to the left of the preview. const removeBtn = document.createElement("button"); removeBtn.classList.add("remove-file-btn"); removeBtn.textContent = "×"; removeBtn.addEventListener("click", function (e) { e.stopPropagation(); - // Remove file from global selected files array. const uploadIndex = file.uploadIndex; window.selectedFiles = window.selectedFiles.filter(f => f.uploadIndex !== uploadIndex); li.remove(); updateFileInfoCount(); }); - // Store the button so we can hide it later when upload completes. li.removeBtn = removeBtn; const preview = document.createElement("div"); @@ -154,8 +145,6 @@ function createFileEntry(file) { progBar.innerText = "0%"; progDiv.appendChild(progBar); - - // Append in order: remove button, preview, name, progress. li.appendChild(removeBtn); li.appendChild(preview); li.appendChild(nameDiv); @@ -171,7 +160,6 @@ function processFiles(filesInput) { const fileInfoContainer = document.getElementById("fileInfoContainer"); const files = Array.from(filesInput); - // Update file info container with preview and file count. if (fileInfoContainer) { if (files.length > 0) { if (files.length === 1) { @@ -195,12 +183,10 @@ function processFiles(filesInput) { } } - // Assign unique uploadIndex to each file. files.forEach((file, index) => { file.uploadIndex = index; }); - // Build progress list. const progressContainer = document.getElementById("uploadProgressContainer"); progressContainer.innerHTML = ""; @@ -209,14 +195,12 @@ function processFiles(filesInput) { const list = document.createElement("ul"); list.classList.add("upload-progress-list"); - // Determine grouping using relative path. const hasRelativePaths = files.some(file => { const rel = file.webkitRelativePath || file.customRelativePath || ""; return rel.trim() !== ""; }); if (hasRelativePaths) { - // Group files by folder. const fileGroups = {}; files.forEach(file => { let folderName = "Root"; @@ -233,15 +217,12 @@ function processFiles(filesInput) { fileGroups[folderName].push(file); }); - // Create list elements for each folder group. Object.keys(fileGroups).forEach(folderName => { - // Folder header with Material Icon. const folderLi = document.createElement("li"); folderLi.classList.add("upload-folder-group"); folderLi.innerHTML = `folder ${folderName}:`; list.appendChild(folderLi); - // Nested list for files. const nestedUl = document.createElement("ul"); nestedUl.classList.add("upload-folder-group-list"); fileGroups[folderName] @@ -253,7 +234,6 @@ function processFiles(filesInput) { list.appendChild(nestedUl); }); } else { - // Flat list. files.forEach((file, index) => { const li = createFileEntry(file); li.style.display = (index < maxDisplay) ? "flex" : "none"; @@ -270,19 +250,15 @@ function processFiles(filesInput) { } const listWrapper = document.createElement("div"); listWrapper.classList.add("upload-progress-wrapper"); - // Set a maximum height and enable vertical scrolling. listWrapper.style.maxHeight = "300px"; listWrapper.style.overflowY = "auto"; listWrapper.appendChild(list); progressContainer.appendChild(listWrapper); } - // Call once on page load: adjustFolderHelpExpansion(); - // Also call on window resize: window.addEventListener("resize", adjustFolderHelpExpansion); - // Store files globally for submission. window.selectedFiles = files; updateFileInfoCount(); } @@ -293,7 +269,6 @@ function submitFiles(allFiles) { const progressContainer = document.getElementById("uploadProgressContainer"); const fileInput = document.getElementById("file"); - // Map uploadIndex to progress element. const progressElements = {}; const listItems = progressContainer.querySelectorAll("li.upload-progress-item"); listItems.forEach(item => { @@ -308,6 +283,8 @@ function submitFiles(allFiles) { const formData = new FormData(); formData.append("file[]", file); formData.append("folder", folderToUse); + // Append CSRF token as "upload_token" + formData.append("upload_token", window.csrfToken); const relativePath = file.webkitRelativePath || file.customRelativePath || ""; if (relativePath.trim() !== "") { formData.append("relativePath", relativePath); @@ -346,7 +323,6 @@ function submitFiles(allFiles) { if (li) { li.progressBar.style.width = "100%"; li.progressBar.innerText = "Done"; - // Hide the remove button now that upload is done. if (li.removeBtn) { li.removeBtn.style.display = "none"; } @@ -391,6 +367,8 @@ function submitFiles(allFiles) { }); xhr.open("POST", "upload.php", true); + // Set the CSRF token header to match the folderManager approach. + xhr.setRequestHeader("X-CSRF-Token", window.csrfToken); xhr.send(formData); }); @@ -400,7 +378,6 @@ function submitFiles(allFiles) { initFileActions(); serverFiles = (serverFiles || []).map(item => item.name.trim().toLowerCase()); allFiles.forEach(file => { - // Skip verification for folder-uploaded files. if ((file.webkitRelativePath || file.customRelativePath || "").trim() !== "") { return; } @@ -415,7 +392,6 @@ function submitFiles(allFiles) { }); setTimeout(() => { if (fileInput) fileInput.value = ""; - // Hide remove buttons in progress container. const removeBtns = progressContainer.querySelectorAll("button.remove-file-btn"); removeBtns.forEach(btn => btn.style.display = "none"); progressContainer.innerHTML = ""; @@ -450,15 +426,12 @@ function initUpload() { const uploadForm = document.getElementById("uploadFileForm"); if (fileInput) { - // Remove folder selection attributes so clicking the input shows files: fileInput.removeAttribute("webkitdirectory"); fileInput.removeAttribute("mozdirectory"); fileInput.removeAttribute("directory"); - // Allow selecting multiple files. fileInput.setAttribute("multiple", ""); } - // Set default drop area content. setDropAreaDefault(); if (dropArea) { diff --git a/upload.php b/upload.php index df06e15..ee84eb1 100644 --- a/upload.php +++ b/upload.php @@ -2,6 +2,17 @@ require_once 'config.php'; header('Content-Type: application/json'); +// --- CSRF Protection for Uploads --- +// Use getallheaders() to read the token from the header. +$headers = array_change_key_case(getallheaders(), CASE_LOWER); +$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; + +if ($receivedToken !== $_SESSION['csrf_token']) { + echo json_encode(["error" => "Invalid CSRF token"]); + http_response_code(403); + exit; +} + // Ensure user is authenticated if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { echo json_encode(["error" => "Unauthorized"]); @@ -9,9 +20,8 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { exit; } -// Validate folder name input. Allow letters, numbers, underscores, dashes, spaces, and forward slashes. +// Validate folder name input. $folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root'; -// 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; @@ -22,7 +32,6 @@ $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 { @@ -36,24 +45,18 @@ $metadataFile = META_DIR . META_FILE; $metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; $metadataChanged = false; -// Define a safe pattern for file names: letters, numbers, underscores, dashes, dots, and spaces. $safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/'; foreach ($_FILES["file"]["name"] as $index => $fileName) { - // Use basename to strip any directory components. $safeFileName = basename($fileName); - - // Validate that the sanitized file name contains only allowed characters. if (!preg_match($safeFileNamePattern, $safeFileName)) { echo json_encode(["error" => "Invalid file name: " . $fileName]); exit; } // --- Minimal Folder/Subfolder Logic --- - // Check if a relativePath was provided (from a folder upload) $relativePath = ''; if (isset($_POST['relativePath'])) { - // In case of multiple files, relativePath may be an array. if (is_array($_POST['relativePath'])) { $relativePath = $_POST['relativePath'][$index] ?? ''; } else { @@ -61,10 +64,8 @@ foreach ($_FILES["file"]["name"] as $index => $fileName) { } } if (!empty($relativePath)) { - // Extract the directory part from the relative path. $subDir = dirname($relativePath); if ($subDir !== '.' && $subDir !== '') { - // If uploading to root, don't add the "root" folder in the path. if ($folder === 'root') { $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $subDir . DIRECTORY_SEPARATOR; } else { @@ -73,7 +74,6 @@ foreach ($_FILES["file"]["name"] as $index => $fileName) { if (!is_dir($uploadDir)) { mkdir($uploadDir, 0775, true); } - // Use the basename from the relative path. $safeFileName = basename($relativePath); } } @@ -82,7 +82,6 @@ foreach ($_FILES["file"]["name"] as $index => $fileName) { $targetPath = $uploadDir . $safeFileName; if (move_uploaded_file($_FILES["file"]["tmp_name"][$index], $targetPath)) { - // Build the metadata key. if (!empty($relativePath)) { $metaKey = ($folder !== 'root') ? $folder . "/" . $relativePath : $relativePath; } else {