Compare commits

...

18 Commits

Author SHA1 Message Date
Ryan
a2d678ee19 1.0.7 2025-04-04 18:18:19 -04:00
Ryan
da62e70c02 mitigate path traversal vulnerability by validating folder and file inputs 2025-04-04 18:02:21 -04:00
Ryan
f19d30f58a demo.filerise.net 2025-04-04 16:16:21 -04:00
Ryan
a8202adbec demo.filerise.net 2025-04-04 16:16:01 -04:00
Ryan
5dc58ffa42 loadCsrfTokenWithRetry 2025-04-04 02:29:27 -04:00
Ryan
f4f700ecda Chain Initialization After CSRF Token Is Loaded 2025-04-04 02:13:00 -04:00
Ryan
94178775d5 loadUserPermissions cleanup 2025-04-04 01:58:36 -04:00
Ryan
1d3f731483 fix UserPermission missing function. 2025-04-03 23:24:43 -04:00
Ryan
6926d5b065 userPermissions issue fixed 2025-04-03 22:06:49 -04:00
Ryan
46e9761cae Add click event listener to the “oidcLoginBtn” 2025-04-03 21:43:32 -04:00
Ryan
fa828f5dea new image 2025-04-03 20:13:41 -04:00
Ryan
3a86903827 v1.0.6 2025-04-03 20:05:14 -04:00
Ryan
4feef5700d video demo2 2025-04-03 20:01:31 -04:00
Ryan
41e2b5af90 New Images 2025-04-03 19:56:04 -04:00
Ryan
27f071ba6e remove testing style 2025-04-03 16:28:55 -04:00
Ryan
9020251ed5 Header Drop Zone Extension 2025-04-03 15:12:48 -04:00
Ryan
84822e699e Fixed fileDragStartHandler to work with tagFiles. 2025-04-02 16:05:29 -04:00
Ryan
3d57efba6c Allow mkv video playback if supported and custom toast opacity increased 2025-04-02 14:41:58 -04:00
33 changed files with 850 additions and 502 deletions

View File

@@ -1,11 +1,16 @@
# FileRise - Elevate your File Management
**Video demo:**
**Demo link:** https://demo.filerise.net
**UserName:** demo
**Password:** demo
Read only permissions but can view the interface.
https://github.com/user-attachments/assets/9546a76b-afb0-4068-875a-0eab478b514d
**4/3/2025 Video demo:**
https://github.com/user-attachments/assets/221f6a53-85f5-48d4-9abe-89445e0af90e
**Dark mode:**
![Dark Mode](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-mode.png)
![Dark Header](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-header.png)
changelogs available here: <https://github.com/error311/FileRise-docker/>
@@ -147,6 +152,10 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
- **Top Bar Drop Zone:**
- A top drop zone is available for reordering or managing cards quickly.
- Dragging a card to the top drop zone provides immediate visual feedback, ensuring a fluid and customizable workflow.
- **Header Drop Zone with State Preservation:**
- Cards can be dragged into the header drop zone, where they are represented by a compact material icon.
- **State Preservation:** Instead of removing the card from the DOM, the original card is moved into a hidden container. This ensures that dynamic features (such as the folder tree in the Folder Management card or file selection in the Upload card) remain fully initialized and retain their state on page refresh.
- **Modal Display:** When the user interacts (via hover or click) with the header icon, the card is temporarily moved into a modal overlay for full interaction. When the modal is closed, the card is returned to the hidden container, keeping its state persistent.
- **Seamless Interaction:**
- Both drop zones support smooth drag-and-drop interactions with animations and pointer event adjustments, ensuring reliable card placement regardless of screen position.
@@ -170,23 +179,23 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
- Features an intuitive interface with Material Icons for quick recognition and access.
- Allows administrators to manage authentication settings, user management, and login methods in real time.
- Includes real-time validation that prevents the accidental disabling of all authentication methods simultaneously.
- User Permissions options
- Folder Only gives user their own root folder
- Read Only makes it so user can only read the files
- Disable upload
- **User Permissions Options:**
- *Folder Only* gives user their own root folder.
- *Read Only* makes it so the user can only read the files.
- *Disable Upload* prevents file uploads.
---
## Screenshots
**Light mode:**
![Dark Admin Panel](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-admin-panel.png)
**Admin Panel:**
![Light Admin Panel](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-admin-panel.png)
**Light mode:**
![Light Mode](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-mode.png)
![Dark SideBar](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-sidebar.png)
**Dark mode default:**
![Default Layout](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-mode-default.png)
**Light mode default:**
![Default Layout](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-topbar.png)
**Dark editor:**
![dark-editor](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-editor.png)
@@ -197,8 +206,8 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
**Restore or Delete Trash:**
![restore-delete](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-trash.png)
**Dark Login page:**
![Login](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-login.png)
**Dark TOTP Setup:**
![Login](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-totp-setup.png)
**Gallery view:**
![Login](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-gallery.png)

View File

@@ -400,6 +400,13 @@ document.addEventListener("DOMContentLoaded", function () {
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
});
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
if (oidcLoginBtn) {
oidcLoginBtn.addEventListener("click", () => {
// Redirect to the OIDC auth endpoint. The endpoint can be adjusted if needed.
window.location.href = "auth.php?oidc=initiate";
});
}
});
export { initAuth, checkAuthentication };

View File

@@ -1,7 +1,7 @@
import { showToast, toggleVisibility } from './domUtils.js';
import { sendRequest } from './networkUtils.js';
const version = "v1.0.5";
const version = "v1.0.7";
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
let lastLoginData = null;

View File

@@ -55,7 +55,7 @@ if (!$encryptionKey) {
function loadUserPermissions($username)
{
global $encryptionKey; // Ensure $encryptionKey is available
global $encryptionKey;
$permissionsFile = USERS_DIR . 'userPermissions.json';
if (file_exists($permissionsFile)) {
@@ -69,21 +69,12 @@ function loadUserPermissions($username)
$permissions = json_decode($content, true);
}
if (!is_array($permissions)) {
} else {
}
if (is_array($permissions) && array_key_exists($username, $permissions)) {
$result = $permissions[$username];
if (empty($result)) {
return false;
return !empty($result) ? $result : false;
}
return $result;
} else {
}
} else {
error_log("loadUserPermissions: Permissions file not found: $permissionsFile");
}
// Removed error_log() to prevent flooding logs when file is not found.
return false; // Return false if no permissions found.
}
@@ -132,7 +123,7 @@ if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token']))
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $tokenData["username"];
// IMPORTANT: Set the folderOnly flag here for auto-login.
$_SESSION["folderOnly"] = loadFolderPermission($tokenData["username"]);
$_SESSION["folderOnly"] = loadUserPermissions($tokenData["username"]);
} else {
unset($persistentTokens[$_COOKIE['remember_me_token']]);
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);

View File

@@ -18,9 +18,8 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
exit;
}
$userPermissions = loadUserPermissions($username);
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {

View File

@@ -23,9 +23,9 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
http_response_code(403);
exit;
}
$userPermissions = loadUserPermissions($username);
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {

View File

@@ -23,9 +23,9 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
http_response_code(403);
exit;
}
$userPermissions = loadUserPermissions($username);
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {

View File

@@ -136,11 +136,11 @@ export function buildFileTableRow(file, folderPath) {
const safeUploader = escapeHTML(file.uploader || "Unknown");
let previewButton = "";
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus|mkv|ogv)$/i.test(file.name)) {
let previewIcon = "";
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
previewIcon = `<i class="material-icons">image</i>`;
} else if (/\.(mp4|webm|mov)$/i.test(file.name)) {
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(file.name)) {
previewIcon = `<i class="material-icons">videocam</i>`;
} else if (/\.pdf$/i.test(file.name)) {
previewIcon = `<i class="material-icons">picture_as_pdf</i>`;

View File

@@ -1,8 +1,6 @@
<?php
require_once 'config.php';
// For GET requests (which download.php will use), we assume session authentication is enough.
// Check if the user is authenticated.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(401);
@@ -22,38 +20,70 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $file)) {
exit;
}
// Determine the directory.
if ($folder !== 'root') {
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
} else {
$directory = UPLOAD_DIR;
// Get the realpath of the upload directory.
$uploadDirReal = realpath(UPLOAD_DIR);
if ($uploadDirReal === false) {
http_response_code(500);
echo json_encode(["error" => "Server misconfiguration."]);
exit;
}
$filePath = $directory . $file;
// Determine the directory.
if ($folder === 'root') {
$directory = $uploadDirReal;
} else {
// Prevent path traversal in folder parameter.
if (strpos($folder, '..') !== false) {
http_response_code(400);
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
if (!file_exists($filePath)) {
$directoryPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
$directory = realpath($directoryPath);
// Ensure that the resolved directory exists and is within the allowed UPLOAD_DIR.
if ($directory === false || strpos($directory, $uploadDirReal) !== 0) {
http_response_code(400);
echo json_encode(["error" => "Invalid folder path."]);
exit;
}
}
// Build the file path.
$filePath = $directory . DIRECTORY_SEPARATOR . $file;
$realFilePath = realpath($filePath);
// Validate that the real file path exists and is within the allowed directory.
if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
http_response_code(403);
echo json_encode(["error" => "Access forbidden."]);
exit;
}
if (!file_exists($realFilePath)) {
http_response_code(404);
echo json_encode(["error" => "File not found."]);
exit;
}
// Serve the file.
$mimeType = mime_content_type($filePath);
$mimeType = mime_content_type($realFilePath);
header("Content-Type: " . $mimeType);
// For images, serve inline; for other types, force download.
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
header('Content-Disposition: inline; filename="' . basename($filePath) . '"');
header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
} else {
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
}
header('Content-Length: ' . filesize($filePath));
header('Content-Length: ' . filesize($realFilePath));
// Disable caching.
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
readfile($filePath);
readfile($realFilePath);
exit;
?>

View File

@@ -1,4 +1,9 @@
// dragAndDrop.js
// This file handles drag-and-drop functionality for cards in the sidebar, header and top drop zones.
// It also manages the visibility of the sidebar and header drop zones based on the current state of the application.
// It includes functions to save and load the order of cards in the sidebar and header from localStorage.
// It also includes functions to handle the drag-and-drop events, including mouse movements and drop zones.
// It uses CSS classes to manage the appearance of the sidebar and header drop zones during drag-and-drop operations.
// Moves cards into the sidebar based on the saved order in localStorage.
export function loadSidebarOrder() {
@@ -27,6 +32,25 @@ export function loadSidebarOrder() {
updateSidebarVisibility();
}
// NEW: Load header order from localStorage.
export function loadHeaderOrder() {
const headerDropArea = document.getElementById('headerDropArea');
if (!headerDropArea) return;
const orderStr = localStorage.getItem('headerOrder');
if (orderStr) {
const order = JSON.parse(orderStr);
if (order.length > 0) {
order.forEach(id => {
const card = document.getElementById(id);
// Only load if card is not already in header drop zone.
if (card && card.parentNode.id !== 'headerDropArea') {
insertCardInHeader(card, null);
}
});
}
}
}
// Internal helper: update sidebar visibility based on its content.
function updateSidebarVisibility() {
const sidebar = document.getElementById('sidebarDropArea');
@@ -44,6 +68,17 @@ function updateSidebarVisibility() {
}
}
// NEW: Save header order to localStorage.
function saveHeaderOrder() {
const headerDropArea = document.getElementById('headerDropArea');
if (headerDropArea) {
const icons = Array.from(headerDropArea.children);
// Each header icon stores its associated card in the property cardElement.
const order = icons.map(icon => icon.cardElement.id);
localStorage.setItem('headerOrder', JSON.stringify(order));
}
}
// Internal helper: update top zone layout (center a card if one column is empty).
function updateTopZoneLayout() {
const leftCol = document.getElementById('leftCol');
@@ -183,7 +218,166 @@ function ensureTopZonePlaceholder() {
}
}
// This sets up all drag-and-drop event listeners for cards.
// --- NEW HELPER FUNCTIONS FOR HEADER DROP ZONE ---
// Show header drop zone and add a "drag-active" class so that the pseudo-element appears.
function showHeaderDropZone() {
const headerDropArea = document.getElementById('headerDropArea');
if (headerDropArea) {
headerDropArea.style.display = 'inline-flex';
headerDropArea.classList.add('drag-active');
}
}
// Hide header drop zone by removing the "drag-active" class.
// If a header icon is present (i.e. a card was dropped), the drop zone remains visible without the dashed border.
function hideHeaderDropZone() {
const headerDropArea = document.getElementById('headerDropArea');
if (headerDropArea) {
headerDropArea.classList.remove('drag-active');
if (headerDropArea.children.length === 0) {
headerDropArea.style.display = 'none';
}
}
}
// === NEW FUNCTION: Insert card into header drop zone as a material icon ===
function insertCardInHeader(card, event) {
const headerDropArea = document.getElementById('headerDropArea');
if (!headerDropArea) return;
// For folder management and upload cards, preserve the original by moving it to a hidden container.
if (card.id === 'folderManagementCard' || card.id === 'uploadCard') {
let hiddenContainer = document.getElementById('hiddenCardsContainer');
if (!hiddenContainer) {
hiddenContainer = document.createElement('div');
hiddenContainer.id = 'hiddenCardsContainer';
hiddenContainer.style.display = 'none';
document.body.appendChild(hiddenContainer);
}
// Move the original card to the hidden container if it's not already there.
if (card.parentNode.id !== 'hiddenCardsContainer') {
hiddenContainer.appendChild(card);
}
} else {
// For other cards, simply remove from current container.
if (card.parentNode) {
card.parentNode.removeChild(card);
}
}
// Create the header icon button.
const iconButton = document.createElement('button');
iconButton.className = 'header-card-icon';
// Remove default button styling.
iconButton.style.border = 'none';
iconButton.style.background = 'none';
iconButton.style.outline = 'none';
iconButton.style.cursor = 'pointer';
// Choose an icon based on the card type with 24px size.
if (card.id === 'uploadCard') {
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">cloud_upload</i>';
} else if (card.id === 'folderManagementCard') {
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">folder</i>';
} else {
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">insert_drive_file</i>';
}
// Save a reference to the card in the icon button.
iconButton.cardElement = card;
// Associate this icon with the card for future removal.
card.headerIconButton = iconButton;
let modal = null;
let isLocked = false;
let hoverActive = false;
// showModal: When triggered, ensure the card is attached to the modal.
function showModal() {
if (!modal) {
modal = document.createElement('div');
modal.className = 'header-card-modal';
modal.style.position = 'fixed';
modal.style.top = '80px';
modal.style.right = '80px';
modal.style.zIndex = '11000';
// Render the modal but initially keep it hidden.
modal.style.display = 'block';
modal.style.visibility = 'hidden';
modal.style.opacity = '0';
modal.style.background = 'none';
modal.style.border = 'none';
modal.style.padding = '0';
modal.style.boxShadow = 'none';
document.body.appendChild(modal);
// Attach modal hover events.
modal.addEventListener('mouseover', handleMouseOver);
modal.addEventListener('mouseout', handleMouseOut);
iconButton.modalInstance = modal;
}
// If the card isn't already in the modal, remove it from the hidden container and attach it.
if (!modal.contains(card)) {
const hiddenContainer = document.getElementById('hiddenCardsContainer');
if (hiddenContainer && hiddenContainer.contains(card)) {
hiddenContainer.removeChild(card);
}
modal.appendChild(card);
}
// Reveal the modal.
modal.style.visibility = 'visible';
modal.style.opacity = '1';
}
// hideModal: Hide the modal and return the card to the hidden container.
function hideModal() {
if (modal && !isLocked && !hoverActive) {
modal.style.visibility = 'hidden';
modal.style.opacity = '0';
// Return the card to the hidden container.
const hiddenContainer = document.getElementById('hiddenCardsContainer');
if (hiddenContainer && modal.contains(card)) {
hiddenContainer.appendChild(card);
}
}
}
function handleMouseOver() {
hoverActive = true;
showModal();
}
function handleMouseOut() {
hoverActive = false;
setTimeout(() => {
if (!hoverActive && !isLocked) {
hideModal();
}
}, 300);
}
// Attach hover events to the icon.
iconButton.addEventListener('mouseover', handleMouseOver);
iconButton.addEventListener('mouseout', handleMouseOut);
// Toggle the locked state on click so the modal stays open.
iconButton.addEventListener('click', (e) => {
isLocked = !isLocked;
if (isLocked) {
showModal();
} else {
hideModal();
}
e.stopPropagation();
});
// Append the header icon button into the header drop zone.
headerDropArea.appendChild(iconButton);
// Save the updated header order.
saveHeaderOrder();
}
// === Main Drag and Drop Initialization ===
export function initDragAndDrop() {
function run() {
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
@@ -205,7 +399,7 @@ export function initDragAndDrop() {
header.addEventListener('mousedown', function (e) {
e.preventDefault();
const card = this.closest('.card');
// Capture the card's initial bounding rectangle once.
// Capture the card's initial bounding rectangle.
const initialRect = card.getBoundingClientRect();
const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100;
const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100;
@@ -226,13 +420,28 @@ export function initDragAndDrop() {
sidebar.style.height = '800px';
}
// Use the stored initialRect rather than recalculating.
// Show header drop zone while dragging.
showHeaderDropZone();
// Use the stored initialRect.
initialLeft = initialRect.left + window.pageXOffset;
initialTop = initialRect.top + window.pageYOffset;
offsetX = e.pageX - initialLeft;
offsetY = e.pageY - initialTop;
// Append card to body and fix its dimensions to prevent shrinking.
// Remove any associated header icon if present.
if (card.headerIconButton) {
if (card.headerIconButton.parentNode) {
card.headerIconButton.parentNode.removeChild(card.headerIconButton);
}
if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) {
card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance);
}
card.headerIconButton = null;
saveHeaderOrder();
}
// Append card to body and fix its dimensions.
document.body.appendChild(card);
card.style.position = 'absolute';
card.style.left = initialLeft + 'px';
@@ -269,8 +478,21 @@ export function initDragAndDrop() {
sidebar.style.height = '';
}
// Remove any existing header icon if present.
if (card.headerIconButton) {
if (card.headerIconButton.parentNode) {
card.headerIconButton.parentNode.removeChild(card.headerIconButton);
}
if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) {
card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance);
}
card.headerIconButton = null;
saveHeaderOrder();
}
let droppedInSidebar = false;
let droppedInTop = false;
let droppedInHeader = false;
// Check if dropped in sidebar drop zone.
const sidebarElem = document.getElementById('sidebarDropArea');
@@ -287,7 +509,7 @@ export function initDragAndDrop() {
droppedInSidebar = true;
}
}
// If not dropped in sidebar, check the top drop zone.
// Check the top drop zone.
const topRow = document.getElementById('uploadFolderRow');
if (!droppedInSidebar && topRow) {
const rect = topRow.getBoundingClientRect();
@@ -308,21 +530,31 @@ export function initDragAndDrop() {
updateTopZoneLayout();
container.appendChild(card);
droppedInTop = true;
// Use computed style to determine container's width.
const containerWidth = parseFloat(window.getComputedStyle(container).width);
// Set a fixed width during animation.
card.style.width = "363px";
// Animate the card sliding in.
animateVerticalSlide(card);
// After animation completes, clear the inline width.
setTimeout(() => {
card.style.removeProperty('width');
}, 210);
}
}
}
// If dropped in neither area, return card to its original container.
if (!droppedInSidebar && !droppedInTop) {
// Check the header drop zone.
const headerDropArea = document.getElementById('headerDropArea');
if (!droppedInSidebar && !droppedInTop && headerDropArea) {
const rect = headerDropArea.getBoundingClientRect();
if (
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom
) {
insertCardInHeader(card, e);
droppedInHeader = true;
}
}
// If card was not dropped in any zone, return it to its original container.
if (!droppedInSidebar && !droppedInTop && !droppedInHeader) {
const orig = document.getElementById(card.dataset.originalContainerId);
if (orig) {
orig.appendChild(card);
@@ -330,7 +562,7 @@ export function initDragAndDrop() {
}
}
// Clear inline styles from dragging.
// Clear inline drag-related styles.
[
'position',
'left',
@@ -351,6 +583,9 @@ export function initDragAndDrop() {
updateTopZoneLayout();
updateSidebarVisibility();
// Hide header drop zone if no icon is present.
hideHeaderDropZone();
}
});
});

View File

@@ -17,9 +17,9 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
exit;
}
$userPermissions = loadUserPermissions($username);
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {

View File

@@ -263,7 +263,7 @@ function previewFile(fileUrl, fileName) {
embed.style.height = "80vh";
embed.style.border = "none";
container.appendChild(embed);
} else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) {
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
const video = document.createElement("video");
video.src = fileUrl;
video.controls = true;
@@ -365,36 +365,55 @@ export function loadFileList(folderParam) {
//
function fileDragStartHandler(event) {
const row = event.currentTarget;
let fileNames = [];
// Check if multiple file checkboxes are selected.
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
let fileNames = [];
if (selectedCheckboxes.length > 1) {
// Gather file names from all selected rows.
selectedCheckboxes.forEach(chk => {
const parentRow = chk.closest("tr");
if (parentRow) {
const cell = parentRow.querySelector("td:nth-child(2)");
if (cell) fileNames.push(cell.textContent.trim());
if (cell) {
let rawName = cell.textContent.trim();
// Attempt to get the tag text from a container that holds the tags.
const tagContainer = cell.querySelector(".tag-badges");
if (tagContainer) {
const tagText = tagContainer.innerText.trim();
if (rawName.endsWith(tagText)) {
rawName = rawName.slice(0, -tagText.length).trim();
}
}
fileNames.push(rawName);
}
}
});
} else {
// Only one file is selected (or none), so get file name from the current row.
const fileNameCell = row.querySelector("td:nth-child(2)");
if (fileNameCell) {
fileNames.push(fileNameCell.textContent.trim());
let rawName = fileNameCell.textContent.trim();
const tagContainer = fileNameCell.querySelector(".tag-badges");
if (tagContainer) {
const tagText = tagContainer.innerText.trim();
if (rawName.endsWith(tagText)) {
rawName = rawName.slice(0, -tagText.length).trim();
}
}
fileNames.push(rawName);
}
}
if (fileNames.length === 0) return;
const dragData = {
files: fileNames, // use an array of file names
sourceFolder: window.currentFolder || "root"
};
// For a single file, send fileName; for multiple, send an array.
const dragData = fileNames.length === 1
? { fileName: fileNames[0], sourceFolder: window.currentFolder || "root" }
: { files: fileNames, sourceFolder: window.currentFolder || "root" };
event.dataTransfer.setData("application/json", JSON.stringify(dragData));
// (Keep your custom drag image code here.)
let dragImage;
if (fileNames.length > 1) {
dragImage = document.createElement("div");
// Create a custom drag image.
let dragImage = document.createElement("div");
dragImage.style.display = "inline-flex";
dragImage.style.width = "auto";
dragImage.style.maxWidth = "fit-content";
@@ -409,31 +428,11 @@ function fileDragStartHandler(event) {
icon.className = "material-icons";
icon.textContent = "insert_drive_file";
icon.style.marginRight = "4px";
const countSpan = document.createElement("span");
countSpan.textContent = fileNames.length + " files";
const label = document.createElement("span");
label.textContent = fileNames.length === 1 ? fileNames[0] : fileNames.length + " files";
dragImage.appendChild(icon);
dragImage.appendChild(countSpan);
} else {
dragImage = document.createElement("div");
dragImage.style.display = "inline-flex";
dragImage.style.width = "auto";
dragImage.style.maxWidth = "fit-content";
dragImage.style.padding = "6px 10px";
dragImage.style.backgroundColor = "#333";
dragImage.style.color = "#fff";
dragImage.style.border = "1px solid #555";
dragImage.style.borderRadius = "4px";
dragImage.style.alignItems = "center";
dragImage.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.3)";
const icon = document.createElement("span");
icon.className = "material-icons";
icon.textContent = "insert_drive_file";
icon.style.marginRight = "4px";
const nameSpan = document.createElement("span");
nameSpan.textContent = fileNames[0];
dragImage.appendChild(icon);
dragImage.appendChild(nameSpan);
}
dragImage.appendChild(label);
document.body.appendChild(dragImage);
event.dataTransfer.setDragImage(dragImage, 5, 5);
setTimeout(() => {

View File

@@ -95,6 +95,9 @@
<h1>FileRise</h1>
</div>
<div class="header-right">
<div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;">
<!-- Your header drop zone -->
<div id="headerDropArea" class="header-drop-zone"></div>
<div class="header-buttons">
<button id="logoutBtn" title="Logout">
<i class="material-icons">exit_to_app</i>
@@ -132,10 +135,12 @@
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
</div>
</div>
</div>
</header>
<!-- Custom Toast Container -->
<div id="customToast"></div>
<div id="hiddenCardsContainer" style="display:none;"></div>
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
<div class="main-wrapper">
@@ -390,7 +395,6 @@
</div>
</div>
</div>
<script type="module" src="main.js"></script>
</body>

91
main.js
View File

@@ -17,12 +17,17 @@ import { loadFolderTree } from './folderManager.js';
import { initUpload } from './upload.js';
import { initAuth, checkAuthentication } from './auth.js';
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
import { initDragAndDrop, loadSidebarOrder } from './dragAndDrop.js'
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
function loadCsrfToken() {
fetch('token.php', { credentials: 'include' })
.then(response => response.json())
function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
return fetch('token.php', { credentials: 'include' })
.then(response => {
if (!response.ok) {
throw new Error("Token fetch failed with status: " + response.status);
}
return response.json();
})
.then(data => {
// Set global variables.
window.csrfToken = data.csrf_token;
@@ -45,11 +50,19 @@ function loadCsrfToken() {
document.head.appendChild(metaShare);
}
metaShare.setAttribute('content', data.share_url);
})
.catch(error => console.error("Error loading CSRF token and share URL:", error));
}
document.addEventListener("DOMContentLoaded", loadCsrfToken);
return data;
})
.catch(error => {
if (retries > 0) {
console.warn(`CSRF token load failed. Retrying in ${delay}ms... (${retries} retries left)`, error);
return new Promise(resolve => setTimeout(resolve, delay))
.then(() => loadCsrfTokenWithRetry(retries - 1, delay * 2));
}
console.error("Failed to load CSRF token after retries.", error);
throw error;
});
}
// Expose functions for inline handlers.
window.sendRequest = sendRequest;
@@ -63,9 +76,41 @@ window.renameFile = renameFile;
window.currentFolder = "root";
document.addEventListener("DOMContentLoaded", function () {
// Call initAuth synchronously.
// First, load the CSRF token (with retry).
loadCsrfTokenWithRetry().then(() => {
// Once CSRF token is loaded, initialize authentication.
initAuth();
// Continue with initializations that rely on a valid CSRF token:
checkAuthentication().then(authenticated => {
if (authenticated) {
window.currentFolder = "root";
initTagSearch();
loadFileList(window.currentFolder);
initDragAndDrop();
loadSidebarOrder();
loadHeaderOrder();
initFileActions();
initUpload();
loadFolderTree();
setupTrashRestoreDelete();
const helpBtn = document.getElementById("folderHelpBtn");
const helpTooltip = document.getElementById("folderHelpTooltip");
helpBtn.addEventListener("click", function () {
// Toggle display of the tooltip.
if (helpTooltip.style.display === "none" || helpTooltip.style.display === "") {
helpTooltip.style.display = "block";
} else {
helpTooltip.style.display = "none";
}
});
} else {
console.warn("User not authenticated. Data loading deferred.");
}
});
// Other DOM initialization that can happen after CSRF is ready.
const newPasswordInput = document.getElementById("newPassword");
if (newPasswordInput) {
newPasswordInput.addEventListener("input", function() {
@@ -74,6 +119,7 @@ document.addEventListener("DOMContentLoaded", function () {
} else {
console.error("newPassword input not found!");
}
// --- Dark Mode Persistence ---
const darkModeToggle = document.getElementById("darkModeToggle");
const storedDarkMode = localStorage.getItem("darkMode");
@@ -126,31 +172,8 @@ document.addEventListener("DOMContentLoaded", function () {
showToast(message);
sessionStorage.removeItem("welcomeMessage");
}
checkAuthentication().then(authenticated => {
if (authenticated) {
window.currentFolder = "root";
initTagSearch();
loadFileList(window.currentFolder);
initDragAndDrop();
loadSidebarOrder();
initFileActions();
initUpload();
loadFolderTree();
setupTrashRestoreDelete();
const helpBtn = document.getElementById("folderHelpBtn");
const helpTooltip = document.getElementById("folderHelpTooltip");
helpBtn.addEventListener("click", function () {
// Toggle display of the tooltip.
if (helpTooltip.style.display === "none" || helpTooltip.style.display === "") {
helpTooltip.style.display = "block";
} else {
helpTooltip.style.display = "none";
}
});
} else {
console.warn("User not authenticated. Data loading deferred.");
}
}).catch(error => {
console.error("Initialization halted due to CSRF token load failure.", error);
});
// --- Auto-scroll During Drag ---

View File

@@ -20,9 +20,8 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(401);
exit;
}
$userPermissions = loadUserPermissions($username);
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {

View File

@@ -21,9 +21,9 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(401);
exit;
}
$userPermissions = loadUserPermissions($username);
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {

View File

@@ -27,9 +27,8 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
http_response_code(403);
exit;
}
$userPermissions = loadUserPermissions($username);
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 410 KiB

BIN
resources/dark-header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 4.0 MiB

BIN
resources/dark-sidebar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 4.0 MiB

BIN
resources/light-topbar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

After

Width:  |  Height:  |  Size: 457 KiB

View File

@@ -18,9 +18,8 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(401);
exit;
}
$userPermissions = loadUserPermissions($username);
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {

View File

@@ -1068,7 +1068,7 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
}
#customToast.show {
opacity: 0.7;
opacity: 0.9;
}
.button-wrap {
@@ -2023,20 +2023,17 @@ body.dark-mode .card {
z-index: 6000 !important;
}
/* Default (light mode) for admin panel content */
.admin-panel-content {
background: #fff;
color: #000;
}
/* Dark mode overrides for admin panel content */
body.dark-mode .admin-panel-content {
background: #2c2c2c; /* dark background */
color: #e0e0e0; /* light text */
background: #2c2c2c;
color: #e0e0e0;
border: 1px solid #444;
}
/* Optionally, adjust input, label, etc. for dark mode */
body.dark-mode .admin-panel-content input,
body.dark-mode .admin-panel-content select,
body.dark-mode .admin-panel-content textarea {
@@ -2067,3 +2064,59 @@ body.dark-mode .admin-panel-content label {
.spinning {
animation: spin 1s linear infinite;
}
.rise-effect {
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.toggle-modal-btn,
.collapse-btn {
background: none;
border: none;
outline: none;
cursor: pointer;
padding: 8px;
font-size: 24px;
color: #616161;
border-radius: 50%;
transition: background 0.3s ease;
}
.toggle-modal-btn:hover,
.collapse-btn:hover {
background: rgba(0, 0, 0, 0.1);
}
.toggle-modal-btn:focus,
.collapse-btn:focus {
outline: none;
}
.header-drop-zone {
width: 66px;
height: 36px;
align-items: center;
justify-content: center;
gap: 5px;
display: inline-flex;
}
.header-drop-zone.drag-active {
border: 2px dashed #1565C0;
background-color: #eef;
background-color: transparent;
transition: width 0.3s ease;
box-sizing: border-box;
}
body.dark-mode .header-drop-zone.drag-active {
background-color: #333;
border: 2px dashed #555;
color: #fff;
}
.header-drop-zone.drag-active:empty::before {
content: "Drop";
font-size: 10px;
color: #aaa;
}

View File

@@ -18,8 +18,9 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(401);
exit;
}
$userPermissions = loadUserPermissions($username);
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['disableUpload']) && $userPermissions['disableUpload'] === true) {