Compare commits

...

46 Commits

Author SHA1 Message Date
Ryan
27de0a9a48 composer dependencies for OIDC 2025-03-29 04:47:21 -04:00
Ryan
051544dc5a New Admin Panel, OIDC Integration & Card dragDrop adjustments 2025-03-29 04:33:10 -04:00
Ryan
89777584cf Merge pull request #5 from BitOlli/patch-1
Readme fixes
2025-03-28 11:59:19 -04:00
BitOlli
ed47e3c3bc Update README.md
fixed PR, typo
2025-03-28 15:30:20 +01:00
BitOlli
edd9094218 Update README.md
corrected docker-compose.yml section on readme.md to have proper YAML indentation
2025-03-28 15:27:33 +01:00
Ryan
3b0083516b Additional audio formats to GalleryView 2025-03-27 13:51:15 -04:00
Ryan
fee3b544dd basic auth 2025-03-27 13:43:51 -04:00
Ryan
99ed05d3de basic auth login 2025-03-27 13:39:47 -04:00
Ryan
32469778dc audio files playback added 2025-03-27 13:14:14 -04:00
Ryan
ecb4ac2c75 update 2025-03-27 11:30:21 -04:00
Ryan
4ae509acd2 Contributing added 2025-03-27 11:28:45 -04:00
Ryan
b1cd4b7bdc contribute md 2025-03-27 11:23:52 -04:00
Ryan
d57687adee new video demo 2025-03-27 05:16:50 -04:00
Ryan
64d41af21b Show extracted names in toast and new images 2025-03-27 05:11:40 -04:00
Ryan
a8f5a6d3bc Double root empty folder fix, side bar drag zone adjusted 2025-03-27 03:22:43 -04:00
Ryan
062cfc0dd4 FileRise changes 2025-03-26 18:55:27 -04:00
Ryan
32d25b1b69 FileRise changes 2025-03-26 18:54:54 -04:00
Ryan
56626aaa40 change to FileRise round 1 2025-03-26 18:26:57 -04:00
Ryan
0697fcb1df New Name FileRise, logo rises, DragDrop Upload & Folder cards 2025-03-26 18:23:42 -04:00
Ryan
c08c903810 Adjust context menu to stay in viewport 2025-03-25 04:23:37 -04:00
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
Ryan
a9c7bb6493 Resumable.js Integration (pause, resume, retry & chunked uploads) 2025-03-23 02:13:10 -04:00
Ryan
6d588eb143 Folder Breadcrumb Rendering with Drag-and-Drop support 2025-03-22 08:17:34 -04:00
Ryan
2092512f43 Add 30day persistent token remember me for login 2025-03-22 07:12:20 -04:00
Ryan
833eaa3194 gallery view default icon fixed 2025-03-22 06:42:00 -04:00
Ryan
edb8ff476a added brute force protection 2025-03-22 05:44:56 -04:00
Ryan
2e55f5f4d7 change password to readme 2025-03-21 18:08:51 -04:00
Ryan
ae48119e15 adjusted readme 2025-03-21 17:46:12 -04:00
Ryan
f5410a92e7 adjusted readme 2025-03-21 17:42:45 -04:00
Ryan
7898ad4f1c folder dash adjusted 2025-03-21 17:38:44 -04:00
Ryan
d3ce26e83d additional readme info 2025-03-21 16:23:39 -04:00
Ryan
b4a903e738 MFE - Multi File Upload Editor 2025-03-21 16:19:00 -04:00
Ryan
53bb72f4ab change password added 2025-03-21 15:46:55 -04:00
Ryan
cef96f0047 Clean up expired share links 2025-03-21 14:17:25 -04:00
Ryan
559df3c396 Updated token generation to 16 bytes (32 hex characters) 2025-03-21 14:05:44 -04:00
Ryan
a24321455a block php script execution 2025-03-21 11:17:24 -04:00
Ryan
3c2faa5218 adjusted readme 2025-03-21 11:15:13 -04:00
39 changed files with 3662 additions and 897 deletions

92
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,92 @@
# Contributing to FileRise
Thank you for your interest in contributing to FileRise! We appreciate your help in making this self-hosted file manager even better.
## Table of Contents
- [Getting Started](#getting-started)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Pull Requests](#pull-requests)
- [Coding Guidelines](#coding-guidelines)
- [Documentation](#documentation)
- [Questions and Support](#questions-and-support)
## Getting Started
1. **Fork the Repository**
Click the **Fork** button on the top-right of the FileRise GitHub page to create your own copy.
2. **Clone Your Fork**
```bash
git clone https://github.com/yourusername/FileRise.git
cd FileRise
```
3. **Set Up a Local Environment**
FileRise runs on a standard LAMP stack. Ensure you have PHP, Apache, and the necessary dependencies installed. For frontend development, Node.js may be required for build tasks if applicable.
4. **Configuration**
Copy any example configuration files (if provided) and adjust them as needed for your local setup.
## Reporting Bugs
If you discover a bug, please open an issue on GitHub and include:
- A clear and descriptive title.
- Detailed steps to reproduce the bug.
- The expected and actual behavior.
- Screenshots or error logs (if applicable).
- Environment details (PHP version, Apache version, OS, etc.).
## Suggesting Enhancements
Have an idea for a new feature or improvement? Before opening a new issue, please check if a similar suggestion already exists. If not, open an issue with:
- A clear description of the enhancement.
- Use cases or examples of how it would be beneficial.
- Any potential drawbacks or alternatives.
## Pull Requests
We welcome pull requests! To submit one, please follow these guidelines:
1. **Create a New Branch**
Always create a feature branch from master.
```bash
git checkout -b feature/your-feature-name
```
2. **Make Your Changes**
Commit your changes with clear, descriptive messages. Make sure your code follows the projects style guidelines.
3. **Write Tests**
If applicable, add tests to cover your changes to help us maintain code quality.
4. **Submit the Pull Request**
Push your branch to your fork and open a pull request against the master branch in the main repository. Provide a detailed description of your changes and why theyre needed.
## Coding Guidelines
- **Code Style:**
Follow the conventions used in the project. Consistent indentation, naming conventions, and clear code organization are key.
- **Documentation:**
Update documentation if your changes affect the usage or configuration of FileRise.
- **Commit Messages:**
Write meaningful commit messages that clearly describe the purpose of your changes.
## Documentation
If you notice any areas in the documentation that need improvement or updating, please feel free to include those changes in your pull requests. Clear documentation is essential for helping others understand and use FileRise.
## Questions and Support
If you have any questions, ideas, or need support, please open an issue or join our discussion on [GitHub Discussions](https://github.com/error311/FileRise/discussions). Were here to help and appreciate your contributions.
---
Thank you for helping to improve FileRise and happy coding!

190
README.md
View File

@@ -1,19 +1,28 @@
# Multi File Upload Editor
# FileRise - Elevate your File Management
https://github.com/user-attachments/assets/179e6940-5798-4482-9a69-696f806c37de
**Video demo:**
changelogs available here: <https://github.com/error311/multi-file-upload-editor-docker/>
https://github.com/user-attachments/assets/9546a76b-afb0-4068-875a-0eab478b514d
Multi File Upload Editor is a lightweight, secure, self-hosted web application for uploading, editing, and managing files. Built with an Apache/PHP backend and a modern JavaScript (ES6 modules) frontend, it offers a responsive, dynamic file management interface. It serves as an alternative to solutions like FileGator or ProjectSend, providing an easy-to-setup experience ideal for document management, image galleries, firmware file hosting, and more.
**Dark mode:**
![Dark Mode](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-mode.png)
changelogs available here: <https://github.com/error311/FileRise-docker/>
FileRise is a lightweight, secure, self-hosted web application for uploading, syntax-highlight editing, drag & drop file management, and more. Built with an Apache/PHP backend and a modern JavaScript (ES6 modules) frontend, it offers a responsive and dynamic interface designed to simplify file handling. As an alternative to solutions like FileGator, TinyFileManager, or ProjectSend, FileRise provides an easy-to-set-up experience ideal for document management, image galleries, firmware hosting, and other file-intensive applications.
---
## Features
- **Multiple File/Folder Uploads with Progress:**
- Users can select and upload multiple files & folders at once.
- Each file upload displays an individual progress bar with percentage and upload speed.
- Image files show a small thumbnail preview (with default Material icons for other file types).
- **Multiple File/Folder Uploads with Progress (Resumable.js Integration):**
- Users can effortlessly upload multiple files and folders simultaneously by either selecting them through the file picker or dragging and dropping them directly into the interface.
- **Chunked Uploads:** Files are uploaded in configurable chunks (default set as 3 MB) to efficiently handle large files.
- **Pause, Resume, and Retry:** Uploads can be paused and resumed at any time, with support for retrying failed chunks.
- **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
@@ -22,50 +31,81 @@ Multi File Upload Editor is a lightweight, secure, self-hosted web application f
- 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.
- Users can quickly preview images, videos, audio 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.
- **Drag & Drop:** Easily move files by selecting them from the file list and simply dragging them onto your desired folder in the folder tree. When you drop the files onto a folder, the system automatically moves them, updating your file organization in one seamless action.
- **Extract Zip:** When one or more ZIP files are selected, users can extract the archive(s) directly into the current folder.
- **Drag & Drop (File Movement):** Easily move files by selecting them from the file list and 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.
- Basic Auth supported for login.
- **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.
@@ -87,32 +127,63 @@ Multi File Upload Editor is a lightweight, secure, self-hosted web application f
- The trash modal displays details such as file name, uploader/deleter, and the trashed date/time.
- Material icons with tooltips visually represent the restore and delete actions.
- **Drag & Drop Cards with Dedicated Drop Zones:**
- **Sidebar Drop Zone:**
- Cards (such as the upload card or folder management card) can be dragged into a dedicated sidebar drop zone for quick access to frequently used operations.
- The sidebar drop zone expands dynamically to accept drops anywhere within its visual area.
- **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.
- **Seamless Interaction:**
- Both drop zones support smooth drag and drop interactions with animations and pointer event adjustments to prevent interference, ensuring that cards can be dropped reliably regardless of screen position.
### 🔒 Admin Panel & OpenID Connect (OIDC) Integration
- **Flexible Authentication:**
- Supports multiple authentication methods including Form-based Login, Basic Auth, and OpenID Connect (OIDC). Allow disable of only two login options.
- **Secure OIDC Authentication:**
- Integrates seamlessly with OIDC providers (e.g., Keycloak, Okta).
- Admin-configurable OIDC settings, including Provider URL, Client ID, Client Secret, and Redirect URI.
- All sensitive configurations are securely stored in an encrypted JSON file.
- **Dynamic Admin Panel:**
- Intuitive Admin Panel with Material Icons for quick recognition and access.
- Allows administrators to easily manage authentication settings, user management, and login methods.
- Real-time validation prevents disabling all authentication methods simultaneously, ensuring continuous secure access.
---
## Screenshots
**Light mode**
![Light Mode](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/light-mode.png)
**Light mode:**
![Dark Admin Panel](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-admin-panel.png)
**Dark mode**
![Dark Mode](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-mode.png)
**Light mode:**
![Light Mode](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-mode.png)
**Dark editor**
![dark-editor](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-editor.png)
**Dark mode default:**
![Default Layout](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-mode-default.png)
**Dark preview**
![dark-preview](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-preview.png)
**Dark editor:**
![dark-editor](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-editor.png)
**Restore or Delete Trash**
![restore-delete](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/restore-delete.png)
**Light preview**
![dark-preview](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-preview.png)
**Login page**
![Login](https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/login-page.png)
**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)
**Gallery view:**
![Login](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-gallery.png)
**iphone screenshots:**
<p align="center">
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-iphone.png" width="45%">
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/light-preview-iphone.png" width="45%">
<img src="https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-iphone.png" width="45%">
<img src="https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-preview-iphone.png" width="45%">
</p>
---
@@ -125,7 +196,7 @@ Multi File Upload Editor is a lightweight, secure, self-hosted web application f
- **Clone:**
```bash
git clone https://github.com/error311/multi-file-upload-editor.git
git clone https://github.com/error311/FileRise.git
```
- **Download:**
@@ -133,9 +204,19 @@ Multi File Upload Editor is a lightweight, secure, self-hosted web application f
2. **Deploy to Your Web Server:**
- Place the project files in your Apache web directory (e.g., `/var/www/html`).
- Ensure PHP 8.1+ is installed along with the required extensions (php-json, php-curl, php-zip, etc.).
- Ensure PHP 8.1+ is installed along with the required extensions (`php-json`, `php-curl`, `php-zip`, etc.).
3. **Directory Setup & Permissions:**
3. **Install Composer Dependencies (Required for OIDC Support):**
- Install Composer if you haven't already ([Installation Guide](https://getcomposer.org/download/)).
- Navigate to the project directory and run:
```bash
composer install
```
- This step will install necessary dependencies like `jumbojett/openid-connect-php` and `phpseclib/phpseclib`.
4. **Directory Setup & Permissions:**
- Create the following directories if they do not exist, and set appropriate permissions:
- `uploads/` for file storage.
- `users/` to store `users.txt` (user authentication data).
@@ -147,11 +228,11 @@ Multi File Upload Editor is a lightweight, secure, self-hosted web application f
chmod -R 775 /var/www/uploads /var/www/users /var/www/metadata
```
4. **Configure Apache:**
5. **Configure Apache:**
- Ensure that directory indexing is disabled (using `Options -Indexes` in your `.htaccess` or Apache configuration).
- Make sure the Apache configuration allows URL rewriting if needed.
5. **Configuration File:**
6. **Configuration File:**
- Open `config.php` and adjust the following constants as necessary:
- `BASE_URL`: Set this to your web apps base URL.
- `UPLOAD_DIR`: Adjust the directory path for uploads.
@@ -168,14 +249,22 @@ Multi File Upload Editor is a lightweight, secure, self-hosted web application f
## Docker Usage
For users who prefer containerization, a Docker image is available
For users who prefer containerization, a Docker image is available.
**Note:** The Docker image already includes Composer dependencies pre-installed (including OIDC support).
### Quickstart
1. **Pull the Docker Image:**
```bash
docker pull error311/multi-file-upload-editor-docker:latest
docker pull error311/filerise-docker:latest
```
macos M series:
```bash
docker pull --platform linux/x86_64 error311/filerise-docker:latest
```
2. **Run the Container:**
@@ -189,8 +278,8 @@ For users who prefer containerization, a Docker image is available
-v /path/to/your/uploads:/var/www/uploads \
-v /path/to/your/users:/var/www/users \
-v /path/to/your/metadata:/var/www/metadata \
--name multi-file-upload-editor \
error311/multi-file-upload-editor-docker:latest
--name FileRise \
error311/filerise-docker:latest
```
3. **Using Docker Compose:**
@@ -200,18 +289,19 @@ For users who prefer containerization, a Docker image is available
```yaml
version: "3.8"
services:
web:
image: error311/multi-file-upload-editor-docker:latest
ports:
- "80:80"
environment:
TIMEZONE: "America/New_York"
TOTAL_UPLOAD_SIZE: "5G"
SECURE: "false"
volumes:
- /path/to/your/uploads:/var/www/uploads
- /path/to/your/users:/var/www/users
- /path/to/your/metadata:/var/www/metadata
web:
image: error311/filerise-docker:latest
ports:
- "80:80"
environment:
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
- /path/to/your/metadata:/var/www/metadata
```
**Then start the container with:**
@@ -239,7 +329,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.
---
@@ -254,4 +344,8 @@ The `config.php` file contains several key constants that may need adjustment fo
- **Logging & Troubleshooting:**
Check Apache logs (located in `/var/log/apache2/`) for troubleshooting any issues during deployment or operation.
Enjoy using the Multi File Upload Editor! For any issues or contributions, please refer to the [GitHub repository](https://github.com/error311/multi-file-upload-editor).
---
## Contributing
We welcome contributions! Please check out our [Contributing Guidelines](CONTRIBUTING.md) before getting started.

View File

@@ -6,9 +6,9 @@ $usersFile = USERS_DIR . USERS_FILE;
// Determine if we are in setup mode:
// - Query parameter setup=1 is passed
// - And users.txt is either missing or empty
$isSetup = (isset($_GET['setup']) && $_GET['setup'] == '1');
if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '')) {
// - And users.txt is either missing or empty (zero bytes or trimmed content is empty)
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
if ($isSetup && (!file_exists($usersFile) || filesize($usersFile) == 0 || trim(file_get_contents($usersFile)) === '')) {
// Allow initial admin creation without session checks.
$setupMode = true;
} else {
@@ -16,7 +16,7 @@ if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile))
// In non-setup mode, check CSRF token and require admin privileges.
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
@@ -30,7 +30,7 @@ if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile))
}
}
// Get input data from JSON
// Get input data from JSON.
$data = json_decode(file_get_contents("php://input"), true);
$newUsername = trim($data["username"] ?? "");
$newPassword = trim($data["password"] ?? "");
@@ -42,7 +42,7 @@ if ($setupMode) {
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; // "1" for admin, "0" for regular user.
}
// Validate input
// Validate input.
if (!$newUsername || !$newPassword) {
echo json_encode(["error" => "Username and password required"]);
exit;
@@ -54,12 +54,12 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) {
exit;
}
// Ensure users.txt exists
// Ensure users.txt exists.
if (!file_exists($usersFile)) {
file_put_contents($usersFile, '');
}
// Check if username already exists
// Check if username already exists.
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($existingUsers as $line) {
list($storedUser, $storedHash, $storedRole) = explode(':', trim($line));
@@ -69,10 +69,10 @@ foreach ($existingUsers as $line) {
}
}
// Hash the password
// Hash the password.
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
// Prepare new user line
// Prepare new user line.
$newUserLine = $newUsername . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL;
// In setup mode, overwrite users.txt; otherwise, append to it.

575
auth.js
View File

@@ -1,107 +1,192 @@
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>';
// Default OIDC configuration (can be overridden via API in production)
const currentOIDCConfig = {
providerUrl: "https://your-oidc-provider.com",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
redirectUri: "https://yourdomain.com/auth.php?oidc=callback"
};
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons) {
// Insert after the third child if available.
if (headerButtons.children.length >= 4) {
headerButtons.insertBefore(restoreBtn, headerButtons.children[4]);
} else {
headerButtons.appendChild(restoreBtn);
}
}
}
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";
// If not admin, hide the restore button.
const restoreBtn = document.getElementById("restoreFilesBtn");
if (restoreBtn) {
restoreBtn.style.display = "none";
function updateItemsPerPageSelect() {
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
}
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
const authForm = document.getElementById("authForm");
if (authForm) {
authForm.style.display = disableFormLogin ? "none" : "block";
}
const basicAuthLink = document.querySelector("a[href='login_basic.php']");
if (basicAuthLink) {
basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
}
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
if (oidcLoginBtn) {
oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block";
}
}
function updateLoginOptionsUIFromStorage() {
const disableFormLogin = localStorage.getItem("disableFormLogin") === "true";
const disableBasicAuth = localStorage.getItem("disableBasicAuth") === "true";
const disableOIDCLogin = localStorage.getItem("disableOIDCLogin") === "true";
updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
}
function loadAdminConfigFunc() {
return fetch("getConfig.php", { credentials: "include" })
.then(response => response.json())
.then(config => {
localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin);
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
updateLoginOptionsUIFromStorage();
})
.catch(() => {
localStorage.setItem("disableFormLogin", "false");
localStorage.setItem("disableBasicAuth", "false");
localStorage.setItem("disableOIDCLogin", "false");
updateLoginOptionsUIFromStorage();
});
}
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 (data.isAdmin) {
let restoreBtn = document.getElementById("restoreFilesBtn");
if (!restoreBtn) {
restoreBtn = document.createElement("button");
restoreBtn.id = "restoreFilesBtn";
restoreBtn.classList.add("btn", "btn-warning");
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 >= 3) {
headerButtons.insertBefore(restoreBtn, headerButtons.children[3]);
} else {
headerButtons.appendChild(restoreBtn);
}
}
// Set items-per-page.
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
restoreBtn.style.display = "block";
let adminPanelBtn = document.getElementById("adminPanelBtn");
if (!adminPanelBtn) {
adminPanelBtn = document.createElement("button");
adminPanelBtn.id = "adminPanelBtn";
adminPanelBtn.classList.add("btn", "btn-info");
// Use material icon for the admin panel button.
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons) {
// Insert the adminPanelBtn immediately after the restoreBtn.
if (restoreBtn.nextSibling) {
headerButtons.insertBefore(adminPanelBtn, restoreBtn.nextSibling);
} else {
headerButtons.appendChild(adminPanelBtn);
}
}
adminPanelBtn.addEventListener("click", openAdminPanel);
} else {
// Do not show a toast message repeatedly during initial check.
toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false);
toggleVisibility("fileListContainer", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
adminPanelBtn.style.display = "block";
}
}).catch(error => {
console.error("Error checking authentication:", error);
});
} else {
const restoreBtn = document.getElementById("restoreFilesBtn");
if (restoreBtn) restoreBtn.style.display = "none";
const adminPanelBtn = document.getElementById("adminPanelBtn");
if (adminPanelBtn) adminPanelBtn.style.display = "none";
}
updateItemsPerPageSelect();
updateLoginOptionsUIFromStorage();
}
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 {
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(() => false);
}
function initAuth() {
checkAuthentication(false);
loadAdminConfigFunc();
// Attach login event listener once.
const authForm = document.getElementById("authForm");
if (authForm) {
authForm.addEventListener("submit", function (event) {
event.preventDefault();
const rememberMe = document.getElementById("rememberMeCheckbox")
? document.getElementById("rememberMeCheckbox").checked
: false;
const formData = {
username: document.getElementById("loginUsername").value.trim(),
password: document.getElementById("loginPassword").value.trim()
password: document.getElementById("loginPassword").value.trim(),
remember_me: rememberMe
};
sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken })
.then(data => {
if (data.success) {
console.log("✅ Login successful. Reloading page.");
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
window.location.reload();
} else {
showToast("Login failed: " + (data.error || "Unknown error"));
if (data.error && data.error.includes("Too many failed login attempts")) {
showToast(data.error);
const loginButton = authForm.querySelector("button[type='submit']");
if (loginButton) {
loginButton.disabled = true;
setTimeout(() => {
loginButton.disabled = false;
showToast("You can now try logging in again.");
}, 30 * 60 * 1000);
}
} else {
showToast("Login failed: " + (data.error || "Unknown error"));
}
}
})
.catch(error => console.error("❌ Error logging in:", error));
.catch(() => {});
});
}
// Attach logout event listener.
document.getElementById("logoutBtn").addEventListener("click", function () {
fetch("logout.php", {
method: "POST",
@@ -109,17 +194,24 @@ function initAuth() {
headers: { "X-CSRF-Token": window.csrfToken }
})
.then(() => window.location.reload(true))
.catch(error => console.error("Logout error:", error));
.catch(() => {});
});
// Add User functionality.
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
if (oidcLoginBtn) {
oidcLoginBtn.addEventListener("click", function () {
window.location.href = "auth.php?oidc";
});
}
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();
const newPassword = document.getElementById("newPassword").value.trim();
const newPassword = document.getElementById("addUserPassword").value.trim();
const isAdmin = document.getElementById("isAdmin").checked;
if (!newUsername || !newPassword) {
showToast("Username and password are required!");
@@ -143,32 +235,28 @@ function initAuth() {
if (data.success) {
showToast("User added successfully!");
closeAddUserModal();
checkAuthentication(false); // Re-check without showing toast
checkAuthentication(false);
} else {
showToast("Error: " + (data.error || "Could not add user"));
}
})
.catch(error => console.error("Error adding user:", error));
});
document.getElementById("cancelUserBtn").addEventListener("click", function () {
closeAddUserModal();
.catch(() => {});
});
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
// Remove User functionality.
document.getElementById("removeUserBtn").addEventListener("click", function () {
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 + "?")) {
return;
}
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
if (!confirmed) return;
fetch("removeUser.php", {
method: "POST",
credentials: "include",
@@ -188,45 +276,284 @@ function initAuth() {
showToast("Error: " + (data.error || "Could not remove user"));
}
})
.catch(error => console.error("Error removing user:", error));
.catch(() => {});
});
document.getElementById("cancelRemoveUserBtn").addEventListener("click", function () {
closeRemoveUserModal();
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
document.getElementById("changePasswordBtn").addEventListener("click", function () {
document.getElementById("changePasswordModal").style.display = "block";
document.getElementById("oldPassword").focus();
});
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
document.getElementById("changePasswordModal").style.display = "none";
});
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
const oldPassword = document.getElementById("oldPassword").value.trim();
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;
}
const data = { oldPassword, newPassword, confirmPassword };
fetch("changePassword.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
showToast(result.success);
document.getElementById("oldPassword").value = "";
document.getElementById("newPassword").value = "";
document.getElementById("confirmPassword").value = "";
document.getElementById("changePasswordModal").style.display = "none";
} else {
showToast("Error: " + (result.error || "Could not change password."));
}
})
.catch(() => {
showToast("Error changing password.");
});
});
}
function checkAuthentication(showLoginToast = true) {
// Optionally pass a flag so we don't show a toast every time.
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;
function loadOIDCConfig() {
return fetch("getConfig.php", { credentials: "include" })
.then(response => response.json())
.then(config => {
if (config.oidc) {
Object.assign(currentOIDCConfig, config.oidc);
}
window.setupMode = false;
if (data.authenticated) {
return data;
return currentOIDCConfig;
})
.catch(() => currentOIDCConfig);
}
function openAdminPanel() {
fetch("getConfig.php", { credentials: "include" })
.then(response => response.json())
.then(config => {
if (config.oidc) {
Object.assign(currentOIDCConfig, config.oidc);
}
const isDarkMode = document.body.classList.contains("dark-mode");
const overlayBackground = isDarkMode ? "rgba(0, 0, 0, 0.7)" : "rgba(0, 0, 0, 0.3)";
const modalContentStyles = `
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
color: ${isDarkMode ? "#e0e0e0" : "#000"};
padding: 20px;
max-width: 600px;
width: 90%;
border-radius: 8px;
position: relative;
overflow-y: auto;
max-height: 90vh;
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
`;
let adminModal = document.getElementById("adminPanelModal");
if (!adminModal) {
adminModal = document.createElement("div");
adminModal.id = "adminPanelModal";
adminModal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: ${overlayBackground};
display: flex;
justify-content: center;
align-items: center;
z-index: 3000;
`;
adminModal.innerHTML = `
<div class="modal-content" style="${modalContentStyles}">
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>Admin Panel</h3>
<form id="adminPanelForm">
<fieldset style="margin-bottom: 15px;">
<legend>OIDC Configuration</legend>
<div class="form-group">
<label for="oidcProviderUrl">OIDC Provider URL:</label>
<input type="text" id="oidcProviderUrl" class="form-control" value="${currentOIDCConfig.providerUrl}" />
</div>
<div class="form-group">
<label for="oidcClientId">OIDC Client ID:</label>
<input type="text" id="oidcClientId" class="form-control" value="${currentOIDCConfig.clientId}" />
</div>
<div class="form-group">
<label for="oidcClientSecret">OIDC Client Secret:</label>
<input type="text" id="oidcClientSecret" class="form-control" value="${currentOIDCConfig.clientSecret}" />
</div>
<div class="form-group">
<label for="oidcRedirectUri">OIDC Redirect URI:</label>
<input type="text" id="oidcRedirectUri" class="form-control" value="${currentOIDCConfig.redirectUri}" />
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>Login Options</legend>
<div class="form-group">
<input type="checkbox" id="disableFormLogin" />
<label for="disableFormLogin">Disable Login Form</label>
</div>
<div class="form-group">
<input type="checkbox" id="disableBasicAuth" />
<label for="disableBasicAuth">Disable Basic HTTP Auth</label>
</div>
<div class="form-group">
<input type="checkbox" id="disableOIDCLogin" />
<label for="disableOIDCLogin">Disable OIDC Login</label>
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>User Management</legend>
<div style="display: flex; gap: 10px;">
<button type="button" id="adminOpenAddUser" class="btn btn-success">Add User</button>
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">Remove User</button>
</div>
</fieldset>
<div style="display: flex; justify-content: space-between;">
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">Cancel</button>
<button type="button" id="saveAdminSettings" class="btn btn-primary">Save Settings</button>
</div>
</form>
</div>
`;
document.body.appendChild(adminModal);
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
adminModal.addEventListener("click", function (e) {
if (e.target === adminModal) {
closeAdminPanel();
}
});
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
document.getElementById("adminOpenAddUser").addEventListener("click", function () {
toggleVisibility("addUserModal", true);
document.getElementById("newUsername").focus();
});
document.getElementById("adminOpenRemoveUser").addEventListener("click", function () {
loadUserList();
toggleVisibility("removeUserModal", true);
});
document.getElementById("saveAdminSettings").addEventListener("click", function () {
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
.filter(cb => cb.checked).length;
if (totalDisabled === 3) {
showToast("At least one login method must remain enabled.");
disableOIDCLoginCheckbox.checked = false;
localStorage.setItem("disableOIDCLogin", "false");
updateLoginOptionsUI({
disableFormLogin: disableFormLoginCheckbox.checked,
disableBasicAuth: disableBasicAuthCheckbox.checked,
disableOIDCLogin: disableOIDCLoginCheckbox.checked
});
return;
}
const newOIDCConfig = {
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
clientId: document.getElementById("oidcClientId").value.trim(),
clientSecret: document.getElementById("oidcClientSecret").value.trim(),
redirectUri: document.getElementById("oidcRedirectUri").value.trim()
};
const disableFormLogin = disableFormLoginCheckbox.checked;
const disableBasicAuth = disableBasicAuthCheckbox.checked;
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
sendRequest("updateConfig.php", "POST", {
oidc: newOIDCConfig,
disableFormLogin,
disableBasicAuth,
disableOIDCLogin
}, { "X-CSRF-Token": window.csrfToken })
.then(response => {
if (response.success) {
showToast("Settings updated successfully.");
localStorage.setItem("disableFormLogin", disableFormLogin);
localStorage.setItem("disableBasicAuth", disableBasicAuth);
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
closeAdminPanel();
} else {
showToast("Error updating settings: " + (response.error || "Unknown error"));
}
})
.catch(() => {});
});
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
function enforceLoginOptionConstraint(changedCheckbox) {
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
.filter(cb => cb.checked).length;
if (changedCheckbox.checked && totalDisabled === 3) {
showToast("At least one login method must remain enabled.");
changedCheckbox.checked = false;
}
}
disableFormLoginCheckbox.addEventListener("change", function () {
enforceLoginOptionConstraint(this);
});
disableBasicAuthCheckbox.addEventListener("change", function () {
enforceLoginOptionConstraint(this);
});
disableOIDCLoginCheckbox.addEventListener("change", function () {
enforceLoginOptionConstraint(this);
});
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
} 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;
const isDarkMode = document.body.classList.contains("dark-mode");
const overlayBackground = isDarkMode ? "rgba(0, 0, 0, 0.7)" : "rgba(0, 0, 0, 0.3)";
adminModal.style.backgroundColor = overlayBackground;
const modalContent = adminModal.querySelector(".modal-content");
if (modalContent) {
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
}
document.getElementById("oidcProviderUrl").value = currentOIDCConfig.providerUrl;
document.getElementById("oidcClientId").value = currentOIDCConfig.clientId;
document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret;
document.getElementById("oidcRedirectUri").value = currentOIDCConfig.redirectUri;
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
adminModal.style.display = "flex";
}
})
.catch(error => {
console.error("Error checking authentication:", error);
return false;
.catch(() => {
let adminModal = document.getElementById("adminPanelModal");
if (adminModal) {
document.getElementById("oidcProviderUrl").value = currentOIDCConfig.providerUrl;
document.getElementById("oidcClientId").value = currentOIDCConfig.clientId;
document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret;
document.getElementById("oidcRedirectUri").value = currentOIDCConfig.redirectUri;
adminModal.style.display = "flex";
} else {
openAdminPanel();
}
});
}
window.checkAuthentication = checkAuthentication;
function closeAdminPanel() {
const adminModal = document.getElementById("adminPanelModal");
if (adminModal) {
adminModal.style.display = "none";
}
}
window.changeItemsPerPage = function (value) {
localStorage.setItem("itemsPerPage", value);
@@ -237,16 +564,16 @@ 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();
const disableFormLogin = localStorage.getItem("disableFormLogin") === "true";
const disableBasicAuth = localStorage.getItem("disableBasicAuth") === "true";
const disableOIDCLogin = localStorage.getItem("disableOIDCLogin") === "true";
updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
});
function resetUserForm() {
document.getElementById("newUsername").value = "";
document.getElementById("newPassword").value = "";
document.getElementById("addUserPassword").value = "";
}
function closeAddUserModal() {
@@ -277,7 +604,7 @@ function loadUserList() {
closeRemoveUserModal();
}
})
.catch(error => console.error("Error loading user list:", error));
.catch(() => {});
}
export { initAuth, checkAuthentication };

158
auth.php
View File

@@ -1,55 +1,183 @@
<?php
require_once 'vendor/autoload.php';
require 'config.php';
header('Content-Type: application/json');
// --- OIDC Authentication Flow ---
if (isset($_GET['oidc'])) {
// Read and decrypt OIDC configuration from JSON file.
$adminConfigFile = USERS_DIR . 'adminConfig.json';
if (file_exists($adminConfigFile)) {
$encryptedContent = file_get_contents($adminConfigFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
if ($decryptedContent === false) {
echo json_encode(['error' => 'Failed to decrypt admin configuration.']);
exit;
}
$adminConfig = json_decode($decryptedContent, true);
if (isset($adminConfig['oidc'])) {
$oidcConfig = $adminConfig['oidc'];
$oidc_provider_url = !empty($oidcConfig['providerUrl']) ? $oidcConfig['providerUrl'] : 'https://your-oidc-provider.com';
$oidc_client_id = !empty($oidcConfig['clientId']) ? $oidcConfig['clientId'] : 'YOUR_CLIENT_ID';
$oidc_client_secret = !empty($oidcConfig['clientSecret']) ? $oidcConfig['clientSecret'] : 'YOUR_CLIENT_SECRET';
$oidc_redirect_uri = !empty($oidcConfig['redirectUri']) ? $oidcConfig['redirectUri'] : 'https://yourdomain.com/auth.php?oidc=callback';
} else {
$oidc_provider_url = 'https://your-oidc-provider.com';
$oidc_client_id = 'YOUR_CLIENT_ID';
$oidc_client_secret = 'YOUR_CLIENT_SECRET';
$oidc_redirect_uri = 'https://yourdomain.com/auth.php?oidc=callback';
}
} else {
$oidc_provider_url = 'https://your-oidc-provider.com';
$oidc_client_id = 'YOUR_CLIENT_ID';
$oidc_client_secret = 'YOUR_CLIENT_SECRET';
$oidc_redirect_uri = 'https://yourdomain.com/auth.php?oidc=callback';
}
$oidc = new Jumbojett\OpenIDConnectClient(
$oidc_provider_url,
$oidc_client_id,
$oidc_client_secret
);
$oidc->setRedirectURL($oidc_redirect_uri);
// Since PKCE is disabled in Keycloak, we do not set any PKCE parameters.
if ($_GET['oidc'] === 'callback') {
try {
$oidc->authenticate();
$username = $oidc->requestUserInfo('preferred_username');
session_regenerate_id(true);
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $username;
$_SESSION["isAdmin"] = false;
header("Location: index.html");
exit();
} catch (Exception $e) {
echo json_encode(["error" => "Authentication failed: " . $e->getMessage()]);
exit();
}
} else {
try {
$oidc->authenticate();
exit();
} catch (Exception $e) {
echo json_encode(["error" => "Authentication initiation failed: " . $e->getMessage()]);
exit();
}
}
}
// --- Fallback: Form-based Authentication ---
$usersFile = USERS_DIR . USERS_FILE;
$maxAttempts = 5;
$lockoutTime = 30 * 60;
$attemptsFile = USERS_DIR . 'failed_logins.json';
$failedLogFile = USERS_DIR . 'failed_login.log';
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
// Function to authenticate user
function authenticate($username, $password)
{
function loadFailedAttempts($file) {
if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true);
if (is_array($data)) {
return $data;
}
}
return [];
}
function saveFailedAttempts($file, $data) {
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
}
$ip = $_SERVER['REMOTE_ADDR'];
$currentTime = time();
$failedAttempts = loadFailedAttempts($attemptsFile);
if (isset($failedAttempts[$ip])) {
$attemptData = $failedAttempts[$ip];
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]);
exit();
}
}
function authenticate($username, $password) {
global $usersFile;
if (!file_exists($usersFile)) {
return false;
}
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
if ($username === $storedUser && password_verify($password, $storedPass)) {
return $storedRole; // Return the user's role
return $storedRole;
}
}
return false;
}
// Get JSON input
$data = json_decode(file_get_contents("php://input"), true);
$username = trim($data["username"] ?? "");
$password = trim($data["password"] ?? "");
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
// Validate input: ensure both fields are provided.
if (!$username || !$password) {
echo json_encode(["error" => "Username and password are required"]);
exit;
exit();
}
// Validate username format: allow only letters, numbers, underscores, dashes, and spaces.
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
exit;
exit();
}
// Authenticate user
$userRole = authenticate($username, $password);
if ($userRole !== false) {
// Regenerate session ID to mitigate session fixation attacks
if (isset($failedAttempts[$ip])) {
unset($failedAttempts[$ip]);
saveFailedAttempts($attemptsFile, $failedAttempts);
}
session_regenerate_id(true);
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $username;
$_SESSION["isAdmin"] = ($userRole === "1"); // "1" indicates admin
$_SESSION["isAdmin"] = ($userRole === "1");
if ($rememberMe) {
$token = bin2hex(random_bytes(32));
$expiry = time() + (30 * 24 * 60 * 60);
$persistentTokens = [];
if (file_exists($persistentTokensFile)) {
$encryptedContent = file_get_contents($persistentTokensFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
$persistentTokens = json_decode($decryptedContent, true);
if (!is_array($persistentTokens)) {
$persistentTokens = [];
}
}
$persistentTokens[$token] = [
"username" => $username,
"expiry" => $expiry,
"isAdmin" => ($userRole === "1")
];
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
}
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
} else {
if (isset($failedAttempts[$ip])) {
$failedAttempts[$ip]['count']++;
$failedAttempts[$ip]['last_attempt'] = $currentTime;
} else {
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
}
saveFailedAttempts($attemptsFile, $failedAttempts);
$logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL;
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
echo json_encode(["error" => "Invalid credentials"]);
}
?>

85
changePassword.php Normal file
View File

@@ -0,0 +1,85 @@
<?php
// changePassword.php
require 'config.php';
header('Content-Type: application/json');
// Make sure the user is logged in.
session_start();
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
exit;
}
$username = $_SESSION['username'] ?? '';
if (!$username) {
echo json_encode(["error" => "No username in session"]);
exit;
}
// CSRF token check.
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
}
// Get POST data.
$data = json_decode(file_get_contents("php://input"), true);
$oldPassword = trim($data["oldPassword"] ?? "");
$newPassword = trim($data["newPassword"] ?? "");
$confirmPassword = trim($data["confirmPassword"] ?? "");
// Validate input.
if (!$oldPassword || !$newPassword || !$confirmPassword) {
echo json_encode(["error" => "All fields are required."]);
exit;
}
if ($newPassword !== $confirmPassword) {
echo json_encode(["error" => "New passwords do not match."]);
exit;
}
// Path to users file.
$usersFile = USERS_DIR . USERS_FILE;
if (!file_exists($usersFile)) {
echo json_encode(["error" => "Users file not found"]);
exit;
}
// Read current users.
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$userFound = false;
$newLines = [];
foreach ($lines as $line) {
list($storedUser, $storedHash, $storedRole) = explode(':', trim($line));
if ($storedUser === $username) {
$userFound = true;
// Verify the old password.
if (!password_verify($oldPassword, $storedHash)) {
echo json_encode(["error" => "Old password is incorrect."]);
exit;
}
// Hash the new password.
$newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
// Rebuild the line with the new hash.
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole;
} else {
$newLines[] = $line;
}
}
if (!$userFound) {
echo json_encode(["error" => "User not found."]);
exit;
}
// Save updated users file.
if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL)) {
echo json_encode(["success" => "Password updated successfully."]);
} else {
echo json_encode(["error" => "Could not update password."]);
}
?>

View File

@@ -1,10 +1,59 @@
<?php
// config.php
// Allow an environment variable to override HTTPS detection.
// Define constants.
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');
date_default_timezone_set(TIMEZONE);
/**
* Encrypts data using AES-256-CBC.
*
* @param string $data The plaintext.
* @param string $encryptionKey The 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 Base64-encoded data containing IV and ciphertext.
* @param string $encryptionKey The 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 environment (override in production).
$encryptionKey = getenv('PERSISTENT_TOKENS_KEY') ?: 'default_please_change_this_key';
if (!$encryptionKey) {
die('Encryption key for persistent tokens is not set.');
}
// Determine whether HTTPS is used.
$envSecure = getenv('SECURE');
if ($envSecure !== false) {
// Convert the environment variable value to a boolean.
$secure = filter_var($envSecure, FILTER_VALIDATE_BOOLEAN);
} else {
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
@@ -13,13 +62,12 @@ if ($envSecure !== false) {
$cookieParams = [
'lifetime' => 7200,
'path' => '/',
'domain' => '', // Specify your domain if needed
'domain' => '', // Set your domain as needed.
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax'
];
session_set_cookie_params($cookieParams);
ini_set('session.gc_maxlifetime', 7200);
session_start();
@@ -27,11 +75,34 @@ if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Define BASE_URL (this should point to where index.html is, e.g. your uploads directory)
// Auto-login via persistent token.
if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token'])) {
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
$persistentTokens = [];
if (file_exists($persistentTokensFile)) {
$encryptedContent = file_get_contents($persistentTokensFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
$persistentTokens = json_decode($decryptedContent, true);
if (!is_array($persistentTokens)) {
$persistentTokens = [];
}
}
if (isset($persistentTokens[$_COOKIE['remember_me_token']])) {
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
if ($tokenData['expiry'] >= time()) {
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $tokenData["username"];
} else {
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);
}
}
}
define('BASE_URL', 'http://yourwebsite/uploads/');
// If BASE_URL is still the default placeholder, use the server's HTTP_HOST.
// Otherwise, use BASE_URL and append share.php.
if (strpos(BASE_URL, 'yourwebsite') !== false) {
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
? "http://" . $_SERVER['HTTP_HOST'] . "/share.php"
@@ -39,17 +110,5 @@ if (strpos(BASE_URL, 'yourwebsite') !== false) {
} else {
$defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php";
}
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

@@ -20,10 +20,8 @@ if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
exit;
}
// Optionally, you could check if the file exists in the uploads directory here.
// Generate a secure token.
$token = bin2hex(random_bytes(4)); // 8 hex characters.
$token = bin2hex(random_bytes(16)); // 32 hex characters.
// Calculate expiration (Unix timestamp).
$expires = time() + ($expirationMinutes * 60);
@@ -42,6 +40,14 @@ if (file_exists($shareFile)) {
}
}
// Clean up expired share links.
$currentTime = time();
foreach ($shareLinks as $key => $link) {
if ($link["expires"] < $currentTime) {
unset($shareLinks[$key]);
}
}
// Add record.
$shareLinks[$token] = [
"folder" => $folder,

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;
}
}
}
@@ -132,18 +136,20 @@ 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|ogg)$/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)$/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|ogg)$/i.test(file.name)) {
} else if (/\.(mp4|webm|mov)$/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>`;
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
previewIcon = `<i class="material-icons">audiotrack</i>`;
}
previewButton = `<button class="btn btn-sm btn-info preview-btn" onclick="event.stopPropagation(); previewFile('${folderPath + encodeURIComponent(file.name)}', '${safeFileName}')">
${previewIcon}
</button>`;
${previewIcon}
</button>`;
}
return `
@@ -227,82 +233,51 @@ export function toggleRowSelection(event, fileName) {
updateFileActionButtons();
}
export function previewFile(fileUrl, fileName) {
let modal = document.getElementById("filePreviewModal");
if (!modal) {
modal = document.createElement("div");
modal.id = "filePreviewModal";
Object.assign(modal.style, {
display: "none",
position: "fixed",
top: "0",
left: "0",
width: "100vw",
height: "100vh",
backgroundColor: "rgba(0,0,0,0.7)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: "1000"
});
modal.innerHTML = `
<div class="modal-content image-preview-modal-content">
<span id="closeFileModal" class="close-image-modal">&times;</span>
<h4 class="image-modal-header"></h4>
<div class="file-preview-container"></div>
</div>`;
document.body.appendChild(modal);
document.getElementById("closeFileModal").addEventListener("click", function () {
const video = modal.querySelector("video");
if (video) {
video.pause();
video.currentTime = 0;
}
modal.style.display = "none";
});
modal.addEventListener("click", function (e) {
if (e.target === modal) {
const video = modal.querySelector("video");
if (video) {
video.pause();
video.currentTime = 0;
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();
}
modal.style.display = "none";
}
});
}
}
modal.querySelector("h4").textContent = fileName;
const container = modal.querySelector(".file-preview-container");
container.innerHTML = "";
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");
const extension = fileName.split('.').pop().toLowerCase();
messageElem.textContent = message;
modal.style.display = "block";
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(fileName)) {
const img = document.createElement("img");
img.src = fileUrl;
img.className = "image-modal-img";
container.appendChild(img);
} else if (extension === "pdf") {
const embed = document.createElement("embed");
const separator = fileUrl.indexOf('?') === -1 ? '?' : '&';
embed.src = fileUrl + separator + 't=' + new Date().getTime();
embed.type = "application/pdf";
embed.style.width = "80vw";
embed.style.height = "80vh";
embed.style.border = "none";
container.appendChild(embed);
} else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) {
const video = document.createElement("video");
video.src = fileUrl;
video.controls = true;
video.className = "image-modal-img";
container.appendChild(video);
} else {
container.textContent = "Preview not available for this file type.";
}
// Cleanup function to hide the modal and remove event listeners.
function cleanup() {
modal.style.display = "none";
yesBtn.removeEventListener("click", onYes);
noBtn.removeEventListener("click", onNo);
}
modal.style.display = "flex";
function onYes() {
cleanup();
resolve(true);
}
function onNo() {
cleanup();
resolve(false);
}
yesBtn.addEventListener("click", onYes);
noBtn.addEventListener("click", onNo);
});
}

364
dragAndDrop.js Normal file
View File

@@ -0,0 +1,364 @@
// dragAndDrop.js
// Moves cards into the sidebar based on the saved order in localStorage.
export function loadSidebarOrder() {
const sidebar = document.getElementById('sidebarDropArea');
if (!sidebar) return;
const orderStr = localStorage.getItem('sidebarOrder');
if (orderStr) {
const order = JSON.parse(orderStr);
if (order.length > 0) {
// Ensure main wrapper is visible.
const mainWrapper = document.querySelector('.main-wrapper');
if (mainWrapper) {
mainWrapper.style.display = 'flex';
}
// For each saved ID, move the card into the sidebar.
order.forEach(id => {
const card = document.getElementById(id);
if (card && card.parentNode.id !== 'sidebarDropArea') {
sidebar.appendChild(card);
// Animate vertical slide for sidebar card
animateVerticalSlide(card);
}
});
}
}
updateSidebarVisibility();
}
// Internal helper: update sidebar visibility based on its content.
function updateSidebarVisibility() {
const sidebar = document.getElementById('sidebarDropArea');
if (sidebar) {
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
if (cards.length > 0) {
sidebar.classList.add('active');
sidebar.style.display = 'block';
} else {
sidebar.classList.remove('active');
sidebar.style.display = 'none';
}
// Save the current order in localStorage.
saveSidebarOrder();
}
}
// Internal helper: update top zone layout (center a card if one column is empty).
function updateTopZoneLayout() {
const leftCol = document.getElementById('leftCol');
const rightCol = document.getElementById('rightCol');
const leftIsEmpty = !leftCol.querySelector('#uploadCard');
const rightIsEmpty = !rightCol.querySelector('#folderManagementCard');
if (leftIsEmpty && !rightIsEmpty) {
leftCol.style.display = 'none';
rightCol.style.margin = '0 auto';
} else if (rightIsEmpty && !leftIsEmpty) {
rightCol.style.display = 'none';
leftCol.style.margin = '0 auto';
} else {
leftCol.style.display = '';
rightCol.style.display = '';
leftCol.style.margin = '';
rightCol.style.margin = '';
}
}
// When a card is being dragged, if the top drop zone is empty, set its min-height.
function addTopZoneHighlight() {
const topZone = document.getElementById('uploadFolderRow');
if (topZone) {
topZone.classList.add('highlight');
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
topZone.style.minHeight = '375px';
}
}
}
// When the drag ends, remove the extra min-height.
function removeTopZoneHighlight() {
const topZone = document.getElementById('uploadFolderRow');
if (topZone) {
topZone.classList.remove('highlight');
topZone.style.minHeight = '';
}
}
// Vertical slide/fade animation helper.
function animateVerticalSlide(card) {
card.style.transform = 'translateY(30px)';
card.style.opacity = '0';
// Force reflow.
card.offsetWidth;
requestAnimationFrame(() => {
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
card.style.transform = 'translateY(0)';
card.style.opacity = '1';
});
setTimeout(() => {
card.style.transition = '';
card.style.transform = '';
card.style.opacity = '';
}, 310);
}
// Internal helper: insert card into sidebar at a proper position based on event.clientY.
function insertCardInSidebar(card, event) {
const sidebar = document.getElementById('sidebarDropArea');
if (!sidebar) return;
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
let inserted = false;
for (const currentCard of existingCards) {
const rect = currentCard.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
if (event.clientY < midY) {
sidebar.insertBefore(card, currentCard);
inserted = true;
break;
}
}
if (!inserted) {
sidebar.appendChild(card);
}
// Ensure card fills the sidebar.
card.style.width = '100%';
animateVerticalSlide(card);
}
// Internal helper: save the current sidebar card order to localStorage.
function saveSidebarOrder() {
const sidebar = document.getElementById('sidebarDropArea');
if (sidebar) {
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
const order = Array.from(cards).map(card => card.id);
localStorage.setItem('sidebarOrder', JSON.stringify(order));
}
}
// Helper: move cards from sidebar back to the top drop area when on small screens.
function moveSidebarCardsToTop() {
if (window.innerWidth < 1205) {
const sidebar = document.getElementById('sidebarDropArea');
if (!sidebar) return;
const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
cards.forEach(card => {
const orig = document.getElementById(card.dataset.originalContainerId);
if (orig) {
orig.appendChild(card);
animateVerticalSlide(card);
}
});
updateSidebarVisibility();
updateTopZoneLayout();
}
}
// Listen for window resize to automatically move sidebar cards back to top on small screens.
window.addEventListener('resize', function () {
if (window.innerWidth < 1205) {
moveSidebarCardsToTop();
}
});
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
function ensureTopZonePlaceholder() {
const topZone = document.getElementById('uploadFolderRow');
if (!topZone) return;
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
let placeholder = topZone.querySelector('.placeholder');
if (!placeholder) {
placeholder = document.createElement('div');
placeholder.className = 'placeholder';
placeholder.style.visibility = 'hidden';
placeholder.style.display = 'block';
placeholder.style.width = '100%';
placeholder.style.height = '375px';
topZone.appendChild(placeholder);
}
} else {
const placeholder = topZone.querySelector('.placeholder');
if (placeholder) placeholder.remove();
}
}
// This sets up all drag-and-drop event listeners for cards.
export function initDragAndDrop() {
function run() {
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
draggableCards.forEach(card => {
if (!card.dataset.originalContainerId) {
card.dataset.originalContainerId = card.parentNode.id;
}
const header = card.querySelector('.card-header');
if (header) {
header.classList.add('drag-header');
}
let isDragging = false;
let dragTimer = null;
let offsetX = 0, offsetY = 0;
let initialLeft, initialTop;
if (header) {
header.addEventListener('mousedown', function (e) {
e.preventDefault();
const card = this.closest('.card');
// Capture the card's initial bounding rectangle once.
const initialRect = card.getBoundingClientRect();
const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100;
const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100;
card.style.transformOrigin = `${originX}% ${originY}%`;
// Store the initial rect so we use it later.
dragTimer = setTimeout(() => {
isDragging = true;
card.classList.add('dragging');
card.style.pointerEvents = 'none';
addTopZoneHighlight();
const sidebar = document.getElementById('sidebarDropArea');
if (sidebar) {
sidebar.classList.add('active');
sidebar.style.display = 'block';
sidebar.classList.add('highlight');
sidebar.style.height = '800px';
}
// Use the stored initialRect rather than recalculating.
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.
document.body.appendChild(card);
card.style.position = 'absolute';
card.style.left = initialLeft + 'px';
card.style.top = initialTop + 'px';
card.style.width = initialRect.width + 'px';
card.style.height = initialRect.height + 'px';
card.style.minWidth = initialRect.width + 'px';
card.style.flexShrink = '0';
card.style.zIndex = '10000';
}, 500);
});
header.addEventListener('mouseup', function () {
clearTimeout(dragTimer);
});
}
document.addEventListener('mousemove', function (e) {
if (isDragging) {
card.style.left = (e.pageX - offsetX) + 'px';
card.style.top = (e.pageY - offsetY) + 'px';
}
});
document.addEventListener('mouseup', function (e) {
if (isDragging) {
isDragging = false;
card.style.pointerEvents = '';
card.classList.remove('dragging');
removeTopZoneHighlight();
const sidebar = document.getElementById('sidebarDropArea');
if (sidebar) {
sidebar.classList.remove('highlight');
sidebar.style.height = '';
}
let droppedInSidebar = false;
let droppedInTop = false;
// Check if dropped in sidebar drop zone.
const sidebarElem = document.getElementById('sidebarDropArea');
if (sidebarElem) {
const rect = sidebarElem.getBoundingClientRect();
const dropZoneBottom = rect.top + 800; // Virtual drop zone height.
if (
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= dropZoneBottom
) {
insertCardInSidebar(card, e);
droppedInSidebar = true;
}
}
// If not dropped in sidebar, check the top drop zone.
const topRow = document.getElementById('uploadFolderRow');
if (!droppedInSidebar && topRow) {
const rect = topRow.getBoundingClientRect();
if (
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom
) {
let container;
if (card.id === 'uploadCard') {
container = document.getElementById('leftCol');
} else if (card.id === 'folderManagementCard') {
container = document.getElementById('rightCol');
}
if (container) {
ensureTopZonePlaceholder();
updateTopZoneLayout();
container.appendChild(card);
droppedInTop = true;
// Use computed style to determine container's width.
const containerWidth = parseFloat(window.getComputedStyle(container).width);
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) {
const orig = document.getElementById(card.dataset.originalContainerId);
if (orig) {
orig.appendChild(card);
card.style.removeProperty('width');
}
}
// Clear inline styles from dragging.
[
'position',
'left',
'top',
'z-index',
'height',
'min-width',
'flex-shrink',
'transition',
'transform',
'opacity'
].forEach(prop => card.style.removeProperty(prop));
// For sidebar drops, force width to 100%.
if (droppedInSidebar) {
card.style.width = '100%';
}
updateTopZoneLayout();
updateSidebarVisibility();
}
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run);
} else {
run();
}
}

155
extractZip.php Normal file
View File

@@ -0,0 +1,155 @@
<?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 = [];
$allSuccess = true;
$extractedFiles = array(); // Array to collect names of extracted files
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
// ---------- 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 {
// Collect extracted file names from this zip.
for ($i = 0; $i < $zip->numFiles; $i++) {
$entryName = $zip->getNameIndex($i);
$extractedFileName = basename($entryName);
if ($extractedFileName) {
$extractedFiles[] = $extractedFileName;
}
}
// 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, "extractedFiles" => $extractedFiles]);
} else {
echo json_encode(["success" => false, "error" => implode(" ", $errors)]);
}
exit;
?>

View File

@@ -9,7 +9,7 @@ import {
showToast,
updateRowHighlight,
toggleRowSelection,
previewFile as originalPreviewFile
attachEnterKeyListener
} from './domUtils.js';
export let fileData = [];
@@ -152,7 +152,7 @@ function openShareModal(file, folder) {
// ==============================================
// FEATURE: Enhanced Preview Modal with Navigation
// ==============================================
function enhancedPreviewFile(fileUrl, fileName) {
function previewFile(fileUrl, fileName) {
let modal = document.getElementById("filePreviewModal");
if (!modal) {
modal = document.createElement("div");
@@ -172,17 +172,29 @@ function enhancedPreviewFile(fileUrl, fileName) {
modal.innerHTML = `
<div class="modal-content image-preview-modal-content" style="position: relative; max-width: 90vw; max-height: 90vh;">
<span id="closeFileModal" class="close-image-modal" style="position: absolute; top: 10px; right: 10px; font-size: 24px; cursor: pointer;">&times;</span>
<h4 class="image-modal-header" style="text-align: center; margin-top: 40px;"></h4>
<h4 class="image-modal-header"></h4>
<div class="file-preview-container" style="position: relative; text-align: center;"></div>
</div>`;
document.body.appendChild(modal);
document.getElementById("closeFileModal").addEventListener("click", function () {
function closeModal() {
// Pause and reset any video or audio elements within the modal
const mediaElements = modal.querySelectorAll("video, audio");
mediaElements.forEach(media => {
media.pause();
try {
media.currentTime = 0;
} catch(e) {
// Some media types might not support setting currentTime.
}
});
modal.style.display = "none";
});
}
document.getElementById("closeFileModal").addEventListener("click", closeModal);
modal.addEventListener("click", function (e) {
if (e.target === modal) {
modal.style.display = "none";
closeModal();
}
});
}
@@ -252,6 +264,13 @@ function enhancedPreviewFile(fileUrl, fileName) {
video.controls = true;
video.className = "image-modal-img";
container.appendChild(video);
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(fileName)) {
const audio = document.createElement("audio");
audio.src = fileUrl;
audio.controls = true;
audio.className = "audio-modal";
audio.style.maxWidth = "80vw";
container.appendChild(audio);
} else {
container.textContent = "Preview not available for this file type.";
}
@@ -259,10 +278,6 @@ function enhancedPreviewFile(fileUrl, fileName) {
modal.style.display = "flex";
}
export function previewFile(fileUrl, fileName) {
enhancedPreviewFile(fileUrl, fileName);
}
// ==============================================
// ORIGINAL FILE MANAGER FUNCTIONS
// ==============================================
@@ -518,6 +533,8 @@ export function renderGalleryView(folder) {
let thumbnail;
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
thumbnail = `<img src="${folderPath + encodeURIComponent(file.name)}?t=${new Date().getTime()}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: 150px; display: block; margin: 0 auto;">`;
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
} else {
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
}
@@ -661,6 +678,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 +689,7 @@ document.addEventListener("DOMContentLoaded", function () {
window.filesToDelete = [];
});
}
const confirmDelete = document.getElementById("confirmDeleteFiles");
if (confirmDelete) {
confirmDelete.addEventListener("click", function () {
@@ -700,7 +719,7 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
});
attachEnterKeyListener("downloadZipModal", "confirmDownloadZip");
export function handleDownloadZipSelected(e) {
e.preventDefault();
e.stopImmediatePropagation();
@@ -711,6 +730,69 @@ 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) {
// If the server returned a list of extracted files, join them into a string.
let toastMessage = "Zip file(s) extracted successfully!";
if (data.extractedFiles && Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
toastMessage = "Extracted: " + data.extractedFiles.join(", ");
}
showToast(toastMessage);
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 +802,7 @@ document.addEventListener("DOMContentLoaded", function () {
document.getElementById("downloadZipModal").style.display = "none";
});
}
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
if (confirmDownloadZip) {
confirmDownloadZip.addEventListener("click", function () {
@@ -1007,7 +1090,7 @@ function adjustEditorSize() {
if (modal && window.currentEditor) {
// Calculate available height for the editor.
// If you have a header or footer inside the modal, subtract their heights.
const headerHeight = 60;
const headerHeight = 60; // adjust this value as needed
const availableHeight = modal.clientHeight - headerHeight;
window.currentEditor.setSize("100%", availableHeight + "px");
}
@@ -1035,7 +1118,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.");
}
@@ -1054,12 +1137,12 @@ export function editFile(fileName, folder) {
modal.innerHTML = `
<div class="editor-header">
<h3 class="editor-title">Editing: ${fileName}</h3>
<div class="editor-controls">
<button id="decreaseFont" class="btn btn-sm btn-secondary">A-</button>
<button id="increaseFont" class="btn btn-sm btn-secondary">A+</button>
</div>
<button id="closeEditorX" class="editor-close-btn">&times;</button>
</div>
<div id="editorControls" class="editor-controls">
<button id="decreaseFont" class="btn btn-sm btn-secondary">A-</button>
<button id="increaseFont" class="btn btn-sm btn-secondary">A+</button>
</div>
<textarea id="fileEditor" class="editor-textarea">${content}</textarea>
<div class="editor-footer">
<button id="saveBtn" class="btn btn-primary">Save</button>
@@ -1157,13 +1240,17 @@ export function saveFile(fileName, folder) {
}
export function displayFilePreview(file, container) {
// Use the underlying File object if it exists (for resumable files)
const actualFile = file.file || file;
container.style.display = "inline-block";
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(actualFile.name)) {
const img = document.createElement("img");
img.src = URL.createObjectURL(file);
img.src = URL.createObjectURL(actualFile);
img.classList.add("file-preview-img");
container.innerHTML = ""; // Clear previous content
container.appendChild(img);
} else {
container.innerHTML = ""; // Clear previous content
const iconSpan = document.createElement("span");
iconSpan.classList.add("material-icons", "file-icon");
iconSpan.textContent = "insert_drive_file";
@@ -1192,13 +1279,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", () => {
@@ -1209,6 +1311,7 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("newFileName").value = "";
});
}
const submitBtn = document.getElementById("submitRenameFile");
if (submitBtn) {
submitBtn.addEventListener("click", function () {
@@ -1269,4 +1372,173 @@ 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 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";
// Use fixed positioning so the menu is relative to the viewport
menu.style.position = "fixed";
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", () => {
menuItem.style.backgroundColor = document.body.classList.contains("dark-mode") ? "#444" : "#f0f0f0";
});
menuItem.addEventListener("mouseout", () => {
menuItem.style.backgroundColor = "";
});
menuItem.addEventListener("click", () => {
item.action();
hideFileContextMenu();
});
menu.appendChild(menuItem);
});
// Use the event's clientX and clientY coordinates (which are viewport-relative)
menu.style.left = x + "px";
menu.style.top = y + "px";
menu.style.display = "block";
// Adjust if the menu would extend past the bottom of the viewport
const menuRect = menu.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (menuRect.bottom > viewportHeight) {
let newTop = viewportHeight - menuRect.height;
if (newTop < 0) newTop = 0;
menu.style.top = newTop + "px";
}
}
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.clientX, e.clientY, 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)
@@ -60,7 +60,109 @@ function getParentFolder(folder) {
}
// ----------------------
// DOM Building Functions
// Breadcrumb Functions
// ----------------------
// Render breadcrumb for a normalized folder path.
function renderBreadcrumb(normalizedFolder) {
if (normalizedFolder === "root") {
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
}
const parts = normalizedFolder.split("/");
let breadcrumbItems = [];
// Always start with "Root".
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="root">Root</span>`);
let cumulative = "";
parts.forEach((part, index) => {
cumulative = index === 0 ? part : cumulative + "/" + part;
breadcrumbItems.push(`<span class="breadcrumb-separator"> / </span>`);
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${cumulative}">${escapeHTML(part)}</span>`);
});
return breadcrumbItems.join('');
}
// Bind click and drag-and-drop events to breadcrumb links.
function bindBreadcrumbEvents() {
const breadcrumbLinks = document.querySelectorAll(".breadcrumb-link");
breadcrumbLinks.forEach(link => {
// Click event for navigation.
link.addEventListener("click", function (e) {
e.stopPropagation();
let folder = this.getAttribute("data-folder");
window.currentFolder = folder;
localStorage.setItem("lastOpenedFolder", folder);
const titleEl = document.getElementById("fileListTitle");
if (folder === "root") {
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
} else {
titleEl.innerHTML = "Files in (" + renderBreadcrumb(folder) + ")";
}
// Expand the folder tree to ensure the target is visible.
expandTreePath(folder);
// Update folder tree selection.
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
if (targetOption) {
targetOption.classList.add("selected");
}
// Load the file list.
loadFileList(folder);
// Re-bind breadcrumb events to ensure all links remain active.
bindBreadcrumbEvents();
});
// Drag-and-drop events.
link.addEventListener("dragover", function (e) {
e.preventDefault();
this.classList.add("drop-hover");
});
link.addEventListener("dragleave", function (e) {
this.classList.remove("drop-hover");
});
link.addEventListener("drop", function (e) {
e.preventDefault();
this.classList.remove("drop-hover");
const dropFolder = this.getAttribute("data-folder");
let dragData;
try {
dragData = JSON.parse(e.dataTransfer.getData("application/json"));
} catch (err) {
console.error("Invalid drag data on breadcrumb:", err);
return;
}
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
if (filesToMove.length === 0) return;
fetch("moveFiles.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content")
},
body: JSON.stringify({
source: dragData.sourceFolder,
files: filesToMove,
destination: dropFolder
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(`File(s) moved successfully to ${dropFolder}!`);
loadFileList(dragData.sourceFolder);
} else {
showToast("Error moving files: " + (data.error || "Unknown error"));
}
})
.catch(error => {
console.error("Error moving files via drop on breadcrumb:", error);
showToast("Error moving files.");
});
});
});
}
// ----------------------
// DOM Building Functions for Folder Tree
// ----------------------
// Recursively builds HTML for the folder tree as nested <ul> elements.
@@ -72,18 +174,16 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
if (folder.toLowerCase() === "trash") {
continue;
}
const fullPath = parentPath ? parentPath + "/" + folder : folder;
const hasChildren = Object.keys(tree[folder]).length > 0;
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
html += `<li class="folder-item">`;
if (hasChildren) {
const toggleSymbol = (displayState === "none") ? "[+]" : "[-]";
const toggleSymbol = (displayState === 'none') ? '[+]' : '[' + '<span class="custom-dash">-</span>' + ']';
html += `<span class="folder-toggle" data-folder="${fullPath}">${toggleSymbol}</span>`;
} else {
html += `<span class="folder-indent-placeholder"></span>`;
}
// Use escapeHTML to safely render the folder name.
html += `<span class="folder-option" data-folder="${fullPath}">${escapeHTML(folder)}</span>`;
if (hasChildren) {
html += renderFolderTree(tree[folder], fullPath, displayState);
@@ -94,7 +194,7 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
return html;
}
// Expands the folder tree along a given path.
// Expands the folder tree along a given normalized path.
function expandTreePath(path) {
const parts = path.split("/");
let cumulative = "";
@@ -109,7 +209,7 @@ function expandTreePath(path) {
nestedUl.classList.add("expanded");
const toggle = li.querySelector(".folder-toggle");
if (toggle) {
toggle.textContent = "[-]";
toggle.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
let state = loadFolderTreeState();
state[cumulative] = "block";
saveFolderTreeState(state);
@@ -122,19 +222,15 @@ function expandTreePath(path) {
// ----------------------
// Drag & Drop Support for Folder Tree Nodes
// ----------------------
// When a draggable file is dragged over a folder node, allow the drop and add a visual cue.
function folderDragOverHandler(event) {
event.preventDefault();
event.currentTarget.classList.add("drop-hover");
}
// Remove the visual cue when the drag leaves.
function folderDragLeaveHandler(event) {
event.currentTarget.classList.remove("drop-hover");
}
// When a file is dropped onto a folder node, send a move request.
function folderDropHandler(event) {
event.preventDefault();
event.currentTarget.classList.remove("drop-hover");
@@ -143,10 +239,9 @@ function folderDropHandler(event) {
try {
dragData = JSON.parse(event.dataTransfer.getData("application/json"));
} catch (e) {
console.error("Invalid drag data");
console.error("Invalid drag data", e);
return;
}
// Use the files array if present, or fall back to a single file.
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
if (filesToMove.length === 0) return;
fetch("moveFiles.php", {
@@ -210,16 +305,10 @@ export async function loadFolderTree(selectedFolder) {
}
let html = `<div id="rootRow" class="root-row">
<span class="folder-toggle" data-folder="root">[-]</span>
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
</div>`;
if (folders.length === 0) {
html += `<ul class="folder-tree expanded">
<li class="folder-item">
<span class="folder-option" data-folder="root">(Root)</span>
</li>
</ul>`;
} else {
<span class="folder-toggle" data-folder="root">[<span class="custom-dash">-</span>]</span>
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
</div>`;
if (folders.length > 0) {
const tree = buildFolderTree(folders);
html += renderFolderTree(tree, "", "block");
}
@@ -232,15 +321,25 @@ export async function loadFolderTree(selectedFolder) {
el.addEventListener("drop", folderDropHandler);
});
// Determine current folder.
// Determine current folder (normalized).
if (selectedFolder) {
window.currentFolder = selectedFolder;
} else {
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
}
localStorage.setItem("lastOpenedFolder", window.currentFolder);
document.getElementById("fileListTitle").textContent =
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")";
// Update file list title using breadcrumb.
const titleEl = document.getElementById("fileListTitle");
if (window.currentFolder === "root") {
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
} else {
titleEl.innerHTML = "Files in (" + renderBreadcrumb(window.currentFolder) + ")";
}
// Bind breadcrumb events (click and drag/drop).
bindBreadcrumbEvents();
// Load file list.
loadFileList(window.currentFolder);
// Expand tree to current folder.
@@ -249,13 +348,14 @@ export async function loadFolderTree(selectedFolder) {
expandTreePath(window.currentFolder);
}
// Highlight current folder.
// Highlight current folder in folder tree.
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
if (selectedEl) {
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
selectedEl.classList.add("selected");
}
// Event binding for folder selection.
// Event binding for folder selection in folder tree.
container.querySelectorAll(".folder-option").forEach(el => {
el.addEventListener("click", function (e) {
e.stopPropagation();
@@ -264,8 +364,14 @@ export async function loadFolderTree(selectedFolder) {
const selected = this.getAttribute("data-folder");
window.currentFolder = selected;
localStorage.setItem("lastOpenedFolder", selected);
document.getElementById("fileListTitle").textContent =
selected === "root" ? "Files in (Root)" : "Files in (" + selected + ")";
const titleEl = document.getElementById("fileListTitle");
if (selected === "root") {
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
} else {
titleEl.innerHTML = "Files in (" + renderBreadcrumb(selected) + ")";
}
// Re-bind breadcrumb events so the new breadcrumb is clickable.
bindBreadcrumbEvents();
loadFileList(selected);
});
});
@@ -281,7 +387,7 @@ export async function loadFolderTree(selectedFolder) {
if (nestedUl.classList.contains("collapsed") || !nestedUl.classList.contains("expanded")) {
nestedUl.classList.remove("collapsed");
nestedUl.classList.add("expanded");
this.textContent = "[-]";
this.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
state["root"] = "block";
} else {
nestedUl.classList.remove("expanded");
@@ -304,7 +410,7 @@ export async function loadFolderTree(selectedFolder) {
if (siblingUl.classList.contains("collapsed") || !siblingUl.classList.contains("expanded")) {
siblingUl.classList.remove("collapsed");
siblingUl.classList.add("expanded");
this.textContent = "[-]";
this.innerHTML = "[" + '<span class="custom-dash">-</span>' + "]";
state[folderPath] = "block";
} else {
siblingUl.classList.remove("expanded");
@@ -332,6 +438,7 @@ export function loadFolderList(selectedFolder) {
// ----------------------
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
function openRenameFolderModal() {
@@ -343,13 +450,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";
@@ -406,7 +519,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');
@@ -437,13 +550,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) {
@@ -484,4 +598,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();

30
getConfig.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
require 'config.php';
header('Content-Type: application/json');
$configFile = USERS_DIR . 'adminConfig.json';
if (file_exists($configFile)) {
$encryptedContent = file_get_contents($configFile);
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
if ($decryptedContent === false) {
http_response_code(500);
echo json_encode(['error' => 'Failed to decrypt configuration.']);
exit;
}
echo $decryptedContent;
} else {
echo json_encode([
'oidc' => [
'providerUrl' => 'https://your-oidc-provider.com',
'clientId' => 'YOUR_CLIENT_ID',
'clientSecret' => 'YOUR_CLIENT_SECRET',
'redirectUri' => 'https://yourdomain.com/auth.php?oidc=callback'
],
'loginOptions' => [
'disableFormLogin' => false,
'disableBasicAuth' => false,
'disableOIDCLogin' => false
]
]);
}
?>

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Multi File Upload Editor</title>
<title>FileRise</title>
<link rel="icon" type="image/png" href="/assets/logo.png">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<meta name="csrf-token" content="">
@@ -20,6 +20,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/resumable.js/1.1.0/resumable.min.js"></script>
<link rel="stylesheet" href="styles.css" />
</head>
@@ -35,9 +36,15 @@
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
<!-- Drop shadow filter -->
<!-- Drop shadow filter with animated attributes for a lifting effect -->
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2" />
<feDropShadow id="dropShadow" dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2">
<!-- Animate the vertical offset: from 2 to 1 (as it rises), hold, then back to 2 -->
<animate attributeName="dy" values="2;1;1;2" keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
<!-- Animate the blur similarly: from 2 to 1.5 then back to 2 -->
<animate attributeName="stdDeviation" values="2;1.5;1.5;2" keyTimes="0;0.2;0.8;1" dur="5s"
fill="freeze" />
</feDropShadow>
</filter>
</defs>
<style type="text/css">
@@ -61,37 +68,40 @@
fill: #1565C0;
}
</style>
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
<!-- Divider lines for drawers -->
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
<!-- Drawers with Handles -->
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="27" r="1.5" class="handle" />
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="39" r="1.5" class="handle" />
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="51" r="1.5" class="handle" />
<!-- Additional detail: a small top handle on the cabinet door -->
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
<!-- Group that will animate upward and then back down once -->
<g id="cabinetGroup">
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
<!-- Divider lines for drawers -->
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
<!-- Drawers with Handles -->
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="27" r="1.5" class="handle" />
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="39" r="1.5" class="handle" />
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
<circle cx="54" cy="51" r="1.5" class="handle" />
<!-- Additional detail: a small top handle on the cabinet door -->
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
<!-- Animate transform: rises by 2 pixels over 1s, holds for 3s, then falls over 1s (total 5s) -->
<animateTransform attributeName="transform" type="translate" values="0 0; 0 -2; 0 -2; 0 0"
keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
</g>
</svg>
</div>
</div>
<div class="header-title">
<h1>Multi File Upload Editor</h1>
<h1>FileRise</h1>
</div>
<div class="header-right">
<div class="header-buttons">
<button id="logoutBtn" title="Logout">
<i class="material-icons">exit_to_app</i>
</button>
<!-- Restore Files Modal (Admin Only) -->
<button id="changePasswordBtn" title="Change Password">
<i class="material-icons">vpn_key</i>
</button>
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
<div class="modal-content">
<h4 class="custom-restore-header">
@@ -113,10 +123,10 @@
</div>
</div>
</div>
<button id="addUserBtn" title="Add User">
<button id="addUserBtn" title="Add User" style="display: none;">
<i class="material-icons">person_add</i>
</button>
<button id="removeUserBtn" title="Remove User">
<button id="removeUserBtn" title="Remove User" style="display: none;">
<i class="material-icons">person_remove</i>
</button>
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
@@ -126,199 +136,219 @@
<!-- Custom Toast Container -->
<div id="customToast"></div>
<div class="container-fluid">
<!-- Login Form -->
<div class="row" id="loginForm">
<div class="col-12">
<form id="authForm" method="post">
<div class="form-group">
<label for="loginUsername">User:</label>
<input type="text" class="form-control" id="loginUsername" name="username" required />
</div>
<div class="form-group">
<label for="loginPassword">Password:</label>
<input type="password" class="form-control" id="loginPassword" name="password" required />
</div>
<button type="submit" class="btn btn-primary btn-block btn-login">Login</button>
</form>
</div>
</div>
<!-- Main Operations: Upload and Folder Management -->
<div id="mainOperations">
<div class="container" style="max-width: 1400px; margin: 0 auto;">
<div class="row align-items-start" id="uploadFolderRow">
<!-- Upload Card: 50% width on medium, 58% on large -->
<div class="col-md-6 col-lg-7 d-flex">
<div id="uploadCard" class="card flex-fill" style="max-width: 900px; width: 100%;">
<div class="card-header">Upload Files/Folders</div>
<div class="card-body d-flex flex-column">
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column"
style="height: 100%;">
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
<div id="uploadDropArea"
style="border:2px dashed #ccc; padding:20px; cursor:pointer; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<span>Drop files/folders here or click 'Choose files'</span>
<br />
<input type="file" id="file" name="file[]" class="form-control-file" multiple required
webkitdirectory directory mozdirectory style="opacity:0; position:absolute; z-index:-1;" />
<button type="button" onclick="document.getElementById('file').click();">Choose Folder</button>
</div>
</div>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
<div id="uploadProgressContainer"></div>
</form>
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
<div class="main-wrapper">
<!-- Sidebar Drop Zone: Hidden until you drag a card (display controlled by JS) -->
<div id="sidebarDropArea" class="drop-target-sidebar"></div>
<!-- Main Column -->
<div id="mainColumn" class="main-column">
<div class="container-fluid">
<!-- Login Form (unchanged) -->
<div class="row" id="loginForm">
<div class="col-12">
<form id="authForm" method="post">
<div class="form-group">
<label for="loginUsername">User:</label>
<input type="text" class="form-control" id="loginUsername" name="username" required />
</div>
<div class="form-group">
<label for="loginPassword">Password:</label>
<input type="password" class="form-control" id="loginPassword" name="password" required />
</div>
<button type="submit" class="btn btn-primary btn-block btn-login">Login</button>
<div class="form-group remember-me-container">
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
<label for="rememberMeCheckbox">Remember me</label>
</div>
</form>
<!-- OIDC Login Option -->
<div class="text-center mt-3">
<button id="oidcLoginBtn" class="btn btn-secondary">Login with OIDC</button>
</div>
<!-- Basic HTTP Login Option -->
<div class="text-center mt-3">
<a href="login_basic.php" class="btn btn-secondary">Use Basic HTTP Login</a>
</div>
</div>
</div>
<!-- Folder Management Card -->
<div class="col-md-6 col-lg-5 d-flex">
<div id="folderManagementCard" class="card flex-fill" style="max-width: 900px; width: 100%; position: relative;">
<!-- Card header with folder management title and help icon -->
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<span>Folder Navigation &amp; Management</span>
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help"
style="padding: 0; border: none; background: none;">
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
</button>
<!-- Main Operations: Upload and Folder Management -->
<div id="mainOperations">
<div class="container" style="max-width: 1400px; margin: 0 auto;">
<!-- Top Zone: Two columns (60% and 40%) -->
<div id="uploadFolderRow" class="row">
<!-- Left Column (60% for Upload Card) -->
<div id="leftCol" class="col-md-7" style="display: flex; justify-content: center;">
<div id="uploadCard" class="card" style="width: 100%;">
<div class="card-header">Upload Files/Folders</div>
<div class="card-body d-flex flex-column">
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column">
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
<div id="uploadDropArea"
style="border:2px dashed #ccc; padding:20px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
<span>Drop files/folders here or click 'Choose Files'</span>
<br />
<input type="file" id="file" name="file[]" class="form-control-file" multiple
style="opacity:0; position:absolute; width:1px; height:1px;" />
<button type="button" id="customChooseBtn">Choose Files</button>
</div>
</div>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
<div id="uploadProgressContainer"></div>
</form>
</div>
</div>
</div>
<div class="card-body custom-folder-card-body">
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
<div id="folderTreeContainer"></div>
</div>
<!-- Folder actions (create, rename, delete) -->
<div class="folder-actions mt-3">
<button id="createFolderBtn" class="btn btn-primary">Create Folder</button>
<!-- Create Folder Modal -->
<div id="createFolderModal" class="modal">
<div class="modal-content">
<h4>Create Folder</h4>
<input type="text" id="newFolderName" class="form-control" placeholder="Enter folder name"
style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelCreateFolder" class="btn btn-secondary">Cancel</button>
<button id="submitCreateFolder" class="btn btn-primary">Create</button>
<!-- Right Column (40% for Folder Management Card) -->
<div id="rightCol" class="col-md-5" style="display: flex; justify-content: center;">
<div id="folderManagementCard" class="card" style="width: 100%; position: relative;">
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<span>Folder Navigation &amp; Management</span>
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help"
style="padding: 0; border: none; background: none;">
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
</button>
</div>
<div class="card-body custom-folder-card-body">
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
<div id="folderTreeContainer"></div>
</div>
<div class="folder-actions mt-3">
<button id="createFolderBtn" class="btn btn-primary">Create Folder</button>
<div id="createFolderModal" class="modal">
<div class="modal-content">
<h4>Create Folder</h4>
<input type="text" id="newFolderName" class="form-control" placeholder="Enter folder name"
style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelCreateFolder" class="btn btn-secondary">Cancel</button>
<button id="submitCreateFolder" class="btn btn-primary">Create</button>
</div>
</div>
</div>
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<div id="renameFolderModal" class="modal">
<div class="modal-content">
<h4>Rename Folder</h4>
<input type="text" id="newRenameFolderName" class="form-control"
placeholder="Enter new folder name" style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFolder" class="btn btn-secondary">Cancel</button>
<button id="submitRenameFolder" class="btn btn-primary">Rename</button>
</div>
</div>
</div>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
<i class="material-icons">delete</i>
</button>
<div id="deleteFolderModal" class="modal">
<div class="modal-content">
<h4>Delete Folder</h4>
<p id="deleteFolderMessage">Are you sure you want to delete this folder?</p>
<div style="margin-top:15px; text-align:right;">
<button id="cancelDeleteFolder" class="btn btn-secondary">Cancel</button>
<button id="confirmDeleteFolder" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
</div>
</div>
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<!-- Rename Folder Modal -->
<div id="renameFolderModal" class="modal">
<div class="modal-content">
<h4>Rename Folder</h4>
<input type="text" id="newRenameFolderName" class="form-control"
placeholder="Enter new folder name" style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFolder" class="btn btn-secondary">Cancel</button>
<button id="submitRenameFolder" class="btn btn-primary">Rename</button>
</div>
</div>
</div>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
<i class="material-icons">delete</i>
</button>
<!-- Delete Folder Modal -->
<div id="deleteFolderModal" class="modal">
<div class="modal-content">
<h4>Delete Folder</h4>
<p id="deleteFolderMessage">Are you sure you want to delete this folder?</p>
<div style="margin-top:15px; text-align:right;">
<button id="cancelDeleteFolder" class="btn btn-secondary">Cancel</button>
<button id="confirmDeleteFolder" class="btn btn-danger">Delete</button>
</div>
<div id="folderHelpTooltip" class="folder-help-tooltip"
style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);">
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
<li>Click on a folder in the tree to view its files.</li>
<li>Use [-] to collapse and [+] to expand folders.</li>
<li>Select a folder and click "Create Folder" to add a subfolder.</li>
<li>To rename or delete a folder, select it and then click the appropriate button.</li>
</ul>
</div>
</div>
</div>
<!-- Help Tooltip: Initially hidden -->
<div id="folderHelpTooltip" class="folder-help-tooltip"
style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);">
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
<li>Click on a folder in the tree to view its files.</li>
<li>Use [-] to collapse and [+] to expand folders.</li>
<li>Select a folder and click "Create Folder" to add a subfolder.</li>
<li>To rename or delete a folder, select it and then click the appropriate button.</li>
</ul>
</div>
</div> <!-- end uploadFolderRow -->
</div> <!-- end container -->
</div> <!-- end mainOperations -->
<!-- File List Section -->
<div id="fileListContainer" style="display: none;">
<h2 id="fileListTitle">Files in (Root)</h2>
<div id="fileListActions" class="file-list-actions">
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Files</button>
<div id="deleteFilesModal" class="modal">
<div class="modal-content">
<h4>Delete Selected Files</h4>
<p id="deleteFilesMessage">Are you sure you want to delete the selected files?</p>
<div class="modal-footer">
<button id="cancelDeleteFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmDeleteFiles" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Files</button>
<div id="copyFilesModal" class="modal">
<div class="modal-content">
<h4>Copy Selected Files</h4>
<p id="copyFilesMessage">Select a target folder for copying the selected files:</p>
<select id="copyTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelCopyFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmCopyFiles" class="btn btn-primary">Copy</button>
</div>
</div>
</div>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Files</button>
<div id="moveFilesModal" class="modal">
<div class="modal-content">
<h4>Move Selected Files</h4>
<p id="moveFilesMessage">Select a target folder for moving the selected files:</p>
<select id="moveTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelMoveFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmMoveFiles" class="btn btn-primary">Move</button>
</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>
<div id="downloadZipModal" class="modal" style="display:none;">
<div class="modal-content">
<h4>Download Selected Files as Zip</h4>
<p>Enter a name for the zip file:</p>
<input type="text" id="zipFileNameInput" class="form-control" placeholder="files.zip" />
<div class="modal-footer" style="margin-top:15px; text-align:right;">
<button id="cancelDownloadZip" class="btn btn-secondary">Cancel</button>
<button id="confirmDownloadZip" class="btn btn-primary">Download</button>
</div>
</div>
</div>
</div>
<div id="fileList"></div>
</div>
</div>
</div>
<!-- File List Section -->
<div id="fileListContainer" style="display: none;">
<h2 id="fileListTitle">Files in (Root)</h2>
<div id="fileListActions" class="file-list-actions">
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Files</button>
<!-- Delete Files Modal -->
<div id="deleteFilesModal" class="modal">
<div class="modal-content">
<h4>Delete Selected Files</h4>
<p id="deleteFilesMessage">Are you sure you want to delete the selected files?</p>
<div class="modal-footer">
<button id="cancelDeleteFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmDeleteFiles" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
</div> <!-- end container-fluid -->
</div> <!-- end mainColumn -->
</div> <!-- end main-wrapper -->
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Files</button>
<!-- Copy Files Modal -->
<div id="copyFilesModal" class="modal">
<div class="modal-content">
<h4>Copy Selected Files</h4>
<p id="copyFilesMessage">Select a target folder for copying the selected files:</p>
<select id="copyTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelCopyFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmCopyFiles" class="btn btn-primary">Copy</button>
</div>
</div>
</div>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Files</button>
<!-- Move Files Modal -->
<div id="moveFilesModal" class="modal">
<div class="modal-content">
<h4>Move Selected Files</h4>
<p id="moveFilesMessage">Select a target folder for moving the selected files:</p>
<select id="moveTargetFolder" class="form-control modal-input"></select>
<div class="modal-footer">
<button id="cancelMoveFiles" class="btn btn-secondary">Cancel</button>
<button id="confirmMoveFiles" class="btn btn-primary">Move</button>
</div>
</div>
</div>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button>
<!-- Download Zip Modal -->
<div id="downloadZipModal" class="modal" style="display:none;">
<div class="modal-content">
<h4>Download Selected Files as Zip</h4>
<p>Enter a name for the zip file:</p>
<input type="text" id="zipFileNameInput" class="form-control" placeholder="files.zip" />
<div class="modal-footer" style="margin-top:15px; text-align:right;">
<button id="cancelDownloadZip" class="btn btn-secondary">Cancel</button>
<button id="confirmDownloadZip" class="btn btn-primary">Download</button>
</div>
</div>
</div>
</div>
<div id="fileList"></div>
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
<div id="changePasswordModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width:400px; margin:auto;">
<span id="closeChangePasswordModal" style="cursor:pointer;">&times;</span>
<h3>Change Password</h3>
<input type="password" id="oldPassword" placeholder="Old Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="newPassword" placeholder="New Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="confirmPassword" placeholder="Confirm New Password"
style="width:100%; margin: 5px 0;" />
<button id="saveNewPasswordBtn" class="btn btn-primary" style="width:100%;">Save</button>
</div>
</div>
<!-- Add User Modal -->
<div id="addUserModal" class="modal">
<div class="modal-content">
<h3>Create New User</h3>
<label for="newUsername">Username:</label>
<input type="text" id="newUsername" class="form-control" />
<label for="newPassword">Password:</label>
<input type="password" id="newPassword" class="form-control" />
<label for="addUserPassword">Password:</label>
<input type="password" id="addUserPassword" class="form-control" />
<div id="adminCheckboxContainer">
<input type="checkbox" id="isAdmin" />
<label for="isAdmin">Grant Admin Access</label>
@@ -329,8 +359,6 @@
</div>
</div>
</div>
<!-- Remove User Modal -->
<div id="removeUserModal" class="modal">
<div class="modal-content">
<h3>Remove User</h3>
@@ -342,8 +370,6 @@
</div>
</div>
</div>
<!-- Rename File Modal -->
<div id="renameFileModal" class="modal">
<div class="modal-content">
<h4>Rename File</h4>
@@ -355,8 +381,6 @@
</div>
</div>
</div>
<!-- Custom Confirm Modal -->
<div id="customConfirmModal" class="modal" style="display:none;">
<div class="modal-content">
<p id="confirmMessage"></p>

62
login_basic.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
require 'config.php';
session_start();
$usersFile = USERS_DIR . USERS_FILE;
// Reuse the same authentication function
function authenticate($username, $password)
{
global $usersFile;
if (!file_exists($usersFile)) {
return false;
}
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
if ($username === $storedUser && password_verify($password, $storedPass)) {
return $storedRole; // Return the user's role
}
}
return false;
}
// Check if the user has sent HTTP Basic auth credentials.
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="FileRise Login"');
header('HTTP/1.0 401 Unauthorized');
echo 'Authorization Required';
exit;
} else {
$username = trim($_SERVER['PHP_AUTH_USER']);
$password = trim($_SERVER['PHP_AUTH_PW']);
// Validate username format (optional)
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
header('WWW-Authenticate: Basic realm="FileRise Login"');
header('HTTP/1.0 401 Unauthorized');
echo 'Invalid username format';
exit;
}
// Attempt authentication
$userRole = authenticate($username, $password);
if ($userRole !== false) {
// Successful login
session_regenerate_id(true);
$_SESSION["authenticated"] = true;
$_SESSION["username"] = $username;
$_SESSION["isAdmin"] = ($userRole === "1"); // Assuming "1" means admin
// Redirect to the main page
header("Location: index.html");
exit;
} else {
// Invalid credentials; prompt again
header('WWW-Authenticate: Basic realm="FileRise Login"');
header('HTTP/1.0 401 Unauthorized');
echo 'Invalid credentials';
exit;
}
}
?>

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;
?>

11
main.js
View File

@@ -17,6 +17,7 @@ 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'
function loadCsrfToken() {
fetch('token.php', { credentials: 'include' })
@@ -64,6 +65,14 @@ document.addEventListener("DOMContentLoaded", function () {
// Call initAuth synchronously.
initAuth();
const newPasswordInput = document.getElementById("newPassword");
if (newPasswordInput) {
newPasswordInput.addEventListener("input", function() {
console.log("newPassword input event:", this.value);
});
} else {
console.error("newPassword input not found!");
}
// --- Dark Mode Persistence ---
const darkModeToggle = document.getElementById("darkModeToggle");
const storedDarkMode = localStorage.getItem("darkMode");
@@ -121,6 +130,8 @@ document.addEventListener("DOMContentLoaded", function () {
if (authenticated) {
window.currentFolder = "root";
loadFileList(window.currentFolder);
initDragAndDrop();
loadSidebarOrder();
initFileActions();
initUpload();
loadFolderTree();

View File

@@ -1,32 +1,31 @@
// networkUtils.js
export function sendRequest(url, method = "GET", data = null) {
console.log("Sending request to:", url, "with method:", method);
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
const options = {
method,
credentials: 'include', // include cookies in requests
credentials: 'include',
headers: {}
};
// Merge custom headers
Object.assign(options.headers, customHeaders);
// If data is provided and is not FormData, assume JSON.
if (data && !(data instanceof FormData)) {
options.headers["Content-Type"] = "application/json";
if (!options.headers["Content-Type"]) {
options.headers["Content-Type"] = "application/json";
}
options.body = JSON.stringify(data);
} else if (data instanceof FormData) {
// For FormData, don't set the Content-Type header; the browser will handle it.
options.body = data;
}
return fetch(url, options)
.then(response => {
console.log("Response status:", response.status);
if (!response.ok) {
return response.text().then(text => {
throw new Error(`HTTP error ${response.status}: ${text}`);
});
}
return response.json().catch(() => {
console.warn("Response is not JSON, returning as text");
return response.text();
});
const clonedResponse = response.clone();
return response.json().catch(() => clonedResponse.text());
});
}

63
removeChunks.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// Validate CSRF token from POST
$receivedToken = isset($_POST['csrf_token']) ? trim($_POST['csrf_token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403);
exit;
}
// Ensure a folder parameter is provided
if (!isset($_POST['folder'])) {
echo json_encode(["error" => "No folder specified"]);
http_response_code(400);
exit;
}
$folder = $_POST['folder'];
// Validate the folder name to allow only expected characters (adjust the regex as needed)
if (!preg_match('/^resumable_[A-Za-z0-9\-]+$/', $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
http_response_code(400);
exit;
}
$tempDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
// If the folder doesn't exist, simply return success.
if (!is_dir($tempDir)) {
echo json_encode(["success" => true, "message" => "Temporary folder already removed."]);
exit;
}
// Recursively delete directory and its contents
function rrmdir($dir) {
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
$path = $dir . DIRECTORY_SEPARATOR . $object;
if (is_dir($path) && !is_link($path)) {
rrmdir($path);
} else {
@unlink($path);
}
}
}
@rmdir($dir);
}
}
rrmdir($tempDir);
// check if folder still exists
if (!is_dir($tempDir)) {
echo json_encode(["success" => true, "message" => "Temporary folder removed."]);
} else {
echo json_encode(["error" => "Failed to remove temporary folder."]);
http_response_code(500);
}
?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 626 KiB

BIN
resources/dark-gallery.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

BIN
resources/dark-login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 400 KiB

BIN
resources/light-preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
resources/light-share.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

BIN
resources/light-trash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

View File

@@ -12,31 +12,28 @@ body {
body {
letter-spacing: 0.2px;
overflow-x: hidden;
}
.custom-dash {
display: inline-block;
transform: scaleX(1.5);
padding-left: 2px !important;
padding-right: 2px !important;
}
/* CONTAINER */
.container {
margin-top: 20px;
}
.container,
.container-fluid {
padding-left: 5px !important;
padding-right: 5px !important;
margin-top: 20px;
margin-top: 10px;
padding-right: 4px !important;
padding-left: 4px !important;
}
/* Increase left/right padding for larger screens */
@media (min-width: 768px) {
@media (min-width: 1300px) {
.container-fluid {
padding-left: 50px !important;
padding-right: 50px !important;
}
}
@media (min-width: 1200px) {
.container-fluid {
padding-left: 100px !important;
padding-right: 100px !important;
padding-left: 40px !important;
padding-right: 40px !important;
}
}
@@ -47,9 +44,6 @@ body {
/************************************************************/
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
/************************************************************/
#uploadCard, #folderManagementCard {
min-height: 342px;
}
.btn-login {
margin-top: 10px;
@@ -142,14 +136,14 @@ body.dark-mode header {
justify-content: flex-end;
flex: 1;
min-width: 150px;
gap: 10px;
gap: 0px;
}
.header-buttons button {
background: none;
border: none;
cursor: pointer;
padding: 10px;
padding: 9px;
border-radius: 50%;
color: #fff;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
@@ -229,17 +223,19 @@ body.dark-mode .folder-help-tooltip {
#folderHelpBtn i.material-icons.folder-help-icon {
-webkit-text-fill-color: orange !important;
color: inherit !important;
padding-right: 10px !important;
}
body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
-webkit-text-fill-color: #ffa500 !important;
padding-right: 10px !important;
}
/************************************************************/
/* RESPONSIVE HEADER FIXES */
/************************************************************/
@media (max-width: 970px) {
@media (max-width: 790px) {
.header-container {
flex-wrap: wrap;
height: auto;
@@ -279,7 +275,7 @@ body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
flex-direction: row;
justify-content: center;
gap: 5px;
margin-top: 10px;
margin-top: 5px;
}
}
@@ -292,12 +288,14 @@ body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
color: white;
}
.material-icons.folder-icon {
.material-icons.folder-icon,
.material-icons.gallery-icon {
color: black;
margin-right: 5px;
}
body.dark-mode .material-icons.folder-icon {
body.dark-mode .material-icons.folder-icon,
body.dark-mode .material-icons.gallery-icon {
color: white;
margin-right: 5px;
}
@@ -311,7 +309,7 @@ body.dark-mode .material-icons.folder-icon {
border: none;
color: red;
cursor: pointer;
margin-right: 8px;
margin-right: 0px;
padding: 0;
border-radius: 50%;
transition: background-color 0.3s;
@@ -325,6 +323,10 @@ body.dark-mode .material-icons.folder-icon {
/* ===========================================================
FORMS & LOGIN
=========================================================== */
.remember-me-container {
margin-top: 20px !important;
}
#loginForm {
margin: 0 auto;
max-width: 400px;
@@ -366,21 +368,6 @@ body.dark-mode #loginForm button:hover {
CARDS & MODALS
=========================================================== */
.card {
background-color: #fff;
color: #000;
border: 1px solid #ddd;
max-width: 900px;
width: 100%;
margin: 0 auto;
}
body.dark-mode .card {
background-color: #2c2c2c;
color: #e0e0e0;
border: 1px solid #444;
}
#restoreFilesModal .modal-content {
position: fixed !important;
top: 50% !important;
@@ -521,17 +508,6 @@ body.dark-mode .modal .modal-content {
border-color: #444;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
}
body.dark-mode .editor-header {
background-color: #2c2c2c;
}
.editor-close-btn {
position: absolute;
top: 10px;
@@ -574,12 +550,12 @@ body.dark-mode .editor-close-btn:hover {
/* Editor Modal */
.editor-modal {
position: fixed;
top: 5%;
top: 2%;
left: 5%;
width: 90vw;
height: 90vh;
background-color: #fff;
padding: 20px;
padding: 10px 20px 20px 20px;
border: 1px solid #ccc;
border-radius: 4px !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important;
@@ -616,15 +592,25 @@ body.dark-mode .editor-modal {
}
}
.editor-title {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
font-size: 1.5rem;
max-width: 95%;
display: block;
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 33px;
padding: 0 10px;
margin-bottom: 7px;
}
.editor-title {
margin: 0;
line-height: 33px;
}
body.dark-mode .editor-header {
background-color: #2c2c2c;
}
@media (max-width: 600px) {
.editor-title {
font-size: 1.2rem;
@@ -634,6 +620,7 @@ body.dark-mode .editor-modal {
.editor-controls {
text-align: right;
margin-right: 30px;
margin-bottom: 5px;
}
@@ -690,6 +677,37 @@ body.dark-mode .editor-modal {
/* ===========================================================
UPLOAD PROGRESS STYLES
=========================================================== */
.pause-resume-btn {
background: none;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
outline: none;
margin-right: 5px;
}
.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;
@@ -899,6 +917,7 @@ body.dark-mode #fileList table tr {
word-break: break-word !important;
text-align: left !important;
line-height: 1.2 !important;
vertical-align: middle !important;
padding: 8px 10px !important;
max-width: 250px !important;
min-width: 120px !important;
@@ -978,29 +997,6 @@ label {
display: none;
}
#uploadFolderRow {
margin-bottom: 20px;
}
@media (max-width: 768px) {
#uploadFolderRow .col-md-6 {
margin-bottom: 15px;
}
#uploadFolderRow .col-md-6:last-child {
margin-bottom: 0;
}
}
.card-header {
font-size: 1.2rem;
font-weight: bold;
}
.card-body .form-group {
margin-bottom: 5px !important;
}
#createFolderBtn {
margin-top: 0px !important;
height: 40px !important;
@@ -1055,34 +1051,6 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
background-color: #555;
}
.folder-option:hover {
background-color: #f0f0f0;
padding: 2px 4px;
}
.folder-option.selected {
background-color: #d0d0d0;
border-radius: 4px;
padding: 2px 4px;
}
body.dark-mode .folder-option.selected {
background-color: #444;
color: #fff;
border-radius: 4px;
padding: 2px 4px;
}
body.dark-mode .folder-option:hover {
background-color: #333;
color: #fff;
padding: 2px 4px;
}
.custom-folder-card-body {
padding-top: 5px !important;
}
#customToast {
position: fixed;
top: 20px;
@@ -1129,9 +1097,17 @@ body.dark-mode .folder-option:hover {
}
#fileListContainer {
padding: 10px;
margin-top: 20px;
margin-bottom: 20px;
max-width: 100%;
padding-bottom: 10px !important;
padding-left: 5px !important;
padding-right: 5px !important;
margin: 0 auto 20px;
}
@media (max-width: 750px) {
#fileListContainer {
width: 99%;
}
}
body.dark-mode #fileListContainer {
@@ -1139,9 +1115,6 @@ body.dark-mode #fileListContainer {
color: #e0e0e0;
border: 1px solid #444;
border-radius: 8px;
padding: 10px;
margin-top: 20px;
}
#fileListContainer>h2,
@@ -1160,7 +1133,7 @@ body.dark-mode #fileListContainer {
}
.col-12.col-md-4.text-left {
margin-left: -15px;
margin-left: -17px;
}
@media (max-width: 600px) {
@@ -1171,6 +1144,7 @@ body.dark-mode #fileListContainer {
#fileListTitle {
font-size: 1.8em;
margin-top: 10px;
margin-bottom: 15px;
}
@@ -1231,6 +1205,24 @@ body.dark-mode #fileListContainer {
/* ===========================================================
FOLDER TREE STYLES
=========================================================== */
/* Make breadcrumb links look clickable */
.breadcrumb-link {
cursor: pointer;
color: #007bff;
text-decoration: underline;
}
.breadcrumb-link:hover {
color: #0056b3;
}
.breadcrumb-link.selected {
background-color: #e9ecef;
font-weight: bold;
padding: 2px 4px;
border-radius: 4px;
}
.folder-tree {
list-style-type: none;
padding-left: 10px;
@@ -1263,15 +1255,38 @@ body.dark-mode #fileListContainer {
width: 30px;
}
.folder-option {
cursor: pointer;
}
#folderTreeContainer {
display: block;
}
.folder-option {
cursor: pointer;
}
.folder-option:hover {
background-color: #f0f0f0;
padding: 2px 4px;
}
.folder-option.selected {
background-color: #d0d0d0;
border-radius: 4px;
padding: 2px 4px;
}
body.dark-mode .folder-option.selected {
background-color: #444;
color: #fff;
border-radius: 4px;
padding: 2px 4px;
}
body.dark-mode .folder-option:hover {
background-color: #333;
color: #fff;
padding: 2px 4px;
}
/* ===========================================================
FILE MANAGER INLINE STYLE REMOVAL - New Classes
=========================================================== */
@@ -1280,12 +1295,13 @@ body.dark-mode #fileListContainer {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
text-align: center;
min-height: 30px;
margin: 0 auto 10px;
padding: 10px;
width: 90% !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: 25px;
padding: 5px;
margin-bottom: 10px;
max-width: 90%;
}
.image-preview-modal-content {
@@ -1329,7 +1345,6 @@ body.dark-mode .image-preview-modal-content {
}
.share-btn {
/* Your custom styles here */
border: none;
color: white;
padding: 8px 12px;
@@ -1567,7 +1582,7 @@ body.dark-mode .btn-secondary {
#toggleViewBtn {
margin-bottom: 20px;
margin-left: 15px;
margin-left: 14px;
padding: 10px 20px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
@@ -1579,9 +1594,15 @@ body.dark-mode .btn-secondary {
transition: background 0.3s ease, box-shadow 0.3s ease;
}
@media (max-width: 768px) {
#toggleViewBtn {
margin-left: 0 !important;
}
}
@media (max-width: 600px) {
#toggleViewBtn {
margin-left: auto !important;
margin-left: 0 !important;
margin-right: auto !important;
display: block !important;
}
@@ -1701,7 +1722,7 @@ body.dark-mode .folder-help-summary {
body.dark-mode .folder-help-icon {
color: #f6a72c;
font-size: 20px;
}
body.dark-mode #searchIcon {
@@ -1796,4 +1817,234 @@ 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;
}
.main-wrapper {
display: flex;
flex-direction: row;
}
.drop-target-sidebar {
display: none;
width: 50px;
transition: width 0.3s ease;
background-color: #f8f9fa;
border-right: 2px dashed #1565C0;
padding: 10px;
}
@media (min-width: 769px) {
.drop-target-sidebar {
display: block;
}
}
.drop-target-sidebar.active {
width: 350px;
}
.main-column {
flex: 1;
transition: margin-left 0.3s ease;
}
#uploadFolderRow {
display: flex;
flex-wrap: nowrap;
gap: 1rem;
}
@media (max-width: 768px) {
#uploadFolderRow {
gap: 0px;
}
}
#leftCol,
#rightCol {
display: flex;
justify-content: center;
min-width: 370px;
align-self: flex-start;
}
#leftCol {
flex: 0 1 60%;
}
#rightCol {
flex: 0 1 40%;
}
@media (max-width: 768px) {
.main-wrapper {
flex-direction: column;
}
.drop-target-sidebar {
display: none !important;
}
#uploadFolderRow {
flex-wrap: wrap;
}
#leftCol, #rightCol {
flex: 0 1 100% !important;
}
#rightCol {
margin-bottom: 0;
}
}
#sidebarDropArea.highlight,
#uploadFolderRow.highlight {
border: 2px dashed #1565C0;
background-color: #eef;
}
.drag-header {
cursor: grab;
user-select: none;
position: relative;
}
.drag-header::after {
content: '⋮⋮';
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
color: #1565C0;
pointer-events: none;
}
.dragging {
transform: scale(1.05);
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.3);
transition: transform 0.2s ease, box-shadow 0.2s ease;
z-index: 10000;
}
#uploadCard,
#folderManagementCard {
transition: transform 0.3s ease, opacity 0.3s ease;
width: 100%;
margin-bottom: 20px;
min-height: 353px;
}
#uploadFolderRow.highlight {
min-height: 353px;
margin-bottom: 20px;
}
#sidebarDropArea,
#uploadFolderRow {
background-color: transparent;
}
#sidebarDropArea {
display: none;
}
body.dark-mode #sidebarDropArea,
body.dark-mode #uploadFolderRow {
background-color: transparent;
}
body.dark-mode #sidebarDropArea.highlight,
body.dark-mode #uploadFolderRow.highlight {
background-color: #333;
border: 2px dashed #555;
color: #fff;
}
.drop-target-sidebar.highlight {
margin-top: 10px;
}
.drop-target-sidebar:not(.highlight) {
border: none !important;
}
.dragging:focus {
outline: none;
}
#sidebarDropArea > .card {
margin-bottom: 1rem;
}
.card {
background-color: #fff;
color: #000;
border: 1px solid #ddd;
max-width: 900px;
width: 100%;
margin: 0 auto;
}
body.dark-mode .card {
background-color: #2c2c2c;
color: #e0e0e0;
border: 1px solid #444;
}
.card-header {
font-size: 1.2rem;
font-weight: bold;
}
.custom-folder-card-body {
padding-top: 5px !important;
padding-right: 0 !important;
}
#addUserModal,
#removeUserModal {
z-index: 5000 !important;
}
#customConfirmModal {
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 */
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 {
background: #3a3a3a;
color: #e0e0e0;
border: 1px solid #555;
}
body.dark-mode .admin-panel-content label {
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,12 +35,7 @@ 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() {
console.log("Setting up trash restore/delete listeners.");
// --- Attach listener to the restore button (created in auth.js) to open the modal.
const restoreBtn = document.getElementById("restoreFilesBtn");
@@ -57,7 +45,6 @@ export function setupTrashRestoreDelete() {
loadTrashItems();
});
} else {
console.warn("restoreFilesBtn not found. It may not be available for the current user.");
setTimeout(() => {
const retryBtn = document.getElementById("restoreFilesBtn");
if (retryBtn) {

82
updateConfig.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
require 'config.php';
header('Content-Type: application/json');
// Verify that the user is authenticated and is an admin.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
!isset($_SESSION['isAdmin']) || !$_SESSION['isAdmin']) {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access.']);
exit;
}
// Validate CSRF token.
$receivedToken = '';
if (isset($_SERVER['HTTP_X_CSRF_TOKEN'])) {
$receivedToken = trim($_SERVER['HTTP_X_CSRF_TOKEN']);
} else {
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
}
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
http_response_code(403);
echo json_encode(['error' => 'Invalid CSRF token.']);
exit;
}
// Retrieve and decode JSON input.
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid input.']);
exit;
}
// Validate and sanitize OIDC configuration.
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
$oidcClientId = isset($oidc['clientId']) ? trim($oidc['clientId']) : '';
$oidcClientSecret = isset($oidc['clientSecret']) ? trim($oidc['clientSecret']) : '';
$oidcRedirectUri = isset($oidc['redirectUri']) ? filter_var($oidc['redirectUri'], FILTER_SANITIZE_URL) : '';
if (!$oidcProviderUrl || !$oidcClientId || !$oidcClientSecret || !$oidcRedirectUri) {
http_response_code(400);
echo json_encode(['error' => 'Incomplete OIDC configuration.']);
exit;
}
// Validate login option booleans.
$disableFormLogin = isset($data['disableFormLogin']) ? filter_var($data['disableFormLogin'], FILTER_VALIDATE_BOOLEAN) : false;
$disableBasicAuth = isset($data['disableBasicAuth']) ? filter_var($data['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN) : false;
$disableOIDCLogin = isset($data['disableOIDCLogin']) ? filter_var($data['disableOIDCLogin'], FILTER_VALIDATE_BOOLEAN) : false;
// Prepare configuration array.
$configUpdate = [
'oidc' => [
'providerUrl' => $oidcProviderUrl,
'clientId' => $oidcClientId,
'clientSecret' => $oidcClientSecret,
'redirectUri' => $oidcRedirectUri,
],
'loginOptions' => [
'disableFormLogin' => $disableFormLogin,
'disableBasicAuth' => $disableBasicAuth,
'disableOIDCLogin' => $disableOIDCLogin,
]
];
// Define the configuration file path.
$configFile = USERS_DIR . 'adminConfig.json';
// Convert and encrypt configuration.
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
$encryptedContent = encryptData($plainTextConfig, $encryptionKey);
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
http_response_code(500);
echo json_encode(['error' => 'Failed to update configuration.']);
exit;
}
echo json_encode(['success' => 'Configuration updated successfully.']);
?>

470
upload.js
View File

@@ -2,11 +2,15 @@ import { loadFileList, displayFilePreview, initFileActions } from './fileManager
import { showToast, escapeHTML } from './domUtils.js';
import { loadFolderTree } from './folderManager.js';
// Helper: Recursively traverse a dropped folder.
/* -----------------------------------------------------
Helpers for DragandDrop Folder Uploads (Original Code)
----------------------------------------------------- */
// Recursively traverse a dropped folder.
function traverseFileTreePromise(item, path = "") {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
if (item.isFile) {
item.file(file => {
// Store relative path for folder uploads.
Object.defineProperty(file, 'customRelativePath', {
value: path + file.name,
writable: true,
@@ -29,7 +33,7 @@ function traverseFileTreePromise(item, path = "") {
});
}
// Helper: Given DataTransfer items, recursively retrieve files.
// Recursively retrieve files from DataTransfer items.
function getFilesFromDataTransferItems(items) {
const promises = [];
for (let i = 0; i < items.length; i++) {
@@ -41,25 +45,27 @@ function getFilesFromDataTransferItems(items) {
return Promise.all(promises).then(results => results.flat());
}
// Helper: Set default drop area content.
/* -----------------------------------------------------
UI Helpers (Mostly unchanged from your original code)
----------------------------------------------------- */
function setDropAreaDefault() {
const dropArea = document.getElementById("uploadDropArea");
if (dropArea) {
dropArea.innerHTML = `
<div id="uploadInstruction" class="upload-instruction">
Drop files/folders here or click 'Choose files'
</div>
<div id="uploadFileRow" class="upload-file-row">
<button id="customChooseBtn" type="button">
Choose files
</button>
</div>
<div id="fileInfoWrapper" class="file-info-wrapper">
<div id="fileInfoContainer" class="file-info-container">
<span id="fileInfoDefault">No files selected</span>
</div>
</div>
`;
<div id="uploadInstruction" class="upload-instruction">
Drop files/folders here or click 'Choose files'
</div>
<div id="uploadFileRow" class="upload-file-row">
<button id="customChooseBtn" type="button">Choose files</button>
</div>
<div id="fileInfoWrapper" class="file-info-wrapper">
<div id="fileInfoContainer" class="file-info-container">
<span id="fileInfoDefault">No files selected</span>
</div>
</div>
<!-- File input for file picker (files only) -->
<input type="file" id="file" name="file[]" class="form-control-file" multiple style="opacity:0; position:absolute; width:1px; height:1px;" />
`;
}
}
@@ -82,7 +88,6 @@ function adjustFolderHelpExpansionClosed() {
}
}
// Helper: Update file info container count/preview.
function updateFileInfoCount() {
const fileInfoContainer = document.getElementById("fileInfoContainer");
if (fileInfoContainer && window.selectedFiles) {
@@ -90,64 +95,180 @@ function updateFileInfoCount() {
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
} else if (window.selectedFiles.length === 1) {
fileInfoContainer.innerHTML = `
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;"></div>
<span id="fileNameDisplay" class="file-name-display">${escapeHTML(window.selectedFiles[0].name)}</span>
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;">
<span class="material-icons file-icon">insert_drive_file</span>
</div>
<span id="fileNameDisplay" class="file-name-display">${escapeHTML(window.selectedFiles[0].name || window.selectedFiles[0].fileName || "Unnamed File")}</span>
`;
} else {
fileInfoContainer.innerHTML = `
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;"></div>
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;">
<span class="material-icons file-icon">insert_drive_file</span>
</div>
<span id="fileCountDisplay" class="file-name-display">${window.selectedFiles.length} files selected</span>
`;
}
const previewContainer = document.getElementById("filePreviewContainer");
if (previewContainer && window.selectedFiles.length > 0) {
previewContainer.innerHTML = "";
displayFilePreview(window.selectedFiles[0], previewContainer);
// For image files, try to show a preview (if available from the file object).
displayFilePreview(window.selectedFiles[0].file || window.selectedFiles[0], previewContainer);
}
}
}
// Helper: Create a file entry element with a remove button.
// Helper function to repeatedly call removeChunks.php
function removeChunkFolderRepeatedly(identifier, csrfToken, maxAttempts = 3, interval = 1000) {
let attempt = 0;
const removalInterval = setInterval(() => {
attempt++;
const params = new URLSearchParams();
// Prefix with "resumable_" to match your PHP regex.
params.append('folder', 'resumable_' + identifier);
params.append('csrf_token', csrfToken);
fetch('removeChunks.php', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
})
.then(response => response.json())
.then(data => {
console.log(`Chunk folder removal attempt ${attempt}:`, data);
})
.catch(err => {
console.error(`Error on removal attempt ${attempt}:`, err);
});
if (attempt >= maxAttempts) {
clearInterval(removalInterval);
}
}, interval);
}
/* -----------------------------------------------------
File Entry Creation (with Pause/Resume and Restart)
----------------------------------------------------- */
// Create a file entry element with a remove button and a pause/resume button.
function createFileEntry(file) {
const li = document.createElement("li");
li.classList.add("upload-progress-item");
li.style.display = "flex";
li.dataset.uploadIndex = file.uploadIndex;
// Remove button (always added)
const removeBtn = document.createElement("button");
removeBtn.classList.add("remove-file-btn");
removeBtn.textContent = "×";
removeBtn.addEventListener("click", function (e) {
e.stopPropagation();
const uploadIndex = file.uploadIndex;
window.selectedFiles = window.selectedFiles.filter(f => f.uploadIndex !== uploadIndex);
li.remove();
updateFileInfoCount();
});
// In your remove button event listener, replace the fetch call with:
removeBtn.addEventListener("click", function (e) {
e.stopPropagation();
const uploadIndex = file.uploadIndex;
window.selectedFiles = window.selectedFiles.filter(f => f.uploadIndex !== uploadIndex);
// Cancel the file upload if possible.
if (typeof file.cancel === "function") {
file.cancel();
console.log("Canceled file upload:", file.fileName);
}
// Remove file from the resumable queue.
if (resumableInstance && typeof resumableInstance.removeFile === "function") {
resumableInstance.removeFile(file);
}
// Call our helper repeatedly to remove the chunk folder.
if (file.uniqueIdentifier) {
removeChunkFolderRepeatedly(file.uniqueIdentifier, window.csrfToken, 3, 1000);
}
li.remove();
updateFileInfoCount();
});
li.removeBtn = removeBtn;
li.appendChild(removeBtn);
// Add pause/resume/restart button if the file supports pause/resume.
// Conditionally add the pause/resume button only if file.pause is available
// Pause/Resume button (for resumable filepicker uploads)
if (typeof file.pause === "function") {
const pauseResumeBtn = document.createElement("button");
pauseResumeBtn.setAttribute("type", "button"); // not a submit button
pauseResumeBtn.classList.add("pause-resume-btn");
// Start with pause icon and disable button until upload starts
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
pauseResumeBtn.disabled = true;
pauseResumeBtn.addEventListener("click", function (e) {
e.stopPropagation();
if (file.isError) {
// If the file previously failed, try restarting upload.
if (typeof file.retry === "function") {
file.retry();
file.isError = false;
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
}
} else if (!file.paused) {
// Pause the upload (if possible)
if (typeof file.pause === "function") {
file.pause();
file.paused = true;
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">play_circle_outline</span>';
} else {
}
} else if (file.paused) {
// Resume sequence: first call to resume (or upload() fallback)
if (typeof file.resume === "function") {
file.resume();
} else {
resumableInstance.upload();
}
// After a short delay, pause again then resume
setTimeout(() => {
if (typeof file.pause === "function") {
file.pause();
} else {
resumableInstance.upload();
}
setTimeout(() => {
if (typeof file.resume === "function") {
file.resume();
} else {
resumableInstance.upload();
}
}, 100);
}, 100);
file.paused = false;
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
} else {
console.error("Pause/resume function not available for file", file);
}
});
li.appendChild(pauseResumeBtn);
}
// Preview element
const preview = document.createElement("div");
preview.className = "file-preview";
displayFilePreview(file, preview);
li.appendChild(preview);
// File name display
const nameDiv = document.createElement("div");
nameDiv.classList.add("upload-file-name");
nameDiv.textContent = file.name;
nameDiv.textContent = file.name || file.fileName || "Unnamed File";
li.appendChild(nameDiv);
// Progress bar container
const progDiv = document.createElement("div");
progDiv.classList.add("progress", "upload-progress-div");
progDiv.style.flex = "0 0 250px";
progDiv.style.marginLeft = "5px";
const progBar = document.createElement("div");
progBar.classList.add("progress-bar");
progBar.style.width = "0%";
progBar.innerText = "0%";
progDiv.appendChild(progBar);
li.appendChild(removeBtn);
li.appendChild(preview);
li.appendChild(nameDiv);
li.appendChild(progDiv);
li.progressBar = progBar;
@@ -155,7 +276,11 @@ function createFileEntry(file) {
return li;
}
// Process selected files: Build preview/progress list and store files for later submission.
/* -----------------------------------------------------
Processing Files
- For draganddrop, use original processing (supports folders).
- For file picker, if using Resumable, those files use resumable.
----------------------------------------------------- */
function processFiles(filesInput) {
const fileInfoContainer = document.getElementById("fileInfoContainer");
const files = Array.from(filesInput);
@@ -164,12 +289,16 @@ function processFiles(filesInput) {
if (files.length > 0) {
if (files.length === 1) {
fileInfoContainer.innerHTML = `
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;"></div>
<span id="fileNameDisplay" class="file-name-display">${escapeHTML(files[0].name)}</span>
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;">
<span class="material-icons file-icon">insert_drive_file</span>
</div>
<span id="fileNameDisplay" class="file-name-display">${escapeHTML(files[0].name || files[0].fileName || "Unnamed File")}</span>
`;
} else {
fileInfoContainer.innerHTML = `
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;"></div>
<div id="filePreviewContainer" class="file-preview-container" style="display:inline-block;">
<span class="material-icons file-icon">insert_drive_file</span>
</div>
<span id="fileCountDisplay" class="file-name-display">${files.length} files selected</span>
`;
}
@@ -195,12 +324,14 @@ function processFiles(filesInput) {
const list = document.createElement("ul");
list.classList.add("upload-progress-list");
// Check for relative paths (for folder uploads).
const hasRelativePaths = files.some(file => {
const rel = file.webkitRelativePath || file.customRelativePath || "";
return rel.trim() !== "";
});
if (hasRelativePaths) {
// Group files by folder.
const fileGroups = {};
files.forEach(file => {
let folderName = "Root";
@@ -218,11 +349,13 @@ function processFiles(filesInput) {
});
Object.keys(fileGroups).forEach(folderName => {
const folderLi = document.createElement("li");
folderLi.classList.add("upload-folder-group");
folderLi.innerHTML = `<i class="material-icons folder-icon" style="vertical-align:middle; margin-right:8px;">folder</i> ${folderName}:`;
list.appendChild(folderLi);
// Only show folder grouping if folderName is not "Root"
if (folderName !== "Root") {
const folderLi = document.createElement("li");
folderLi.classList.add("upload-folder-group");
folderLi.innerHTML = `<i class="material-icons folder-icon" style="vertical-align:middle; margin-right:8px;">folder</i> ${folderName}:`;
list.appendChild(folderLi);
}
const nestedUl = document.createElement("ul");
nestedUl.classList.add("upload-folder-group-list");
fileGroups[folderName]
@@ -234,6 +367,7 @@ function processFiles(filesInput) {
list.appendChild(nestedUl);
});
} else {
// No relative paths list files directly.
files.forEach((file, index) => {
const li = createFileEntry(file);
li.style.display = (index < maxDisplay) ? "flex" : "none";
@@ -263,7 +397,159 @@ function processFiles(filesInput) {
updateFileInfoCount();
}
// Function to handle file uploads; triggered when the user clicks the "Upload" button.
/* -----------------------------------------------------
Resumable.js Integration for File Picker Uploads
(Only files chosen via file input use Resumable; folder uploads use original code.)
----------------------------------------------------- */
const useResumable = true; // Enable resumable for file picker uploads
let resumableInstance;
function initResumableUpload() {
resumableInstance = new Resumable({
target: "upload.php",
query: { folder: window.currentFolder || "root", upload_token: window.csrfToken },
chunkSize: 3 * 1024 * 1024, // 3 MB chunks
simultaneousUploads: 3,
testChunks: false,
throttleProgressCallbacks: 1,
headers: { "X-CSRF-Token": window.csrfToken }
});
const fileInput = document.getElementById("file");
if (fileInput) {
// Assign Resumable to file input for file picker uploads.
resumableInstance.assignBrowse(fileInput);
fileInput.addEventListener("change", function () {
for (let i = 0; i < fileInput.files.length; i++) {
resumableInstance.addFile(fileInput.files[i]);
}
});
}
resumableInstance.on("fileAdded", function (file) {
// Initialize custom paused flag
file.paused = false;
file.uploadIndex = file.uniqueIdentifier;
if (!window.selectedFiles) {
window.selectedFiles = [];
}
window.selectedFiles.push(file);
const progressContainer = document.getElementById("uploadProgressContainer");
// Check if a wrapper already exists; if not, create one with a UL inside.
let listWrapper = progressContainer.querySelector(".upload-progress-wrapper");
let list;
if (!listWrapper) {
listWrapper = document.createElement("div");
listWrapper.classList.add("upload-progress-wrapper");
listWrapper.style.maxHeight = "300px";
listWrapper.style.overflowY = "auto";
list = document.createElement("ul");
list.classList.add("upload-progress-list");
listWrapper.appendChild(list);
progressContainer.appendChild(listWrapper);
} else {
list = listWrapper.querySelector("ul.upload-progress-list");
}
const li = createFileEntry(file);
li.dataset.uploadIndex = file.uniqueIdentifier;
list.appendChild(li);
updateFileInfoCount();
});
resumableInstance.on("fileProgress", function (file) {
const percent = Math.floor(file.progress() * 100);
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
if (li && li.progressBar) {
li.progressBar.style.width = percent + "%";
// Calculate elapsed time since file entry was created.
const elapsed = (Date.now() - li.startTime) / 1000;
let speed = "";
if (elapsed > 0) {
// Calculate total bytes uploaded so far using file.progress() * file.size
const bytesUploaded = file.progress() * file.size;
const spd = bytesUploaded / elapsed;
if (spd < 1024) {
speed = spd.toFixed(0) + " B/s";
} else if (spd < 1048576) {
speed = (spd / 1024).toFixed(1) + " KB/s";
} else {
speed = (spd / 1048576).toFixed(1) + " MB/s";
}
}
li.progressBar.innerText = percent + "% (" + speed + ")";
// Enable the pause/resume button once progress starts
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
if (pauseResumeBtn) {
pauseResumeBtn.disabled = false;
}
}
});
resumableInstance.on("fileSuccess", function (file, message) {
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
if (li && li.progressBar) {
li.progressBar.style.width = "100%";
li.progressBar.innerText = "Done";
// Hide the pause/resume button when upload is complete.
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
if (pauseResumeBtn) {
pauseResumeBtn.style.display = "none";
}
const removeBtn = li.querySelector(".remove-file-btn");
if (removeBtn) {
removeBtn.style.display = "none";
}
}
loadFileList(window.currentFolder);
});
resumableInstance.on("fileError", function (file, message) {
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
if (li && li.progressBar) {
li.progressBar.innerText = "Error";
}
// Mark file as errored so that the pause/resume button acts as a restart button.
file.isError = true;
// Change the pause/resume button to show a restart icon.
const pauseResumeBtn = li ? li.querySelector(".pause-resume-btn") : null;
if (pauseResumeBtn) {
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">replay</span>';
pauseResumeBtn.disabled = false;
}
showToast("Error uploading file: " + file.fileName);
});
resumableInstance.on("complete", function () {
// Check if any file in the current selection is marked with an error.
const hasError = window.selectedFiles.some(f => f.isError);
if (!hasError) {
// All files succeeded; clear the file list after 5 seconds.
setTimeout(() => {
if (fileInput) fileInput.value = "";
const progressContainer = document.getElementById("uploadProgressContainer");
progressContainer.innerHTML = "";
window.selectedFiles = [];
adjustFolderHelpExpansionClosed();
window.addEventListener("resize", adjustFolderHelpExpansionClosed);
const fileInfoContainer = document.getElementById("fileInfoContainer");
if (fileInfoContainer) {
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
}
const dropArea = document.getElementById("uploadDropArea");
if (dropArea) setDropAreaDefault();
}, 5000);
} else {
showToast("Some files failed to upload. Please check the list.");
}
});
}
/* -----------------------------------------------------
XHR-based submitFiles for DragandDrop (Folder) Uploads
----------------------------------------------------- */
function submitFiles(allFiles) {
const folderToUse = window.currentFolder || "root";
const progressContainer = document.getElementById("uploadProgressContainer");
@@ -323,9 +609,7 @@ function submitFiles(allFiles) {
if (li) {
li.progressBar.style.width = "100%";
li.progressBar.innerText = "Done";
if (li.removeBtn) {
li.removeBtn.style.display = "none";
}
if (li.removeBtn) li.removeBtn.style.display = "none";
}
uploadResults[file.uploadIndex] = true;
} else {
@@ -367,7 +651,6 @@ function submitFiles(allFiles) {
});
xhr.open("POST", "upload.php", true);
// Set the CSRF token header to match the folderManager approach.
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
xhr.send(formData);
});
@@ -377,35 +660,40 @@ function submitFiles(allFiles) {
.then(serverFiles => {
initFileActions();
serverFiles = (serverFiles || []).map(item => item.name.trim().toLowerCase());
let allSucceeded = true;
allFiles.forEach(file => {
if ((file.webkitRelativePath || file.customRelativePath || "").trim() !== "") {
return;
}
const clientFileName = file.name.trim().toLowerCase();
if (!uploadResults[file.uploadIndex] || !serverFiles.includes(clientFileName)) {
const li = progressElements[file.uploadIndex];
if (li) {
li.progressBar.innerText = "Error";
// For files without a relative path
if ((file.webkitRelativePath || file.customRelativePath || "").trim() === "") {
const clientFileName = file.name.trim().toLowerCase();
if (!uploadResults[file.uploadIndex] || !serverFiles.includes(clientFileName)) {
const li = progressElements[file.uploadIndex];
if (li) {
li.progressBar.innerText = "Error";
}
allSucceeded = false;
}
allSucceeded = false;
}
});
setTimeout(() => {
if (fileInput) fileInput.value = "";
const removeBtns = progressContainer.querySelectorAll("button.remove-file-btn");
removeBtns.forEach(btn => btn.style.display = "none");
progressContainer.innerHTML = "";
window.selectedFiles = [];
adjustFolderHelpExpansionClosed();
window.addEventListener("resize", adjustFolderHelpExpansionClosed);
const fileInfoContainer = document.getElementById("fileInfoContainer");
if (fileInfoContainer) {
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
}
const dropArea = document.getElementById("uploadDropArea");
if (dropArea) setDropAreaDefault();
}, 5000);
if (!allSucceeded) {
if (allSucceeded) {
// All files succeeded—clear the list after 5 seconds.
setTimeout(() => {
if (fileInput) fileInput.value = "";
const removeBtns = progressContainer.querySelectorAll("button.remove-file-btn");
removeBtns.forEach(btn => btn.style.display = "none");
progressContainer.innerHTML = "";
window.selectedFiles = [];
adjustFolderHelpExpansionClosed();
window.addEventListener("resize", adjustFolderHelpExpansionClosed);
const fileInfoContainer = document.getElementById("fileInfoContainer");
if (fileInfoContainer) {
fileInfoContainer.innerHTML = `<span id="fileInfoDefault">No files selected</span>`;
}
const dropArea = document.getElementById("uploadDropArea");
if (dropArea) setDropAreaDefault();
}, 5000);
} else {
// Some files failed—keep the list visible and show a toast.
showToast("Some files failed to upload. Please check the list.");
}
})
@@ -419,12 +707,15 @@ function submitFiles(allFiles) {
}
}
// Main initUpload: sets up file input, drop area, and form submission.
/* -----------------------------------------------------
Main initUpload: Sets up file input, drop area, and form submission.
----------------------------------------------------- */
function initUpload() {
const fileInput = document.getElementById("file");
const dropArea = document.getElementById("uploadDropArea");
const uploadForm = document.getElementById("uploadFileForm");
// For file picker, remove directory attributes so only files can be chosen.
if (fileInput) {
fileInput.removeAttribute("webkitdirectory");
fileInput.removeAttribute("mozdirectory");
@@ -434,6 +725,7 @@ function initUpload() {
setDropAreaDefault();
// Draganddrop events (for folder uploads) use original processing.
if (dropArea) {
dropArea.classList.add("upload-drop-area");
dropArea.addEventListener("dragover", function (e) {
@@ -458,6 +750,7 @@ function initUpload() {
processFiles(dt.files);
}
});
// Clicking drop area triggers file input.
dropArea.addEventListener("click", function () {
if (fileInput) fileInput.click();
});
@@ -465,7 +758,14 @@ function initUpload() {
if (fileInput) {
fileInput.addEventListener("change", function () {
processFiles(fileInput.files);
if (useResumable) {
// For file picker, if resumable is enabled, let it handle the files.
for (let i = 0; i < fileInput.files.length; i++) {
resumableInstance.addFile(fileInput.files[i]);
}
} else {
processFiles(fileInput.files);
}
});
}
@@ -477,9 +777,21 @@ function initUpload() {
showToast("No files selected.");
return;
}
submitFiles(files);
// If files come from file picker (no relative path), use Resumable.
if (useResumable && (!files[0].customRelativePath || files[0].customRelativePath === "")) {
// Ensure current folder is updated.
resumableInstance.opts.query.folder = window.currentFolder || "root";
resumableInstance.upload();
showToast("Resumable upload started...");
} else {
submitFiles(files);
}
});
}
if (useResumable) {
initResumableUpload();
}
}
export { initUpload };

View File

@@ -12,122 +12,225 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
exit;
}
// Ensure user is authenticated
// Ensure user is authenticated.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401);
exit;
}
// Validate folder name input.
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
exit;
}
// Determine the base upload directory.
$baseUploadDir = UPLOAD_DIR;
if ($folder !== 'root') {
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
if (!is_dir($baseUploadDir)) {
mkdir($baseUploadDir, 0775, true);
// Check if this is a chunked upload (Resumable.js sends "resumableChunkNumber").
if (isset($_POST['resumableChunkNumber'])) {
// ------------- Chunked Upload Handling -------------
$chunkNumber = intval($_POST['resumableChunkNumber']); // current chunk (1-indexed)
$totalChunks = intval($_POST['resumableTotalChunks']);
$chunkSize = intval($_POST['resumableChunkSize']);
$totalSize = intval($_POST['resumableTotalSize']);
$resumableIdentifier = $_POST['resumableIdentifier']; // unique file identifier
$resumableFilename = $_POST['resumableFilename'];
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
exit;
}
} else {
if (!is_dir($baseUploadDir)) {
mkdir($baseUploadDir, 0775, true);
// Determine the base upload directory.
$baseUploadDir = UPLOAD_DIR;
if ($folder !== 'root') {
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
if (!is_dir($baseUploadDir)) {
mkdir($baseUploadDir, 0775, true);
}
} else {
if (!is_dir($baseUploadDir)) {
mkdir($baseUploadDir, 0775, true);
}
}
}
// Prepare a collection to hold metadata for each folder.
$metadataCollection = []; // key: folder path, value: metadata array
$metadataChanged = []; // key: folder path, value: boolean
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
foreach ($_FILES["file"]["name"] as $index => $fileName) {
$safeFileName = basename($fileName);
if (!preg_match($safeFileNamePattern, $safeFileName)) {
echo json_encode(["error" => "Invalid file name: " . $fileName]);
// Use a temporary directory for the chunks.
$tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR;
if (!is_dir($tempDir)) {
mkdir($tempDir, 0775, true);
}
// Save the current chunk.
$chunkFile = $tempDir . $chunkNumber; // store chunk using its number as filename
if (!move_uploaded_file($_FILES["file"]["tmp_name"], $chunkFile)) {
echo json_encode(["error" => "Failed to move uploaded chunk"]);
exit;
}
// --- Minimal Folder/Subfolder Logic ---
$relativePath = '';
if (isset($_POST['relativePath'])) {
if (is_array($_POST['relativePath'])) {
$relativePath = $_POST['relativePath'][$index] ?? '';
} else {
$relativePath = $_POST['relativePath'];
// Check if all chunks have been uploaded.
$uploadedChunks = glob($tempDir . "*");
if (count($uploadedChunks) >= $totalChunks) {
// All chunks uploaded. Merge chunks.
$targetPath = $baseUploadDir . $resumableFilename;
if (!$out = fopen($targetPath, "wb")) {
echo json_encode(["error" => "Failed to open target file for writing"]);
exit;
}
}
// Determine the complete folder path for upload and for metadata.
$folderPath = $folder; // Base folder as provided ("root" or a subfolder)
$uploadDir = $baseUploadDir; // Start with the base upload directory
if (!empty($relativePath)) {
$subDir = dirname($relativePath);
if ($subDir !== '.' && $subDir !== '') {
// If base folder is 'root', then folderPath is just the subDir
// Otherwise, append the subdirectory to the base folder
$folderPath = ($folder === 'root') ? $subDir : $folder . "/" . $subDir;
// Update the upload directory accordingly.
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
. str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR;
// Concatenate each chunk in order.
for ($i = 1; $i <= $totalChunks; $i++) {
$chunkPath = $tempDir . $i;
if (!$in = fopen($chunkPath, "rb")) {
fclose($out);
echo json_encode(["error" => "Failed to open chunk $i"]);
exit;
}
while ($buff = fread($in, 4096)) {
fwrite($out, $buff);
}
fclose($in);
}
// Ensure the file name is taken from the relative path.
$safeFileName = basename($relativePath);
}
// --- End Minimal Folder/Subfolder Logic ---
// Make sure the final upload directory exists.
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$targetPath = $uploadDir . $safeFileName;
if (move_uploaded_file($_FILES["file"]["tmp_name"][$index], $targetPath)) {
// Generate a unique metadata file name based on the folder path.
// Replace slashes, backslashes, and spaces with dashes.
fclose($out);
// --- Metadata Update for Chunked Upload ---
// For chunked uploads, assume no relativePath; so folderPath is simply $folder.
$folderPath = $folder;
$metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath;
// Generate a metadata file name based on the folder path.
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
$metadataFile = META_DIR . $metadataFileName;
// Load metadata for this folder if not already loaded.
if (!isset($metadataCollection[$metadataKey])) {
if (file_exists($metadataFile)) {
$metadataCollection[$metadataKey] = json_decode(file_get_contents($metadataFile), true);
} else {
$metadataCollection[$metadataKey] = [];
$uploadedDate = date(DATE_TIME_FORMAT);
$uploader = $_SESSION['username'] ?? "Unknown";
// Load existing metadata, if any.
if (file_exists($metadataFile)) {
$metadataCollection = json_decode(file_get_contents($metadataFile), true);
if (!is_array($metadataCollection)) {
$metadataCollection = [];
}
$metadataChanged[$metadataKey] = false;
} else {
$metadataCollection = [];
}
// Add metadata for this file if not already present.
if (!isset($metadataCollection[$metadataKey][$safeFileName])) {
$uploadedDate = date(DATE_TIME_FORMAT);
$uploader = $_SESSION['username'] ?? "Unknown";
$metadataCollection[$metadataKey][$safeFileName] = [
if (!isset($metadataCollection[$resumableFilename])) {
$metadataCollection[$resumableFilename] = [
"uploaded" => $uploadedDate,
"uploader" => $uploader
];
$metadataChanged[$metadataKey] = true;
file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT));
}
// --- End Metadata Update ---
// Cleanup: remove the temporary directory and its chunks.
array_map('unlink', glob("$tempDir*"));
rmdir($tempDir);
echo json_encode(["success" => "File uploaded successfully"]);
exit;
} else {
echo json_encode(["error" => "Error uploading file"]);
// Chunk successfully uploaded, but more chunks remain.
echo json_encode(["status" => "chunk uploaded"]);
exit;
}
}
// After processing all files, write out metadata files for folders that changed.
foreach ($metadataCollection as $folderKey => $data) {
if ($metadataChanged[$folderKey]) {
$metadataFileName = str_replace(['/', '\\', ' '], '-', $folderKey) . '_metadata.json';
$metadataFile = META_DIR . $metadataFileName;
file_put_contents($metadataFile, json_encode($data, JSON_PRETTY_PRINT));
} else {
// ------------- Full Upload (Non-chunked) -------------
// Validate folder name input.
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
exit;
}
// Determine the base upload directory.
$baseUploadDir = UPLOAD_DIR;
if ($folder !== 'root') {
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
if (!is_dir($baseUploadDir)) {
mkdir($baseUploadDir, 0775, true);
}
} else {
if (!is_dir($baseUploadDir)) {
mkdir($baseUploadDir, 0775, true);
}
}
// Prepare a collection to hold metadata for each folder.
$metadataCollection = []; // key: folder path, value: metadata array
$metadataChanged = []; // key: folder path, value: boolean
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
foreach ($_FILES["file"]["name"] as $index => $fileName) {
$safeFileName = basename($fileName);
if (!preg_match($safeFileNamePattern, $safeFileName)) {
echo json_encode(["error" => "Invalid file name: " . $fileName]);
exit;
}
// --- Minimal Folder/Subfolder Logic ---
$relativePath = '';
if (isset($_POST['relativePath'])) {
if (is_array($_POST['relativePath'])) {
$relativePath = $_POST['relativePath'][$index] ?? '';
} else {
$relativePath = $_POST['relativePath'];
}
}
// Determine the complete folder path for upload and for metadata.
$folderPath = $folder; // Base folder as provided ("root" or a subfolder)
$uploadDir = $baseUploadDir; // Start with the base upload directory
if (!empty($relativePath)) {
$subDir = dirname($relativePath);
if ($subDir !== '.' && $subDir !== '') {
$folderPath = ($folder === 'root') ? $subDir : $folder . "/" . $subDir;
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
. str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR;
}
$safeFileName = basename($relativePath);
}
// --- End Minimal Folder/Subfolder Logic ---
// Make sure the final upload directory exists.
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$targetPath = $uploadDir . $safeFileName;
if (move_uploaded_file($_FILES["file"]["tmp_name"][$index], $targetPath)) {
// Generate a unique metadata file name based on the folder path.
$metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath;
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
$metadataFile = META_DIR . $metadataFileName;
if (!isset($metadataCollection[$metadataKey])) {
if (file_exists($metadataFile)) {
$metadataCollection[$metadataKey] = json_decode(file_get_contents($metadataFile), true);
} else {
$metadataCollection[$metadataKey] = [];
}
$metadataChanged[$metadataKey] = false;
}
if (!isset($metadataCollection[$metadataKey][$safeFileName])) {
$uploadedDate = date(DATE_TIME_FORMAT);
$uploader = $_SESSION['username'] ?? "Unknown";
$metadataCollection[$metadataKey][$safeFileName] = [
"uploaded" => $uploadedDate,
"uploader" => $uploader
];
$metadataChanged[$metadataKey] = true;
}
} else {
echo json_encode(["error" => "Error uploading file"]);
exit;
}
}
// After processing all files, write out metadata files for folders that changed.
foreach ($metadataCollection as $folderKey => $data) {
if ($metadataChanged[$folderKey]) {
$metadataFileName = str_replace(['/', '\\', ' '], '-', $folderKey) . '_metadata.json';
$metadataFile = META_DIR . $metadataFileName;
file_put_contents($metadataFile, json_encode($data, JSON_PRETTY_PRINT));
}
}
echo json_encode(["success" => "Files uploaded successfully"]);
}
echo json_encode(["success" => "Files uploaded successfully"]);
?>

View File

@@ -1 +1,7 @@
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
<IfModule mod_php.c>
php_flag engine off
</IfModule>
Options -Indexes