Compare commits

...

10 Commits

Author SHA1 Message Date
Ryan
2c8374a66c New features added 2025-03-25 03:36:44 -04:00
Ryan
49138835ce Authentication & Initialization Changes plus File & Fold Manager Enhancements 2025-03-25 03:29:32 -04:00
Ryan
c0dc0ce391 Rename file modal select and focus filename 2025-03-24 16:53:37 -04:00
Ryan
6426f4b924 Redirection 2025-03-24 16:36:32 -04:00
Ryan
b72356b657 attachEnterKeyListener, focus and showCustomConfirmModal added 2025-03-24 13:46:35 -04:00
Ryan
fc45767712 Save admin status in persistent token 2025-03-24 10:21:20 -04:00
Ryan
1d5c6a48b5 PERSISTENT_TOKENS_KEY updates 2025-03-24 00:16:09 -04:00
Ryan
772326c8e0 Added PERSISTENT_TOKENS_KEY to Using Docker Compose: 2025-03-24 00:08:06 -04:00
Ryan
5892236aa9 encrypt and decrypt persistent tokens 2025-03-23 23:29:51 -04:00
Ryan
0215bd3d76 add highlight to pauseResumeBtn 2025-03-23 02:43:16 -04:00
13 changed files with 919 additions and 200 deletions

View File

@@ -22,6 +22,7 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
- **Real-Time Progress:** Each file shows an individual progress bar that displays percentage complete and upload speed.
- **File & Folder Grouping:** When many files are dropped, files are automatically grouped into a scrollable wrapper, ensuring the interface remains clean.
- **Secure Uploads:** All uploads integrate CSRF token validation and other security checks.
- **Built-in File Editing & Renaming:**
- Text-based files (e.g., .txt, .html, .js) can be opened and edited in a modal window using CodeMirror for:
- Syntax highlighting
@@ -30,52 +31,80 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
- Files can be renamed directly through the interface.
- The renaming functionality now supports names with parentheses and checks for duplicate names, automatically generating a unique name (e.g., appending “ (1)”) when needed.
- Folder-specific metadata is updated accordingly.
- **Enhanced File Editing Check:** Files with a Content-Length of 0 KB are now allowed to be edited.
- **Built-in File Preview:**
- Users can quickly preview images, videos, and PDFs directly in modal popups without leaving the page.
- The preview modal supports inline display of images (with proper scaling) and videos with playback controls.
- Navigation (prev/next) within image previews is supported for a seamless browsing experience.
- **Gallery (Grid) View:**
- In addition to the traditional table view, users can toggle to a gallery view that arranges image thumbnails in a grid layout.
- The gallery view offers multiple column options (e.g., 3, 4, or 5 columns) so that users can choose the layout that best fits their screen.
- Action buttons (Download, Edit, Rename, Share) appear beneath each thumbnail for quick access.
- **Batch Operations (Delete/Copy/Move/Download):**
- **Batch Operations (Delete/Copy/Move/Download/Extract Zip):**
- **Delete Files:** Delete multiple files at once.
- **Copy Files:** Copy selected files to another folder with a unique-naming feature to prevent overwrites.
- **Move Files:** Move selected files to a different folder, automatically generating a unique filename if needed to avoid data loss.
- **Download Files as ZIP:** Download selected files as a ZIP archive. Users can specify a custom name for the ZIP file via a modal dialog.
- **Extract Zip:** When one or more ZIP files are selected, users can extract the archive(s) directly into the current folder.
- **Drag & Drop:** Easily move files by selecting them from the file list and simply dragging them onto your desired folder in the folder tree or breadcrumb. When you drop the files onto a folder, the system automatically moves them, updating your file organization in one seamless action.
- **Enhanced Context Menu & Keyboard Shortcuts:**
- **Right-Click Context Menu:**
- A custom context menu appears on right-clicking within the file list.
- For multiple selections, options include Delete Selected, Copy Selected, Move Selected, Download Zip, and (if applicable) Extract Zip.
- When exactly one file is selected, additional options (Preview, Edit [if editable], and Rename) are available.
- **Keyboard Shortcut for Deletion:**
- A global keydown listener detects Delete/Backspace key presses (when no input is focused) to trigger the delete operation.
- **Folder Management:**
- Organize files into folders and subfolders with the ability to create, rename, and delete folders.
- A dynamic folder tree in the UI allows users to navigate directories easily, and any changes are immediately reflected in real time.
- **Per-Folder Metadata Storage:** Each folder has its own metadata JSON file (e.g., `root_metadata.json`, `FolderName_metadata.json`), and operations (copy/move/rename) update these metadata files accordingly.
- **Intuitive Breadcrumb Navigation:** Clickable breadcrumbs enable users to quickly jump to any parent folder, streamlining navigation across subfolders. Supports drag & drop to move files.
- **Folder Manager Context Menu:**
- Right-clicking on a folder (in the folder tree or breadcrumb) brings up a custom context menu with options for creating, renaming, and deleting folders.
- **Keyboard Shortcut for Folder Deletion:**
- A global key listener (Delete/Backspace) is provided to trigger folder deletion (with safeguards to prevent deleting the root folder).
- **Sorting & Pagination:**
- The file list can be sorted by name, modified date, upload date, file size, or uploader.
- Pagination controls let users navigate through files with selectable page sizes (10, 20, 50, or 100 items per page) and “Prev”/“Next” navigation buttons.
- **Share Link Functionality:**
- Generate shareable links for files with configurable expiration times (e.g., 30, 60, 120, 180, 240 minutes, and a 1-day option) and optional password protection.
- Share links are stored in a JSON file with details including the folder, file, expiration timestamp, and hashed password.
- The share endpoint (`share.php`) validates tokens, expiration, and password before serving files (or forcing downloads).
- The share URL is configurable via environment variables or auto-detected from the server.
- **User Authentication & Management:**
- Secure, session-based authentication protects the file manager.
- Admin users can add or remove users through the interface.
- Passwords are hashed using PHPs `password_hash()` for security.
- All state-changing endpoints include CSRF token validation.
- Change password supported for all users.
- **Persistent Login (Remember Me) with Encrypted Tokens:**
- Users can remain logged in across sessions securely.
- Persistent tokens are encrypted using AES256CBC before being stored in a JSON file.
- On auto-login, the tokens are decrypted on the server to re-establish user sessions without requiring re-authentication.
- **Responsive, Dynamic & Persistent UI:**
- The interface is mobile-friendly and adapts to various screen sizes by hiding non-critical columns on small devices.
- Asynchronous updates (via Fetch API and XMLHttpRequest) keep the UI responsive without full page reloads.
- Persistent settings (such as items per page, dark/light mode preference, folder tree state, and the last open folder) ensure a smooth and customized user experience.
- **Dark Mode/Light Mode:**
- The application automatically adapts to the operating systems theme preference by default and offers a manual toggle.
- The dark mode provides a darker background with lighter text and adjusts UI elements (including the CodeMirror editor) for optimal readability in low-light conditions.
- The light mode maintains a bright interface for well-lit environments.
- **Server & Security Enhancements:**
- The Apache configuration (or .htaccess files) is set to disable directory indexing (e.g., using `Options -Indexes` in the uploads directory), preventing unauthorized users from viewing directory contents.
- Direct access to sensitive files (e.g., `users.txt`) is restricted through .htaccess rules.
- A proxy download mechanism has been implemented (via endpoints like `download.php` and `downloadZip.php`) so that every file download request goes through a PHP script. This script validates the session and CSRF token before streaming the file, ensuring that even if a file URL is guessed, only authenticated users can access it.
- Administrators are advised to deploy the app on a secure internal network or use the proxy download mechanism for public deployments to further protect file content.
- **Trash Management with Restore & Delete:**
- **Trash Storage & Metadata:**
- Deleted files are moved to a designated “Trash” folder rather than being immediately removed.
@@ -215,6 +244,7 @@ For users who prefer containerization, a Docker image is available
TIMEZONE: "America/New_York"
TOTAL_UPLOAD_SIZE: "5G"
SECURE: "false"
PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
volumes:
- /path/to/your/uploads:/var/www/uploads
- /path/to/your/users:/var/www/users
@@ -246,7 +276,7 @@ The `config.php` file contains several key constants that may need adjustment fo
Defines the maximum upload size (default is `5G`). Ensure that PHPs `upload_max_filesize` and `post_max_size` in your `php.ini` are consistent with this setting. The startup script (`start.sh`) updates PHP limits at runtime based on this value.
- **Environment Variables (Docker):**
The Docker image supports overriding configuration via environment variables. For example, you can set `SECURE`, `SHARE_URL`, and port settings via the containers environment.
The Docker image supports overriding configuration via environment variables. For example, you can set `SECURE`, `SHARE_URL`, `PERSISTENT_TOKENS_KEY` and port settings via the containers environment.
---

245
auth.js
View File

@@ -1,75 +1,112 @@
import { sendRequest } from './networkUtils.js';
import { toggleVisibility, showToast } from './domUtils.js';
import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmModal } from './domUtils.js';
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
import { loadFolderTree } from './folderManager.js';
function initAuth() {
// First, check if the user is already authenticated.
checkAuthentication(false).then(data => {
if (data.setup) {
window.setupMode = true;
showToast("Setup mode: No users found. Please add an admin user.");
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
toggleVisibility("addUserModal", true);
return;
}
window.setupMode = false;
if (data.authenticated) {
// User is logged in—show the main UI.
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", true);
toggleVisibility("uploadFileForm", true);
toggleVisibility("fileListContainer", true);
document.querySelector(".header-buttons").style.visibility = "visible";
// If admin, show admin-only buttons.
if (data.isAdmin) {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "block";
if (removeUserBtn) removeUserBtn.style.display = "block";
// Create and show the restore button.
let restoreBtn = document.getElementById("restoreFilesBtn");
if (!restoreBtn) {
restoreBtn = document.createElement("button");
restoreBtn.id = "restoreFilesBtn";
restoreBtn.classList.add("btn", "btn-warning");
// Use a material icon.
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons) {
if (headerButtons.children.length >= 5) {
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
} else {
headerButtons.appendChild(restoreBtn);
}
}
/**
* Updates the select element to reflect the stored items-per-page value.
*/
function updateItemsPerPageSelect() {
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
}
/**
* Updates the UI for an authenticated user.
* This includes showing the main UI panels, attaching key listeners, updating header buttons,
* and displaying admin-only buttons if applicable.
*/
function updateAuthenticatedUI(data) {
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", true);
toggleVisibility("uploadFileForm", true);
toggleVisibility("fileListContainer", true);
attachEnterKeyListener("addUserModal", "saveUserBtn");
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
document.querySelector(".header-buttons").style.visibility = "visible";
// If admin, show admin-only buttons; otherwise hide them.
if (data.isAdmin) {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "block";
if (removeUserBtn) removeUserBtn.style.display = "block";
let restoreBtn = document.getElementById("restoreFilesBtn");
if (!restoreBtn) {
restoreBtn = document.createElement("button");
restoreBtn.id = "restoreFilesBtn";
restoreBtn.classList.add("btn", "btn-warning");
// Using a material icon for restore.
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons) {
if (headerButtons.children.length >= 5) {
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
} else {
headerButtons.appendChild(restoreBtn);
}
restoreBtn.style.display = "block";
}
}
restoreBtn.style.display = "block";
} else {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "none";
if (removeUserBtn) removeUserBtn.style.display = "none";
const restoreBtn = document.getElementById("restoreFilesBtn");
if (restoreBtn) restoreBtn.style.display = "none";
}
updateItemsPerPageSelect();
}
/**
* Checks the user's authentication state and updates the UI accordingly.
* If in setup mode or not authenticated, it shows the proper UI elements.
* When authenticated, it calls updateAuthenticatedUI to handle the UI updates.
*/
function checkAuthentication(showLoginToast = true) {
return sendRequest("checkAuth.php")
.then(data => {
if (data.setup) {
window.setupMode = true;
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
toggleVisibility("addUserModal", true);
document.getElementById('newUsername').focus();
return false;
}
window.setupMode = false;
if (data.authenticated) {
updateAuthenticatedUI(data);
return data;
} else {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "none";
if (removeUserBtn) removeUserBtn.style.display = "none";
const restoreBtn = document.getElementById("restoreFilesBtn");
if (restoreBtn) {
restoreBtn.style.display = "none";
}
if (showLoginToast) showToast("Please log in to continue.");
toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false);
toggleVisibility("fileListContainer", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
return false;
}
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
} else {
toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false);
toggleVisibility("fileListContainer", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
}
}).catch(error => {
})
.catch(error => {
console.error("Error checking authentication:", error);
return false;
});
}
/**
* Initializes authentication by checking the user's state and setting up event listeners.
* The UI will update automatically based on the auth state.
*/
function initAuth() {
checkAuthentication(false).catch(error => {
console.error("Error checking authentication:", error);
});
@@ -78,9 +115,8 @@ function initAuth() {
if (authForm) {
authForm.addEventListener("submit", function (event) {
event.preventDefault();
// Get the "Remember me" checkbox value.
const rememberMe = document.getElementById("rememberMeCheckbox")
? document.getElementById("rememberMeCheckbox").checked
const rememberMe = document.getElementById("rememberMeCheckbox")
? document.getElementById("rememberMeCheckbox").checked
: false;
const formData = {
username: document.getElementById("loginUsername").value.trim(),
@@ -128,13 +164,12 @@ function initAuth() {
document.getElementById("addUserBtn").addEventListener("click", function () {
resetUserForm();
toggleVisibility("addUserModal", true);
document.getElementById('newUsername').focus();
});
document.getElementById("saveUserBtn").addEventListener("click", function () {
const newUsername = document.getElementById("newUsername").value.trim();
// Use the new ID for the add user modal's password field.
const newPassword = document.getElementById("addUserPassword").value.trim();
const isAdmin = document.getElementById("isAdmin").checked;
console.log("newUsername:", newUsername, "newPassword:", newPassword);
if (!newUsername || !newPassword) {
showToast("Username and password are required!");
return;
@@ -157,6 +192,7 @@ function initAuth() {
if (data.success) {
showToast("User added successfully!");
closeAddUserModal();
// Re-check auth state to update the UI after adding a user.
checkAuthentication(false);
} else {
showToast("Error: " + (data.error || "Could not add user"));
@@ -173,14 +209,16 @@ function initAuth() {
loadUserList();
toggleVisibility("removeUserModal", true);
});
document.getElementById("deleteUserBtn").addEventListener("click", function () {
document.getElementById("deleteUserBtn").addEventListener("click", async function () {
const selectElem = document.getElementById("removeUsernameSelect");
const usernameToRemove = selectElem.value;
if (!usernameToRemove) {
showToast("Please select a user to remove.");
return;
}
if (!confirm("Are you sure you want to delete user " + usernameToRemove + "?")) {
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
if (!confirmed) {
return;
}
fetch("removeUser.php", {
@@ -204,39 +242,33 @@ function initAuth() {
})
.catch(error => console.error("Error removing user:", error));
});
document.getElementById("cancelRemoveUserBtn").addEventListener("click", function () {
closeRemoveUserModal();
});
document.getElementById("changePasswordBtn").addEventListener("click", function() {
// Show the Change Password modal.
document.getElementById("changePasswordBtn").addEventListener("click", function () {
document.getElementById("changePasswordModal").style.display = "block";
document.getElementById("oldPassword").focus();
});
document.getElementById("closeChangePasswordModal").addEventListener("click", function() {
// Hide the Change Password modal.
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
document.getElementById("changePasswordModal").style.display = "none";
});
document.getElementById("saveNewPasswordBtn").addEventListener("click", function() {
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
const oldPassword = document.getElementById("oldPassword").value.trim();
const newPassword = document.getElementById("newPassword").value.trim(); // Change Password modal field
const newPassword = document.getElementById("newPassword").value.trim();
const confirmPassword = document.getElementById("confirmPassword").value.trim();
if (!oldPassword || !newPassword || !confirmPassword) {
showToast("Please fill in all fields.");
return;
}
if (newPassword !== confirmPassword) {
showToast("New passwords do not match.");
return;
}
// Prepare the data to send.
const data = { oldPassword, newPassword, confirmPassword };
// Send request to changePassword.php.
fetch("changePassword.php", {
method: "POST",
credentials: "include",
@@ -250,7 +282,6 @@ function initAuth() {
.then(result => {
if (result.success) {
showToast(result.success);
// Clear form fields and close modal.
document.getElementById("oldPassword").value = "";
document.getElementById("newPassword").value = "";
document.getElementById("confirmPassword").value = "";
@@ -266,38 +297,6 @@ function initAuth() {
});
}
function checkAuthentication(showLoginToast = true) {
return sendRequest("checkAuth.php")
.then(data => {
if (data.setup) {
window.setupMode = true;
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
toggleVisibility("addUserModal", true);
return false;
}
window.setupMode = false;
if (data.authenticated) {
return data;
} else {
if (showLoginToast) showToast("Please log in to continue.");
toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false);
toggleVisibility("fileListContainer", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
return false;
}
})
.catch(error => {
console.error("Error checking authentication:", error);
return false;
});
}
window.checkAuthentication = checkAuthentication;
window.changeItemsPerPage = function (value) {
localStorage.setItem("itemsPerPage", value);
const folder = window.currentFolder || "root";
@@ -307,16 +306,12 @@ window.changeItemsPerPage = function (value) {
};
document.addEventListener("DOMContentLoaded", function () {
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
updateItemsPerPageSelect();
});
function resetUserForm() {
document.getElementById("newUsername").value = "";
document.getElementById("addUserPassword").value = ""; // Updated for add user modal
document.getElementById("addUserPassword").value = "";
}
function closeAddUserModal() {

View File

@@ -99,20 +99,25 @@ if ($userRole !== false) {
// Generate a secure random token.
$token = bin2hex(random_bytes(32));
$expiry = time() + (30 * 24 * 60 * 60); // 30 days
// Load existing persistent tokens.
$persistentTokens = [];
if (file_exists($persistentTokensFile)) {
$persistentTokens = json_decode(file_get_contents($persistentTokensFile), true);
$encryptedContent = file_get_contents($persistentTokensFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
$persistentTokens = json_decode($decryptedContent, true);
if (!is_array($persistentTokens)) {
$persistentTokens = [];
}
}
// Save token along with username and expiry.
// Save token along with username, expiry, and admin status.
$persistentTokens[$token] = [
"username" => $username,
"expiry" => $expiry
"expiry" => $expiry,
"isAdmin" => ($userRole === "1")
];
file_put_contents($persistentTokensFile, json_encode($persistentTokens, JSON_PRETTY_PRINT));
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
// Set the cookie. (Assuming $secure is defined in config.php.)
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
}

View File

@@ -1,6 +1,57 @@
<?php
// config.php
// Define constants first.
define('UPLOAD_DIR', '/var/www/uploads/');
define('USERS_DIR', '/var/www/users/');
define('USERS_FILE', 'users.txt');
define('META_DIR', '/var/www/metadata/');
define('META_FILE', 'file_metadata.json');
define('TRASH_DIR', UPLOAD_DIR . 'trash/');
define('TIMEZONE', 'America/New_York');
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
define('TOTAL_UPLOAD_SIZE', '5G');
// Set the default timezone.
date_default_timezone_set(TIMEZONE);
/**
* Encrypts data using AES-256-CBC.
*
* @param string $data The plaintext data.
* @param string $encryptionKey The secret encryption key.
* @return string Base64-encoded string containing IV and ciphertext.
*/
function encryptData($data, $encryptionKey) {
$cipher = 'AES-256-CBC';
$ivlen = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivlen);
$ciphertext = openssl_encrypt($data, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv . $ciphertext);
}
/**
* Decrypts data encrypted with AES-256-CBC.
*
* @param string $encryptedData The Base64-encoded data containing IV and ciphertext.
* @param string $encryptionKey The secret encryption key.
* @return string|false The decrypted plaintext or false on failure.
*/
function decryptData($encryptedData, $encryptionKey) {
$cipher = 'AES-256-CBC';
$data = base64_decode($encryptedData);
$ivlen = openssl_cipher_iv_length($cipher);
$iv = substr($data, 0, $ivlen);
$ciphertext = substr($data, $ivlen);
return openssl_decrypt($ciphertext, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
}
// Load encryption key from an environment variable (default for testing; override in production)
$encryptionKey = getenv('PERSISTENT_TOKENS_KEY') ?: 'default_please_change_this_key';
if (!$encryptionKey) {
die('Encryption key for persistent tokens is not set.');
}
// Allow an environment variable to override HTTPS detection.
$envSecure = getenv('SECURE');
if ($envSecure !== false) {
@@ -23,6 +74,7 @@ session_set_cookie_params($cookieParams);
ini_set('session.gc_maxlifetime', 7200);
session_start();
// Generate CSRF token if not already set.
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
@@ -30,22 +82,28 @@ if (empty($_SESSION['csrf_token'])) {
// Auto-login via persistent token if session is not active.
if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token'])) {
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
$persistentTokens = [];
if (file_exists($persistentTokensFile)) {
$persistentTokens = json_decode(file_get_contents($persistentTokensFile), true);
if (is_array($persistentTokens) && isset($persistentTokens[$_COOKIE['remember_me_token']])) {
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
if ($tokenData['expiry'] >= time()) {
// Token is valid; auto-authenticate the user.
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $tokenData["username"];
// Optionally, set admin status if stored in token data:
// $_SESSION["isAdmin"] = $tokenData["isAdmin"];
} else {
// Token expired; remove it and clear the cookie.
unset($persistentTokens[$_COOKIE['remember_me_token']]);
file_put_contents($persistentTokensFile, json_encode($persistentTokens, JSON_PRETTY_PRINT));
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
}
$encryptedContent = file_get_contents($persistentTokensFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
$persistentTokens = json_decode($decryptedContent, true);
if (!is_array($persistentTokens)) {
$persistentTokens = [];
}
}
if (is_array($persistentTokens) && isset($persistentTokens[$_COOKIE['remember_me_token']])) {
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
if ($tokenData['expiry'] >= time()) {
// Token is valid; auto-authenticate the user.
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $tokenData["username"];
$_SESSION["isAdmin"] = $tokenData["isAdmin"]; // Restore admin status from the token
} else {
// Token expired; remove it and clear the cookie.
unset($persistentTokens[$_COOKIE['remember_me_token']]);
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
}
}
}
@@ -64,15 +122,4 @@ if (strpos(BASE_URL, 'yourwebsite') !== false) {
}
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
define('UPLOAD_DIR', '/var/www/uploads/');
define('TIMEZONE', 'America/New_York');
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
define('TOTAL_UPLOAD_SIZE', '5G');
define('USERS_DIR', '/var/www/users/');
define('USERS_FILE', 'users.txt');
define('META_DIR','/var/www/metadata/');
define('META_FILE','file_metadata.json');
define('TRASH_DIR', UPLOAD_DIR . 'trash/');
date_default_timezone_set(TIMEZONE);
?>

View File

@@ -28,35 +28,39 @@ export function toggleAllCheckboxes(masterCheckbox) {
}
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 zipBtn = document.getElementById("downloadZipBtn");
const extractZipBtn = document.getElementById("extractZipBtn");
if (fileCheckboxes.length === 0) {
if (copyBtn) copyBtn.style.display = "none";
if (moveBtn) moveBtn.style.display = "none";
if (deleteBtn) deleteBtn.style.display = "none";
if (zipBtn) zipBtn.style.display = "none";
if (extractZipBtn) extractZipBtn.style.display = "none";
} else {
if (copyBtn) copyBtn.style.display = "inline-block";
if (moveBtn) moveBtn.style.display = "inline-block";
if (deleteBtn) deleteBtn.style.display = "inline-block";
if (zipBtn) zipBtn.style.display = "inline-block";
if (extractZipBtn) extractZipBtn.style.display = "inline-block";
if (selectedCheckboxes.length > 0) {
if (copyBtn) copyBtn.disabled = false;
if (moveBtn) moveBtn.disabled = false;
if (deleteBtn) deleteBtn.disabled = false;
if (zipBtn) zipBtn.disabled = false;
} else {
if (copyBtn) copyBtn.disabled = true;
if (moveBtn) moveBtn.disabled = true;
if (deleteBtn) deleteBtn.disabled = true;
if (zipBtn) zipBtn.disabled = true;
const anySelected = selectedCheckboxes.length > 0;
if (copyBtn) copyBtn.disabled = !anySelected;
if (moveBtn) moveBtn.disabled = !anySelected;
if (deleteBtn) deleteBtn.disabled = !anySelected;
if (zipBtn) zipBtn.disabled = !anySelected;
if (extractZipBtn) {
// Enable only if at least one selected file ends with .zip (case-insensitive).
const anyZipSelected = Array.from(selectedCheckboxes).some(chk =>
chk.value.toLowerCase().endsWith(".zip")
);
extractZipBtn.disabled = !anyZipSelected;
}
}
}
@@ -305,4 +309,53 @@ export function previewFile(fileUrl, fileName) {
}
modal.style.display = "flex";
}
export function attachEnterKeyListener(modalId, buttonId) {
const modal = document.getElementById(modalId);
if (modal) {
// Make the modal focusable
modal.setAttribute("tabindex", "-1");
modal.focus();
modal.addEventListener("keydown", function(e) {
if (e.key === "Enter") {
e.preventDefault();
const btn = document.getElementById(buttonId);
if (btn) {
btn.click();
}
}
});
}
}
export function showCustomConfirmModal(message) {
return new Promise((resolve) => {
const modal = document.getElementById("customConfirmModal");
const messageElem = document.getElementById("confirmMessage");
const yesBtn = document.getElementById("confirmYesBtn");
const noBtn = document.getElementById("confirmNoBtn");
messageElem.textContent = message;
modal.style.display = "block";
// Cleanup function to hide the modal and remove event listeners.
function cleanup() {
modal.style.display = "none";
yesBtn.removeEventListener("click", onYes);
noBtn.removeEventListener("click", onNo);
}
function onYes() {
cleanup();
resolve(true);
}
function onNo() {
cleanup();
resolve(false);
}
yesBtn.addEventListener("click", onYes);
noBtn.addEventListener("click", onNo);
});
}

146
extractZip.php Normal file
View File

@@ -0,0 +1,146 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// --- CSRF Protection ---
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
http_response_code(403);
echo json_encode(["error" => "Invalid CSRF token"]);
exit;
}
// Ensure user is authenticated.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(401);
echo json_encode(["error" => "Unauthorized"]);
exit;
}
// Read and decode the JSON input.
$rawData = file_get_contents("php://input");
$data = json_decode($rawData, true);
if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) {
http_response_code(400);
echo json_encode(["error" => "Invalid input."]);
exit;
}
$folder = $data['folder'];
$files = $data['files'];
if (empty($files)) {
http_response_code(400);
echo json_encode(["error" => "No files specified."]);
exit;
}
// Validate folder name (allow "root" or valid subfolder names).
if ($folder !== "root") {
$parts = explode('/', $folder);
foreach ($parts as $part) {
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
http_response_code(400);
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
}
$relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
} else {
$relativePath = "";
}
$baseDir = realpath(UPLOAD_DIR);
if ($baseDir === false) {
http_response_code(500);
echo json_encode(["error" => "Uploads directory not configured correctly."]);
exit;
}
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath;
$folderPathReal = realpath($folderPath);
if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
http_response_code(404);
echo json_encode(["error" => "Folder not found."]);
exit;
}
// ---------- Metadata Setup ----------
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
$srcMetaFile = getMetadataFilePath($folder);
$destMetaFile = getMetadataFilePath($folder);
$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : [];
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
$errors = [];
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
$allSuccess = true;
// ---------- Process Each File ----------
foreach ($files as $zipFileName) {
$originalName = basename(trim($zipFileName));
// Process only .zip files.
if (strtolower(substr($originalName, -4)) !== '.zip') {
continue;
}
if (!preg_match($safeFileNamePattern, $originalName)) {
$errors[] = "$originalName has an invalid name.";
$allSuccess = false;
continue;
}
$zipFilePath = $folderPathReal . DIRECTORY_SEPARATOR . $originalName;
if (!file_exists($zipFilePath)) {
$errors[] = "$originalName does not exist in folder.";
$allSuccess = false;
continue;
}
$zip = new ZipArchive();
if ($zip->open($zipFilePath) !== TRUE) {
$errors[] = "Could not open $originalName as a zip file.";
$allSuccess = false;
continue;
}
// Attempt extraction.
if (!$zip->extractTo($folderPathReal)) {
$errors[] = "Failed to extract $originalName.";
$allSuccess = false;
} else {
// Update metadata for each extracted file if the zip file has metadata.
if (isset($srcMetadata[$originalName])) {
$zipMeta = $srcMetadata[$originalName];
// Iterate through all entries in the zip.
for ($i = 0; $i < $zip->numFiles; $i++) {
$entryName = $zip->getNameIndex($i);
$extractedFileName = basename($entryName);
if ($extractedFileName) {
$destMetadata[$extractedFileName] = $zipMeta;
}
}
}
}
$zip->close();
}
// Write updated metadata back to the destination metadata file.
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
$errors[] = "Failed to update metadata.";
$allSuccess = false;
}
if ($allSuccess) {
echo json_encode(["success" => true]);
} else {
echo json_encode(["success" => false, "error" => implode(" ", $errors)]);
}
exit;
?>

View File

@@ -9,6 +9,7 @@ import {
showToast,
updateRowHighlight,
toggleRowSelection,
attachEnterKeyListener,
previewFile as originalPreviewFile
} from './domUtils.js';
@@ -661,6 +662,7 @@ export function handleDeleteSelected(e) {
document.getElementById("deleteFilesMessage").textContent =
"Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?";
document.getElementById("deleteFilesModal").style.display = "block";
attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles");
}
document.addEventListener("DOMContentLoaded", function () {
@@ -671,6 +673,7 @@ document.addEventListener("DOMContentLoaded", function () {
window.filesToDelete = [];
});
}
const confirmDelete = document.getElementById("confirmDeleteFiles");
if (confirmDelete) {
confirmDelete.addEventListener("click", function () {
@@ -700,7 +703,7 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
});
attachEnterKeyListener("downloadZipModal", "confirmDownloadZip");
export function handleDownloadZipSelected(e) {
e.preventDefault();
e.stopImmediatePropagation();
@@ -711,6 +714,64 @@ export function handleDownloadZipSelected(e) {
}
window.filesToDownload = Array.from(checkboxes).map(chk => chk.value);
document.getElementById("downloadZipModal").style.display = "block";
setTimeout(() => {
const input = document.getElementById("zipFileNameInput");
input.focus();
}, 100);
}
export function handleExtractZipSelected(e) {
if (e) {
e.preventDefault();
e.stopImmediatePropagation();
}
// Get selected file names
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
if (!checkboxes.length) {
showToast("No files selected.");
return;
}
// Filter for zip files only
const zipFiles = Array.from(checkboxes)
.map(chk => chk.value)
.filter(name => name.toLowerCase().endsWith(".zip"));
if (!zipFiles.length) {
showToast("No zip files selected.");
return;
}
// Call the extract endpoint with the selected zip files
fetch("extractZip.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify({
folder: window.currentFolder || "root",
files: zipFiles
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Zip file(s) extracted successfully!");
loadFileList(window.currentFolder);
} else {
showToast("Error extracting zip: " + (data.error || "Unknown error"));
}
})
.catch(error => {
console.error("Error extracting zip files:", error);
showToast("Error extracting zip files.");
});
}
const extractZipBtn = document.getElementById("extractZipBtn");
if (extractZipBtn) {
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
}
document.addEventListener("DOMContentLoaded", function () {
@@ -720,6 +781,7 @@ document.addEventListener("DOMContentLoaded", function () {
document.getElementById("downloadZipModal").style.display = "none";
});
}
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
if (confirmDownloadZip) {
confirmDownloadZip.addEventListener("click", function () {
@@ -1035,7 +1097,7 @@ export function editFile(fileName, folder) {
fetch(fileUrl, { method: "HEAD" })
.then(response => {
const contentLength = response.headers.get("Content-Length");
if (!contentLength || parseInt(contentLength) > 10485760) {
if (contentLength !== null && parseInt(contentLength) > 10485760) {
showToast("This file is larger than 10 MB and cannot be edited in the browser.");
throw new Error("File too large.");
}
@@ -1196,13 +1258,28 @@ export function initFileActions() {
downloadZipBtn.replaceWith(downloadZipBtn.cloneNode(true));
document.getElementById("downloadZipBtn").addEventListener("click", handleDownloadZipSelected);
}
const extractZipBtn = document.getElementById("extractZipBtn");
if (extractZipBtn) {
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
}
}
attachEnterKeyListener("renameFileModal", "submitRenameFile");
export function renameFile(oldName, folder) {
window.fileToRename = oldName;
window.fileFolder = folder || window.currentFolder || "root";
document.getElementById("newFileName").value = oldName;
document.getElementById("renameFileModal").style.display = "block";
setTimeout(() => {
const input = document.getElementById("newFileName");
input.focus();
const lastDot = oldName.lastIndexOf('.');
if (lastDot > 0) {
input.setSelectionRange(0, lastDot);
} else {
input.select();
}
}, 100);
}
document.addEventListener("DOMContentLoaded", () => {
@@ -1213,6 +1290,7 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("newFileName").value = "";
});
}
const submitBtn = document.getElementById("submitRenameFile");
if (submitBtn) {
submitBtn.addEventListener("click", function () {
@@ -1273,4 +1351,164 @@ document.addEventListener("DOMContentLoaded", function () {
el.addEventListener("dragleave", folderDragLeaveHandler);
el.addEventListener("drop", folderDropHandler);
});
});
});
document.addEventListener("keydown", function(e) {
// Skip if focus is on an input, textarea, or any contentEditable element.
const tag = e.target.tagName.toLowerCase();
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
return;
}
// On Mac, the delete key is often reported as "Backspace" (keyCode 8)
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) {
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
if (selectedCheckboxes.length > 0) {
e.preventDefault(); // Prevent default back navigation in some browsers.
handleDeleteSelected(new Event("click"));
}
}
});
// ---------- CONTEXT MENU SUPPORT FOR FILE LIST ----------
// Function to display the context menu with provided items at (x, y)
function showFileContextMenu(x, y, menuItems) {
let menu = document.getElementById("fileContextMenu");
if (!menu) {
menu = document.createElement("div");
menu.id = "fileContextMenu";
menu.style.position = "absolute";
menu.style.backgroundColor = "#fff";
menu.style.border = "1px solid #ccc";
menu.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.2)";
menu.style.zIndex = "9999";
menu.style.padding = "5px 0";
menu.style.minWidth = "150px";
document.body.appendChild(menu);
}
// Clear previous items
menu.innerHTML = "";
menuItems.forEach(item => {
let menuItem = document.createElement("div");
menuItem.textContent = item.label;
menuItem.style.padding = "5px 15px";
menuItem.style.cursor = "pointer";
menuItem.addEventListener("mouseover", () => {
if (document.body.classList.contains("dark-mode")) {
menuItem.style.backgroundColor = "#444"; // darker gray for dark mode
} else {
menuItem.style.backgroundColor = "#f0f0f0"; // light gray for light mode
}
});
menuItem.addEventListener("mouseout", () => {
menuItem.style.backgroundColor = "";
});
menuItem.addEventListener("click", () => {
item.action();
hideFileContextMenu();
});
menu.appendChild(menuItem);
});
menu.style.left = x + "px";
menu.style.top = y + "px";
menu.style.display = "block";
}
function hideFileContextMenu() {
const menu = document.getElementById("fileContextMenu");
if (menu) {
menu.style.display = "none";
}
}
// Context menu handler for the file list.
function fileListContextMenuHandler(e) {
e.preventDefault();
// If no file is selected, try to select the row that was right-clicked.
let row = e.target.closest("tr");
if (row) {
const checkbox = row.querySelector(".file-checkbox");
if (checkbox && !checkbox.checked) {
checkbox.checked = true;
updateRowHighlight(checkbox);
updateFileActionButtons();
}
}
// Get selected file names.
const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value);
// Build the context menu items.
let menuItems = [
{ label: "Delete Selected", action: () => { handleDeleteSelected(new Event("click")); } },
{ label: "Copy Selected", action: () => { handleCopySelected(new Event("click")); } },
{ label: "Move Selected", action: () => { handleMoveSelected(new Event("click")); } },
{ label: "Download Zip", action: () => { handleDownloadZipSelected(new Event("click")); } }
];
if (selected.some(name => name.toLowerCase().endsWith(".zip"))) {
menuItems.push({
label: "Extract Zip",
action: () => { handleExtractZipSelected(new Event("click")); }
});
}
if (selected.length === 1) {
// Look up the file object.
const file = fileData.find(f => f.name === selected[0]);
// Add Preview option.
menuItems.push({
label: "Preview",
action: () => {
const folder = window.currentFolder || "root";
const folderPath = folder === "root"
? "uploads/"
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
previewFile(folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime(), file.name);
}
});
// Only show Edit option if file is editable.
if (canEditFile(file.name)) {
menuItems.push({
label: "Edit",
action: () => { editFile(selected[0], window.currentFolder); }
});
}
// Add Rename option.
menuItems.push({
label: "Rename",
action: () => { renameFile(selected[0], window.currentFolder); }
});
}
showFileContextMenu(e.pageX, e.pageY, menuItems);
}
// Bind the context menu to the file list container.
// (This is set every time the file list is rendered.)
function bindFileListContextMenu() {
const fileListContainer = document.getElementById("fileList");
if (fileListContainer) {
fileListContainer.oncontextmenu = fileListContextMenuHandler;
}
}
// Hide the context menu if clicking anywhere else.
document.addEventListener("click", function(e) {
const menu = document.getElementById("fileContextMenu");
if (menu && menu.style.display === "block") {
hideFileContextMenu();
}
});
// After rendering the file table, bind the context menu handler.
(function() {
const originalRenderFileTable = renderFileTable;
renderFileTable = function(folder) {
originalRenderFileTable(folder);
bindFileListContextMenu();
};
})();

View File

@@ -1,7 +1,7 @@
// folderManager.js
import { loadFileList } from './fileManager.js';
import { showToast, escapeHTML } from './domUtils.js';
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
// ----------------------
// Helper Functions (Data/State)
@@ -90,7 +90,6 @@ function bindBreadcrumbEvents() {
link.addEventListener("click", function (e) {
e.stopPropagation();
let folder = this.getAttribute("data-folder");
console.log("Breadcrumb clicked, folder:", folder);
window.currentFolder = folder;
localStorage.setItem("lastOpenedFolder", folder);
const titleEl = document.getElementById("fileListTitle");
@@ -447,6 +446,7 @@ export function loadFolderList(selectedFolder) {
// ----------------------
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
function openRenameFolderModal() {
@@ -458,13 +458,19 @@ function openRenameFolderModal() {
const parts = selectedFolder.split("/");
document.getElementById("newRenameFolderName").value = parts[parts.length - 1];
document.getElementById("renameFolderModal").style.display = "block";
// Focus the input field after a short delay to ensure modal is visible.
setTimeout(() => {
const input = document.getElementById("newRenameFolderName");
input.focus();
input.select();
}, 100);
}
document.getElementById("cancelRenameFolder").addEventListener("click", function () {
document.getElementById("renameFolderModal").style.display = "none";
document.getElementById("newRenameFolderName").value = "";
});
attachEnterKeyListener("renameFolderModal", "submitRenameFolder");
document.getElementById("submitRenameFolder").addEventListener("click", function (event) {
event.preventDefault();
const selectedFolder = window.currentFolder || "root";
@@ -521,7 +527,7 @@ function openDeleteFolderModal() {
document.getElementById("cancelDeleteFolder").addEventListener("click", function () {
document.getElementById("deleteFolderModal").style.display = "none";
});
attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder");
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
const selectedFolder = window.currentFolder || "root";
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
@@ -552,13 +558,14 @@ document.getElementById("confirmDeleteFolder").addEventListener("click", functio
document.getElementById("createFolderBtn").addEventListener("click", function () {
document.getElementById("createFolderModal").style.display = "block";
document.getElementById("newFolderName").focus();
});
document.getElementById("cancelCreateFolder").addEventListener("click", function () {
document.getElementById("createFolderModal").style.display = "none";
document.getElementById("newFolderName").value = "";
});
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
document.getElementById("submitCreateFolder").addEventListener("click", function () {
const folderInput = document.getElementById("newFolderName").value.trim();
if (!folderInput) {
@@ -599,4 +606,150 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
console.error("Error creating folder:", error);
document.getElementById("createFolderModal").style.display = "none";
});
});
});
// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ----------
// Function to display the custom context menu at (x, y) with given menu items.
function showFolderManagerContextMenu(x, y, menuItems) {
let menu = document.getElementById("folderManagerContextMenu");
if (!menu) {
menu = document.createElement("div");
menu.id = "folderManagerContextMenu";
menu.style.position = "absolute";
menu.style.padding = "5px 0";
menu.style.minWidth = "150px";
menu.style.zIndex = "9999";
document.body.appendChild(menu);
}
// Set styles based on dark mode.
if (document.body.classList.contains("dark-mode")) {
menu.style.backgroundColor = "#2c2c2c";
menu.style.border = "1px solid #555";
menu.style.color = "#e0e0e0";
} else {
menu.style.backgroundColor = "#fff";
menu.style.border = "1px solid #ccc";
menu.style.color = "#000";
}
// Clear previous items.
menu.innerHTML = "";
menuItems.forEach(item => {
const menuItem = document.createElement("div");
menuItem.textContent = item.label;
menuItem.style.padding = "5px 15px";
menuItem.style.cursor = "pointer";
menuItem.addEventListener("mouseover", () => {
if (document.body.classList.contains("dark-mode")) {
menuItem.style.backgroundColor = "#444";
} else {
menuItem.style.backgroundColor = "#f0f0f0";
}
});
menuItem.addEventListener("mouseout", () => {
menuItem.style.backgroundColor = "";
});
menuItem.addEventListener("click", () => {
item.action();
hideFolderManagerContextMenu();
});
menu.appendChild(menuItem);
});
menu.style.left = x + "px";
menu.style.top = y + "px";
menu.style.display = "block";
}
function hideFolderManagerContextMenu() {
const menu = document.getElementById("folderManagerContextMenu");
if (menu) {
menu.style.display = "none";
}
}
// Context menu handler for folder tree and breadcrumb items.
function folderManagerContextMenuHandler(e) {
e.preventDefault();
e.stopPropagation();
// Get the closest folder element (either from the tree or breadcrumb).
const target = e.target.closest(".folder-option, .breadcrumb-link");
if (!target) return;
const folder = target.getAttribute("data-folder");
if (!folder) return;
// Update current folder and highlight the selected element.
window.currentFolder = folder;
document.querySelectorAll(".folder-option, .breadcrumb-link").forEach(el => el.classList.remove("selected"));
target.classList.add("selected");
// Build context menu items.
const menuItems = [
{
label: "Create Folder",
action: () => {
document.getElementById("createFolderModal").style.display = "block";
document.getElementById("newFolderName").focus();
}
},
{
label: "Rename Folder",
action: () => { openRenameFolderModal(); }
},
{
label: "Delete Folder",
action: () => { openDeleteFolderModal(); }
}
];
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
}
// Bind contextmenu events to folder tree and breadcrumb elements.
function bindFolderManagerContextMenu() {
// Bind context menu to folder tree container.
const container = document.getElementById("folderTreeContainer");
if (container) {
container.removeEventListener("contextmenu", folderManagerContextMenuHandler);
container.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
}
// Bind context menu to breadcrumb links.
const breadcrumbNodes = document.querySelectorAll(".breadcrumb-link");
breadcrumbNodes.forEach(node => {
node.removeEventListener("contextmenu", folderManagerContextMenuHandler);
node.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
});
}
// Hide context menu when clicking elsewhere.
document.addEventListener("click", function () {
hideFolderManagerContextMenu();
});
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("keydown", function(e) {
// Skip if the user is typing in an input, textarea, or contentEditable element.
const tag = e.target.tagName.toLowerCase();
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
return;
}
// On macOS, "Delete" is typically reported as "Backspace" (keyCode 8)
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) {
// Ensure a folder is selected and it isn't the root folder.
if (window.currentFolder && window.currentFolder !== "root") {
// Prevent default (avoid navigating back on macOS).
e.preventDefault();
// Call your existing folder delete function.
openDeleteFolderModal();
}
}
});
});
// Call this binding function after rendering the folder tree and breadcrumbs.
bindFolderManagerContextMenu();

View File

@@ -302,8 +302,8 @@
</div>
</div>
</div>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button>
<button id="extractZipBtn" class="btn btn-sm btn-info" title="Extract Zip">Extract Zip</button>
<!-- Download Zip Modal -->
<div id="downloadZipModal" class="modal" style="display:none;">
<div class="modal-content">

View File

@@ -1,19 +1,37 @@
<?php
session_start();
require 'config.php';
// Retrieve headers and check CSRF token.
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
// Fallback: If a CSRF token exists in the session and doesn't match the one provided,
// log the mismatch but proceed with logout.
// If there's a mismatch, log it but continue with logout.
if (isset($_SESSION['csrf_token']) && $receivedToken !== $_SESSION['csrf_token']) {
// Optionally log this event:
error_log("CSRF token mismatch on logout. Proceeding with logout.");
}
$_SESSION = []; // Clear session data
session_destroy(); // Destroy session
// If the remember me token is set, remove it from the persistent tokens file.
if (isset($_COOKIE['remember_me_token'])) {
$token = $_COOKIE['remember_me_token'];
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
if (file_exists($persistentTokensFile)) {
$encryptedContent = file_get_contents($persistentTokensFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
$persistentTokens = json_decode($decryptedContent, true);
if (is_array($persistentTokens) && isset($persistentTokens[$token])) {
unset($persistentTokens[$token]);
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
}
}
// Clear the cookie.
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
}
header('Content-Type: application/json');
echo json_encode(["success" => "Logged out"]);
// Clear session data and destroy the session.
$_SESSION = [];
session_destroy();
header("Location: index.html");
exit;
?>

View File

@@ -1,4 +1,3 @@
// networkUtils.js
export function sendRequest(url, method = "GET", data = null) {
console.log("Sending request to:", url, "with method:", method);
const options = {
@@ -24,9 +23,11 @@ export function sendRequest(url, method = "GET", data = null) {
throw new Error(`HTTP error ${response.status}: ${text}`);
});
}
// Clone the response so we can safely fall back if JSON parsing fails.
const clonedResponse = response.clone();
return response.json().catch(() => {
console.warn("Response is not JSON, returning as text");
return response.text();
return clonedResponse.text();
});
});
}

View File

@@ -216,6 +216,7 @@ body.dark-mode header {
background-color: rgba(255, 255, 255, 0.2);
}
/* Folder Help Tooltip - Light Mode */
.folder-help-tooltip {
background-color: #fff;
color: #333;
@@ -225,6 +226,7 @@ body.dark-mode header {
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2);
}
/* Folder Help Tooltip - Dark Mode */
body.dark-mode .folder-help-tooltip {
background-color: #333 !important;
color: #eee !important;
@@ -713,12 +715,25 @@ body.dark-mode .editor-header {
.material-icons.pauseResumeBtn {
color: black !important;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease;
}
body.dark-mode .material-icons.pauseResumeBtn {
color: white !important;
}
body.dark-mode .material-icons.pauseResumeBtn:hover {
background-color: rgba(255, 215, 0, 0.3);
color: #fff;
}
body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
background-color: rgba(0, 0, 0, 0.1);
color: #000;
}
#uploadProgressContainer ul {
list-style: none;
padding: 0;
@@ -1269,16 +1284,20 @@ body.dark-mode #fileListContainer {
/* ===========================================================
FOLDER TREE STYLES
=========================================================== */
/* Make breadcrumb links look clickable */
.breadcrumb-link {
cursor: pointer;
color: #007bff;
/* Blue color, for example */
text-decoration: underline;
}
/* Change color on hover */
.breadcrumb-link:hover {
color: #0056b3;
}
/* Style for the selected breadcrumb */
.breadcrumb-link.selected {
background-color: #e9ecef;
font-weight: bold;
@@ -1334,11 +1353,17 @@ body.dark-mode #fileListContainer {
.image-modal-header {
display: flex;
align-items: center;
/* Vertically center the text within a fixed height */
justify-content: center;
/* Center horizontally */
white-space: nowrap;
/* Prevent wrapping */
overflow: hidden;
/* Hide any overflowing text */
text-overflow: ellipsis;
/* Truncate with an ellipsis */
height: 25px;
/* Fixed height for a single line */
padding: 5px;
margin-bottom: 10px;
max-width: 90%;
@@ -1858,4 +1883,23 @@ body.dark-mode .drop-hover {
#restoreFilesList li label {
margin-left: 8px !important;
}
body.dark-mode #fileContextMenu {
background-color: #2c2c2c !important;
border: 1px solid #555 !important;
color: #e0e0e0 !important;
}
body.dark-mode #fileContextMenu div {
color: #e0e0e0 !important;
}
#folderContextMenu {
font-family: Arial, sans-serif;
font-size: 14px;
}
body.dark-mode #folderContextMenu {
background-color: #2c2c2c;
border-color: #555;
color: #e0e0e0;
}

View File

@@ -4,20 +4,13 @@ import { toggleVisibility, showToast } from './domUtils.js';
import { loadFileList } from './fileManager.js';
import { loadFolderTree } from './folderManager.js';
/**
* Displays a custom confirmation modal with the given message.
* Calls onConfirm() if the user confirms.
*/
function showConfirm(message, onConfirm) {
// Assume your custom confirm modal exists with id "customConfirmModal"
// and has elements "confirmMessage", "confirmYesBtn", and "confirmNoBtn".
const modal = document.getElementById("customConfirmModal");
const messageElem = document.getElementById("confirmMessage");
const yesBtn = document.getElementById("confirmYesBtn");
const noBtn = document.getElementById("confirmNoBtn");
if (!modal || !messageElem || !yesBtn || !noBtn) {
// Fallback to browser confirm if custom modal is not found.
if (confirm(message)) {
onConfirm();
}
@@ -42,10 +35,6 @@ function showConfirm(message, onConfirm) {
});
}
/**
* Sets up event listeners for trash restore and delete operations.
* This function should be called from main.js after authentication.
*/
export function setupTrashRestoreDelete() {
// --- Attach listener to the restore button (created in auth.js) to open the modal.