Compare commits

...

35 Commits

Author SHA1 Message Date
Ryan
a9c4200827 Extend i18n support: Add new translation keys for Download and Share modals 2025-04-13 18:38:21 -04:00
Ryan
97559873dc Added translations and data attributes for almost all user-facing text 2025-04-13 15:22:52 -04:00
Ryan
0683b27534 New Admin section Header Settings to change Header Title. 2025-04-13 05:51:19 -04:00
Ryan
49c42e8096 RecoveryCodeModal i18n 2025-04-13 04:17:32 -04:00
Ryan
ed39e112a9 more i18n.js keys added 2025-04-13 03:55:51 -04:00
Ryan
25edab923a Decreased header size some more and clickable logo 2025-04-13 02:52:14 -04:00
Ryan
b8ae3c4402 Advanced/Basic search button as material icon on same row as search bar. 2025-04-12 16:31:05 -04:00
Ryan
fb537b1d61 Change search box text when enabling advanced search. 2025-04-12 15:36:23 -04:00
Ryan
90439022e3 Gallery View Toggle button moved to header 2025-04-12 15:16:59 -04:00
Ryan
b4c8738b8a advance search readme added 2025-04-12 14:18:14 -04:00
Ryan
e193bf9b13 Advanced Search Implementation 2025-04-12 14:16:52 -04:00
Ryan
a70d8fc2c7 v1.1.2 2025-04-12 12:52:15 -04:00
Ryan
d9f69d7917 Fuse.js Integration for Indexed Real-Time Searching & Dependencies added 2025-04-12 12:46:28 -04:00
Ryan
28ac23c2f6 Fix totp_setup.php to use header-based CSRF token verification 2025-04-11 23:40:27 -04:00
Ryan
b06c49f213 ensure consistent session behavior 2025-04-11 22:36:43 -04:00
Ryan
8553efabc1 Upgrade dependencies: update robthree/twofactorauth to v3 and endroid/qr-code to v5; update TOTP integration (namespace, enum, QR provider) accordingly 2025-04-11 18:41:44 -04:00
Ryan
81a08ffd5b fix missing 2025-04-11 10:55:33 -04:00
Ryan
296dae96a5 regex configuration constants 2025-04-11 10:44:26 -04:00
Ryan
337f529afd fix drag-drop, UI glitches, & update validation 2025-04-11 03:21:09 -04:00
Ryan
4360f2830a new filetag endpoint and config.php update 2025-04-10 10:33:13 -04:00
Ryan
894cc938a5 i18n-title for folder buttons 2025-04-10 04:27:40 -04:00
Ryan
01801ba950 fix gallery view filename wrapping 2025-04-10 03:58:26 -04:00
Ryan
5b592575a4 adjust card mouseover start position 2025-04-10 03:18:08 -04:00
Ryan
7cce03d092 Use create folder material icon and increase version 2025-04-10 02:59:04 -04:00
Ryan
ff92a6d26c Reduce header height & create folder material icon 2025-04-10 02:29:48 -04:00
Ryan
4fa5faa2bf Shift Key Multi‑Selection & Total Files and File Size 2025-04-10 00:45:35 -04:00
Ryan
98850a7c65 Update 2025-04-09 02:20:16 -04:00
Ryan
15bac15c33 update readme 2025-04-09 01:48:56 -04:00
Ryan
b2ff3efb3b Folder sharing added 2025-04-09 01:46:07 -04:00
Ryan
b9ce3f92a4 Progress modal for handleExtractZip 2025-04-08 21:04:44 -04:00
Ryan
f65b151bc3 Progress Modal on download buttons 2025-04-08 20:30:17 -04:00
Ryan
703c93db25 semi-complete internationalization 2025-04-08 18:49:30 -04:00
Ryan
d0353b137b German translation added 2025-04-08 18:48:22 -04:00
Ryan
a6c4c1d39c Start i18n Integration 2025-04-08 18:40:01 -04:00
Ryan
7aa4fe142a readme update for user permissions 2025-04-08 02:08:10 -04:00
63 changed files with 3599 additions and 704 deletions

View File

@@ -1,5 +1,171 @@
# Changelog
## Changes 4/13/2025 v1.1.3
- Decreased header height some more and clickable logo.
- authModals.js fully updated with i18n.js keys.
- main.js added Dark & Light mode i18n.js keys.
- New Admin section Header Settings to change Header Title.
- Admin Panel confirm unsaved changes.
- Added translations and data attributes for almost all user-facing text
- Extend i18n support: Add new translation keys for Download and Share modals
---
## Changes 4/12/2025
- Moved Gallery view toggle button into header.
- Removed css entries that are not needed anymore for Gallery View Toggle.
- Change search box text when enabling advanced search.
- Advanced/Basic search button as material icon on same row as search bar.
### Advanced Search Implementation
- **Advanced Search Toggle:**
- Added a global toggle (`window.advancedSearchEnabled`) and a UI button to switch between basic and advanced search modes.
- The toggle button label changes between "Advanced Search" and "Basic Search" to reflect the active mode.
- **Fuse.js Integration Updates:**
- Modified the `searchFiles()` function to conditionally include the `"content"` key in the Fuse.js keys only when advanced search mode is enabled.
- Adjusted Fuse.js options by adding `ignoreLocation: true`, adjusting the `threshold`, and optionally assigning weights (e.g., a lower weight for `name` and a higher weight for `content`) to prioritize matches in file content.
- **Backend (PHP) Enhancements:**
- Updated **getFileList.php** to read the content of text-based files (e.g., `.txt`, `.html`, `.md`, etc.) using `file_get_contents()`.
- Added a `"content"` property to the JSON response for eligible files to allow for full-text search in advanced mode.
### Fuse.js Integration for Indexed Real-Time Searching**
- **Added Fuse.js Library:** Included Fuse.js via a CDN `<script>` tag to leverage its clientside fuzzy search capabilities.
- **Created searchFiles Helper Function:** Introduced a new function that uses Fuse.js to build an index and perform fuzzy searches over file properties (file name, uploader, and nested tag names).
- **Transformed JSON Object to Array:** Updated the loadFileList() function to convert the returned file data into an array (if it isnt already) and assign file names from JSON keys.
- **Updated Rendering Functions:** Modified both renderFileTable() and renderGalleryView() to use the searchFiles() helper instead of a simple inarray .filter(). This ensures that every search—realtime by user input—is powered by Fuse.jss indexed search.
- **Enhanced Search Configuration:** Configured Fuse.js to search across multiple keys (file name, uploader, and tags) so that users can find files based on any of these properties.
---
## Changes 4/11/2025
- Fixed fileDragDrop issue from previous update.
- Fixed User Panel height changing unexpectedly on mouse over.
- Improved JS file comments for better documentation.
- Fixed userPermissions not updating after initial setting.
- Disabled folder and file sharing for readOnly users.
- Moved change password close button to the top right of the modal.
- Updated upload regex pattern to be Unicodeenabled and added additional security measures. [(#19)](https://github.com/error311/FileRise/issues/19)
- Updated filename, folder, and username regex acceptance patterns.
- Updated robthree/twofactorauth to v3 and endroid/qr-code to v5
- Updated TOTP integration (namespace, enum, QR provider) accordingly
- Updated docker image from 22.04 to 24.04 <https://github.com/error311/filerise-docker>
- Ensure consistent session behavior
- Fix totp_setup.php to use header-based CSRF token verification
---
## Shift Key MultiSelection Changes 4/10/2025 v1.1.1
- **Implemented Range Selection:**
- Modified the `toggleRowSelection` function so that when the Shift key is held down, all rows between the last clicked (anchor) row (stored as `window.lastSelectedFileRow`) and the currently clicked row are selected.
- **Modifier Handling:**
- Regular clicks (or Ctrl/Cmd clicks) simply toggle the clicked row without clearing other selections.
- **Prevented Default Browser Behavior:**
- Added `event.preventDefault()` in the Shiftclick branch to avoid unwanted text selection.
- **Maintaining the Anchor:**
- The last clicked row is stored for future range selections.
## Total Files and File Size Summary
- **Size Calculation:**
- Created `parseSizeToBytes(sizeStr)` to convert file size strings (e.g. `"456.9KB"`, `"1.2 MB"`) into a numerical byte value.
- Created `formatSize(totalBytes)` to format a byte value into a humanreadable string (choosing between Bytes, KB, MB, or GB).
- Created `buildFolderSummary(filteredFiles)` to:
- Sum the sizes of all files (using `parseSizeToBytes`).
- Count the total number of files.
- **Dynamic Display in `loadFileList`:**
- Updated `loadFileList` to update a summary element (with `id="fileSummary"`) inside the `#fileListActions` container when files are present.
- When no files are found, the summary element is hidden (setting its `display` to `"none"` or clearing the container).
- **Responsive Styling:**
- Added CSS media queries to the `#fileSummary` element so that on small screens it is centered and any extra side margins are removed. Dark and light mode supported.
- **Other changes**
- `shareFolder.php` updated to display format size.
- Fix to prevent the filename text from overflowing its container in the gallery view.
- Reduced header height.
- Create Folder changed to Material Icon `create_new_folder`
---
## Folder Sharing Feature - Changelog 4/9/2025 v1.1.0
### New Endpoints
- **createFolderShareLink.php:**
- Generates secure, expiring share tokens for folders (with an optional password and allow-upload flag).
- Stores folder share records separately from file shares in `share_folder_links.json`.
- Builds share links that point to **shareFolder.php**, using a proper BASE_URL or the servers IP when a default placeholder is detected.
- **shareFolder.php:**
- Serves shared folders via GET requests by reading tokens from `share_folder_links.json`.
- Validates token expiration and password (if set).
- Displays folder contents with pagination (10 items per page) and shows file sizes in megabytes.
- Provides navigation links (Prev, Next, and numbered pages) for folder listings.
- Includes an upload form (if allowed) that redirects back to the same share page after upload.
- **downloadSharedFile.php:**
- A dedicated, secure download endpoint for shared files.
- Validates the share token and ensures the requested file is inside the shared folder.
- Serves files using proper MIME types and Content-Disposition headers (inline for images, attachment for others).
- **uploadToSharedFolder.php:**
- Handles file uploads for public folder shares.
- Enforces file size limits and file type whitelists.
- Generates unique filenames (with a unique prefix) to prevent collisions.
- Updates metadata for the uploaded file (upload date and sets uploader as "Outside Share").
- Redirects back to **shareFolder.php** after a successful upload so the file listing refreshes.
### New Front-End Module
- **folderShareModal.js:**
- Provides a modal interface for users to generate folder share links.
- Includes expiration selection, optional password entry, and an allow-upload checkbox.
- Uses the **createFolderShareLink.php** endpoint to generate share links.
- Displays the generated share link with a “copy to clipboard” button.
---
## Changes 4/8/2025
**May have missed some stuff or could have bugs. Please report any issue you may encounter.**
- **i18n Integration:**
- Implemented a semi-complete internationalization (i18n) system for all user-facing texts in FileRise.
- Created an `i18n.js` module containing a translations object with full keys for English (en), Spanish (es), and French (fr).
- Updated JavaScript code to replace hard-coded strings with the `t()` translation function.
- Enhanced HTML and modal templates to support dynamic language translations using data attributes (data-i18n-key, data-i18n-placeholder, etc.).
- **Language Dropdown & Persistence:**
- Added a language dropdown to the user panel modal allowing users to select their preferred language.
- Persisted the selected language in localStorage, ensuring that the preferred language is automatically applied on page refresh.
- Updated main.js to load and set the users language preference on DOMContentLoaded by calling `setLocale()` and `applyTranslations()`.
- **Bug Fixes & Improvements:**
- Fixed issues with evaluation of translation function calls in template literals (ensured proper syntax with `${t("key")}`).
- Updated the t() function to be more defensive against missing keys.
- Provided instructions and code examples to ensure the language change settings are reliably saved and applied across sessions.
- **ZIP Download Flow**
- Progress Modal: In the ZIP download handler (confirmDownloadZip), added code to show a progress modal (with a spinning icon) as soon as the user confirms the download and before the request to create the ZIP begins. Once the blob is received or an error occurs, we hide the progress modal.
- Inline Handlers and Global Exposure: Ensured that functions like confirmDownloadZip are attached to the global window object (or called via appropriate inline handlers) so that the inline onclick events in the HTML work without reference errors.
- **Single File Download Flow**
- Modal Popup for Single File: Replaced the direct download link for single files with a modal-driven flow. When the download button is clicked, the openDownloadModal(fileName, folder) function is called. This stores the file details and shows a modal where the user can confirm (or edit) the file name.
- Confirm Download Function: When the user clicks the Download button in the modal, the confirmSingleDownload() function is called. This function constructs a URL for download.php (using GET parameters for folder and file), fetches the file as a blob, and triggers a download using a temporary anchor element. A progress modal is also used here to give feedback during the download process.
- **Zip Extraction**
- Reused Zip Download modal to use same progress Modal Popup with Extracting files.... text.
---
## Changes 4/7/2025 v1.0.9
- TOTP one time recovery code added

View File

@@ -11,6 +11,7 @@ Thank you for your interest in contributing to FileRise! We appreciate your help
- [Coding Guidelines](#coding-guidelines)
- [Documentation](#documentation)
- [Questions and Support](#questions-and-support)
- [Adding New Language Translations](#adding-new-language-translations)
## Getting Started
@@ -25,7 +26,7 @@ Thank you for your interest in contributing to FileRise! We appreciate your help
```
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.
FileRise runs on a standard LAMP stack. Ensure you have PHP, Apache, and the necessary dependencies installed.
4. **Configuration**
Copy any example configuration files (if provided) and adjust them as needed for your local setup.
@@ -87,6 +88,156 @@ If you notice any areas in the documentation that need improvement or updating,
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.
## Adding New Language Translations
FileRise supports internationalization (i18n) and localization via a central translation file (`i18n.js`). If you would like to contribute a new language translation, please follow these steps:
1. **Update `i18n.js`:**
Open the `i18n.js` file located in the `js` directory. Within the `translations` object, add a new property using the appropriate [ISO language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) as the key. Copy the structure from an existing language block and translate each key.
**Example (for German):**
```js
de: {
"please_log_in_to_continue": "Bitte melden Sie sich an, um fortzufahren.",
"no_files_selected": "Keine Dateien ausgewählt.",
"confirm_delete_files": "Sind Sie sicher, dass Sie {count} ausgewählte Datei(en) löschen möchten?",
"element_not_found": "Element mit der ID \"{id}\" wurde nicht gefunden.",
"search_placeholder": "Suche nach Dateien oder Tags...",
"file_name": "Dateiname",
"date_modified": "Änderungsdatum",
"upload_date": "Hochladedatum",
"file_size": "Dateigröße",
"uploader": "Hochgeladen von",
"enter_totp_code": "Geben Sie den TOTP-Code ein",
"use_recovery_code_instead": "Verwenden Sie stattdessen den Wiederherstellungscode",
"enter_recovery_code": "Geben Sie den Wiederherstellungscode ein",
"editing": "Bearbeitung",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Speichern",
"close": "Schließen",
"no_files_found": "Keine Dateien gefunden.",
"switch_to_table_view": "Zur Tabellenansicht wechseln",
"switch_to_gallery_view": "Zur Galerieansicht wechseln",
"share_file": "Datei teilen",
"set_expiration": "Ablauf festlegen:",
"password_optional": "Passwort (optional):",
"generate_share_link": "Freigabelink generieren",
"shareable_link": "Freigabelink:",
"copy_link": "Link kopieren",
"tag_file": "Datei taggen",
"tag_name": "Tagname:",
"tag_color": "Tagfarbe:",
"save_tag": "Tag speichern",
"files_in": "Dateien in",
"light_mode": "Heller Modus",
"dark_mode": "Dunkler Modus",
"upload_instruction": "Ziehen Sie Dateien/Ordner hierher oder klicken Sie auf 'Dateien auswählen'",
"no_files_selected_default": "Keine Dateien ausgewählt",
"choose_files": "Dateien auswählen",
"delete_selected": "Ausgewählte löschen",
"copy_selected": "Ausgewählte kopieren",
"move_selected": "Ausgewählte verschieben",
"tag_selected": "Ausgewählte taggen",
"download_zip": "Zip herunterladen",
"extract_zip": "Zip entpacken",
"preview": "Vorschau",
"edit": "Bearbeiten",
"rename": "Umbenennen",
"trash_empty": "Papierkorb ist leer.",
"no_trash_selected": "Keine Elemente im Papierkorb für die Wiederherstellung ausgewählt.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Abmelden",
"change_password": "Passwort ändern",
"restore_text": "Wiederherstellen oder",
"delete_text": "Papierkorbeinträge löschen",
"restore_selected": "Ausgewählte wiederherstellen",
"restore_all": "Alle wiederherstellen",
"delete_selected_trash": "Ausgewählte löschen",
"delete_all": "Alle löschen",
"upload_header": "Dateien/Ordner hochladen",
// Folder Management keys:
"folder_navigation": "Ordnernavigation & Verwaltung",
"create_folder": "Ordner erstellen",
"create_folder_title": "Ordner erstellen",
"enter_folder_name": "Geben Sie den Ordnernamen ein",
"cancel": "Abbrechen",
"create": "Erstellen",
"rename_folder": "Ordner umbenennen",
"rename_folder_title": "Ordner umbenennen",
"rename_folder_placeholder": "Neuen Ordnernamen eingeben",
"delete_folder": "Ordner löschen",
"delete_folder_title": "Ordner löschen",
"delete_folder_message": "Sind Sie sicher, dass Sie diesen Ordner löschen möchten?",
"folder_help": "Ordnerhilfe",
"folder_help_item_1": "Klicken Sie auf einen Ordner, um dessen Dateien anzuzeigen.",
"folder_help_item_2": "Verwenden Sie [-] um zu minimieren und [+] um zu erweitern.",
"folder_help_item_3": "Klicken Sie auf \"Ordner erstellen\", um einen Unterordner hinzuzufügen.",
"folder_help_item_4": "Um einen Ordner umzubenennen oder zu löschen, wählen Sie ihn und klicken Sie auf die entsprechende Schaltfläche.",
// File List keys:
"file_list_title": "Dateien in (Root)",
"delete_files": "Dateien löschen",
"delete_selected_files_title": "Ausgewählte Dateien löschen",
"delete_files_message": "Sind Sie sicher, dass Sie die ausgewählten Dateien löschen möchten?",
"copy_files": "Dateien kopieren",
"copy_files_title": "Ausgewählte Dateien kopieren",
"copy_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu kopieren:",
"move_files": "Dateien verschieben",
"move_files_title": "Ausgewählte Dateien verschieben",
"move_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu verschieben:",
"move": "Verschieben",
"extract_zip_button": "Zip entpacken",
"download_zip_title": "Ausgewählte Dateien als Zip herunterladen",
"download_zip_prompt": "Geben Sie einen Namen für die Zip-Datei ein:",
"zip_placeholder": "dateien.zip",
// Login Form keys:
"login": "Anmelden",
"remember_me": "Angemeldet bleiben",
"login_oidc": "Mit OIDC anmelden",
"basic_http_login": "HTTP-Basisauthentifizierung verwenden",
// Change Password keys:
"change_password_title": "Passwort ändern",
"old_password": "Altes Passwort",
"new_password": "Neues Passwort",
"confirm_new_password": "Neues Passwort bestätigen",
// Add User keys:
"create_new_user_title": "Neuen Benutzer erstellen",
"username": "Benutzername:",
"password": "Passwort:",
"grant_admin": "Admin-Rechte vergeben",
"save_user": "Benutzer speichern",
// Remove User keys:
"remove_user_title": "Benutzer entfernen",
"select_user_remove": "Wählen Sie einen Benutzer zum Entfernen:",
"delete_user": "Benutzer löschen",
// Rename File keys:
"rename_file_title": "Datei umbenennen",
"rename_file_placeholder": "Neuen Dateinamen eingeben",
// Custom Confirm Modal keys:
"yes": "Ja",
"no": "Nein",
"delete": "Löschen",
"download": "Herunterladen",
"upload": "Hochladen",
"copy": "Kopieren",
"extract": "Entpacken",
// Dark Mode Toggle
"dark_mode_toggle": "Dunkler Modus"
}
---
Thank you for helping to improve FileRise and happy coding!

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 SeNS
Copyright (c) 2025 FileRise
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -12,20 +12,24 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
---
## Features at a Glance
## Features at a Glance or [Full Features Wiki](https://github.com/error311/FileRise/wiki/Features)
- 🚀 **Easy File Uploads:** Upload multiple files and folders via drag & drop or file picker. Supports large files with pause/resumable chunked uploads and shows real-time progress for each file. No more failed transfers FileRise will pick up where it left off if your connection drops.
- 🗂️ **File Management:** Full set of file/folder operations move or copy files (via intuitive drag-drop or dialogs), rename items, and delete in batches. You can even download selected files as a ZIP archive or extract uploaded ZIP files server-side. Organize content with an interactive folder tree and breadcrumb navigation for quick jumps.
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) convenient for sending individual files without exposing the whole app.
- 📝 **Built-in Editor & Preview:** View images, videos, audio, and PDFs inline with a preview modal no need to download just to see them. Edit text/code files right in your browser with a CodeMirror-based editor featuring syntax highlighting and line numbers. Great for config files or notes tweak and save changes without leaving FileRise.
- 🏷️ **Tags & Search:** Categorize your files with color-coded tags (labels) and later find them easily. The global search bar filters by filename or tag, making it simple to locate that “important” document in seconds. Tag management is built-in create, reuse, or remove tags as needed.
- 🏷️ **Tags & Search:** Categorize your files with color-coded tags and locate them instantly using our indexed real-time search. Easily switch to Advanced Search mode to enable fuzzy matching not only across file names, tags, and uploader fields but also within the content of text files—helping you find that “important” document even if you make a typo or need to search deep within the file.
- 🔒 **User Authentication, User Permissions & Sharing:** Secure your portal with username/password login. Supports multiple users create user accounts (admin UI provided) for family or team members. Basic user permissions such as User Folder Only, Read Only and Disable Upload. FileRise also integrates with Single Sign-On (OIDC) providers (e.g., OAuth2/OIDC for Google/Authentik/Keycloak) and offers optional TOTP two-factor auth for extra security. Share files with others using one-time or expiring public links (with password protection if desired) convenient for sending files without exposing the whole app.
- 🔒 **User Authentication & User Permissions:** Secure your portal with username/password login. Supports multiple users create user accounts (admin UI provided) for family or team members. User permissions such as User Folder Only” feature assigns each user a dedicated folder within the root directory, named after their username, restricting them from viewing or modifying other directories. User Read Only and Disable Upload are additional permissions. FileRise also integrates with Single Sign-On (OIDC) providers (e.g., OAuth2/OIDC for Google/Authentik/Keycloak) and offers optional TOTP two-factor auth for extra security.
- 🎨 **Responsive UI (Dark/Light Mode):** FileRise is mobile-friendly out of the box manage files from your phone or tablet with a responsive layout. Choose between Dark mode or Light theme, or let it follow your system preference. The interface remembers your preferences (layout, items per page, last visited folder, etc.) for a personalized experience each time.
- 🌐 **Internationalization & Localization:** FileRise supports multiple languages via an integrated i18n system. Users can switch languages through a user panel dropdown, and their choice is saved in local storage for a consistent experience across sessions. Currently available in English, Spanish, and French—please report any translation issues you encounter.
- 🗑️ **Trash & File Recovery:** Mistakenly deleted files? No worries deleted items go to the Trash instead of immediate removal. Admins can restore files from Trash or empty it to free space. FileRise auto-purges old trash entries (default 3 days) to keep your storage tidy.
- ⚙️ **Lightweight & Self-Contained:** FileRise runs on PHP 8.1+ with no external database required data is stored in files (users, metadata) for simplicity. Its a single-folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre-built image for a hassle-free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features.
@@ -173,6 +177,26 @@ Areas where you can help: translations, bug fixes, UI improvements, or building
---
## Dependencies
### PHP Libraries
- **[jumbojett/openid-connect-php](https://github.com/jumbojett/OpenID-Connect-PHP)** (v^1.0.0)
- **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7)
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
- **[endroid/qr-code](https://github.com/endroid/qr-code)** (v^5.0)
### Client-Side Libraries
- **Google Fonts** [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) For code editing functionality.
- **[Resumable.js](http://www.resumablejs.com/)** (v1.1.0) For file uploads.
- **[DOMPurify](https://github.com/cure53/DOMPurify)** (v2.4.0) For sanitizing HTML.
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) For indexed, fuzzy searching.
---
## License
This project is open-source under the MIT License. That means youre free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!

View File

@@ -49,7 +49,7 @@ if (!$newUsername || !$newPassword) {
}
// Validate username using preg_match (allow letters, numbers, underscores, dashes, and spaces).
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) {
if (!preg_match(REGEX_USER, $newUsername)) {
echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
exit;
}

View File

@@ -2,7 +2,9 @@
require_once 'vendor/autoload.php';
require_once 'config.php';
// Only send the Content-Type header; CORS and related headers are handled via .htaccess.
use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
header('Content-Type: application/json');
// Global exception handler: logs errors and returns a generic error message.
@@ -177,7 +179,7 @@ if (!$username || !$password) {
exit();
}
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
if (!preg_match(REGEX_USER, $username)) {
http_response_code(400);
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
exit();
@@ -197,7 +199,13 @@ if ($user !== false) {
]);
exit();
} else {
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
$tfa = new \RobThree\Auth\TwoFactorAuth(
new GoogleChartsQrCodeProvider(), // QR code provider
'FileRise', // issuer
6, // number of digits
30, // period in seconds
Algorithm::Sha1 // Correct enum case name from your enum
);
$providedCode = trim($data['totp_code']);
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
echo json_encode(["error" => "Invalid TOTP code"]);

View File

@@ -5,7 +5,7 @@
"require": {
"jumbojett/openid-connect-php": "^1.0.0",
"phpseclib/phpseclib": "~3.0.7",
"robthree/twofactorauth": "^1.7",
"endroid/qr-code": "^4.0"
"robthree/twofactorauth": "^3.0",
"endroid/qr-code": "^5.0"
}
}

74
composer.lock generated
View File

@@ -4,32 +4,32 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c9857f23364f2280ef4b71cdc72d3f78",
"content-hash": "6b70aec0c1830ebb2b8f9bb625b04a22",
"packages": [
{
"name": "bacon/bacon-qr-code",
"version": "2.0.8",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f",
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^7.1 || ^8.0"
"php": "^8.1"
},
"require-dev": {
"phly/keep-a-changelog": "^2.1",
"phpunit/phpunit": "^7 | ^8 | ^9",
"spatie/phpunit-snapshot-assertions": "^4.2.9",
"squizlabs/php_codesniffer": "^3.4"
"phly/keep-a-changelog": "^2.12",
"phpunit/phpunit": "^10.5.11 || 11.0.4",
"spatie/phpunit-snapshot-assertions": "^5.1.5",
"squizlabs/php_codesniffer": "^3.9"
},
"suggest": {
"ext-imagick": "to generate QR code images"
@@ -56,9 +56,9 @@
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1"
},
"time": "2022-12-07T17:46:57+00:00"
"time": "2024-10-01T13:55:55+00:00"
},
{
"name": "dasprid/enum",
@@ -112,29 +112,26 @@
},
{
"name": "endroid/qr-code",
"version": "4.8.5",
"version": "5.1.0",
"source": {
"type": "git",
"url": "https://github.com/endroid/qr-code.git",
"reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77"
"reference": "393fec6c4cbdc1bd65570ac9d245704428010122"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/endroid/qr-code/zipball/0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
"reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
"url": "https://api.github.com/repos/endroid/qr-code/zipball/393fec6c4cbdc1bd65570ac9d245704428010122",
"reference": "393fec6c4cbdc1bd65570ac9d245704428010122",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^2.0.5",
"bacon/bacon-qr-code": "^3.0",
"php": "^8.1"
},
"conflict": {
"khanamiryan/qrcode-detector-decoder": "^1.0.6"
},
"require-dev": {
"endroid/quality": "dev-master",
"endroid/quality": "dev-main",
"ext-gd": "*",
"khanamiryan/qrcode-detector-decoder": "^1.0.4||^2.0.2",
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
"setasign/fpdf": "^1.8.2"
},
"suggest": {
@@ -146,7 +143,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
"dev-main": "5.x-dev"
}
},
"autoload": {
@@ -175,7 +172,7 @@
],
"support": {
"issues": "https://github.com/endroid/qr-code/issues",
"source": "https://github.com/endroid/qr-code/tree/4.8.5"
"source": "https://github.com/endroid/qr-code/tree/5.1.0"
},
"funding": [
{
@@ -183,7 +180,7 @@
"type": "github"
}
],
"time": "2023-09-29T14:03:20+00:00"
"time": "2024-09-08T08:52:55+00:00"
},
{
"name": "jumbojett/openid-connect-php",
@@ -456,24 +453,25 @@
},
{
"name": "robthree/twofactorauth",
"version": "1.8.2",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/RobThree/TwoFactorAuth.git",
"reference": "65681de5a324eae05140ac58b08648a60212afc0"
"reference": "6d70f9ca8e25568f163a7b3b3ff77bd8ea743978"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/65681de5a324eae05140ac58b08648a60212afc0",
"reference": "65681de5a324eae05140ac58b08648a60212afc0",
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/6d70f9ca8e25568f163a7b3b3ff77bd8ea743978",
"reference": "6d70f9ca8e25568f163a7b3b3ff77bd8ea743978",
"shasum": ""
},
"require": {
"php": ">=5.6.0"
"php": ">=8.2.0"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpunit/phpunit": "@stable"
"friendsofphp/php-cs-fixer": "^3.13",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^9"
},
"suggest": {
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
@@ -494,6 +492,16 @@
"name": "Rob Janssen",
"homepage": "http://robiii.me",
"role": "Developer"
},
{
"name": "Nicolas CARPi",
"homepage": "https://github.com/NicolasCARPi",
"role": "Developer"
},
{
"name": "Will Power",
"homepage": "https://github.com/willpower232",
"role": "Developer"
}
],
"description": "Two Factor Authentication",
@@ -522,7 +530,7 @@
"type": "github"
}
],
"time": "2022-03-22T16:11:07+00:00"
"time": "2024-10-24T15:14:25+00:00"
}
],
"packages-dev": [],

View File

@@ -1,5 +1,20 @@
<?php
// config.php
header("Cache-Control: no-cache, must-revalidate");
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
header("Pragma: no-cache");
header("Expires: 0");
header('X-Content-Type-Options: nosniff');
// Security headers
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: SAMEORIGIN");
header("Referrer-Policy: no-referrer-when-downgrade");
// Only include Strict-Transport-Security if you are using HTTPS
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
header("Strict-Transport-Security: max-age=31536000; includeSubDomains; preload");
}
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
header("X-XSS-Protection: 1; mode=block");
// Define constants.
define('UPLOAD_DIR', '/var/www/uploads/');
@@ -11,6 +26,10 @@ 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');
define('REGEX_FOLDER_NAME', '/^[\p{L}\p{N}_\-\s\/\\\\]+$/u');
define('PATTERN_FOLDER_NAME', '[\p{L}\p{N}_\-\s\/\\\\]+');
define('REGEX_FILE_NAME', '/^[\p{L}\p{N}\p{M}%\-\.\(\) _]+$/u');
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
date_default_timezone_set(TIMEZONE);
@@ -48,9 +67,12 @@ function decryptData($encryptedData, $encryptionKey)
}
// 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.');
$envKey = getenv('PERSISTENT_TOKENS_KEY');
if ($envKey === false || $envKey === '') {
$encryptionKey = 'default_please_change_this_key';
error_log('WARNING: Using default encryption key. Please set PERSISTENT_TOKENS_KEY in your environment.');
} else {
$encryptionKey = $envKey;
}
function loadUserPermissions($username)

View File

@@ -44,7 +44,7 @@ $destinationFolder = trim($data['destination']);
$files = $data['files'];
// Validate folder names: allow letters, numbers, underscores, dashes, spaces, and forward slashes.
$folderPattern = '/^[A-Za-z0-9_\- \/]+$/';
$folderPattern = REGEX_FOLDER_NAME;
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
echo json_encode(["error" => "Invalid source folder name."]);
exit;
@@ -104,7 +104,7 @@ $destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($dest
$errors = [];
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($files as $fileName) {
// Save the original name for metadata lookup.

View File

@@ -45,13 +45,13 @@ $folderName = trim($input['folderName']);
$parent = isset($input['parent']) ? trim($input['parent']) : "";
// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces in folderName
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $folderName)) {
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
exit;
}
// Optionally, sanitize the parent folder if needed.
if ($parent && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $parent)) {
if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) {
echo json_encode(['success' => false, 'error' => 'Invalid parent folder name.']);
exit;
}

94
createFolderShareLink.php Normal file
View File

@@ -0,0 +1,94 @@
<?php
// createFolderShareLink.php
require_once 'config.php';
// Get POST input.
$input = json_decode(file_get_contents("php://input"), true);
if (!$input) {
echo json_encode(["error" => "Invalid input."]);
exit;
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to create shared folders."]);
exit();
}
}
$folder = isset($input['folder']) ? trim($input['folder']) : "";
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
$password = isset($input['password']) ? $input['password'] : "";
$allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0;
// Validate folder name using regex.
// Allow letters, numbers, underscores, hyphens, spaces and slashes.
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
// Generate a secure token.
try {
$token = bin2hex(random_bytes(16)); // 32 hex characters.
} catch (Exception $e) {
echo json_encode(["error" => "Could not generate token."]);
exit;
}
// Calculate expiration time (Unix timestamp).
$expires = time() + ($expirationMinutes * 60);
// Hash password if provided.
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
// Define the file to store share folder links.
$shareFile = META_DIR . "share_folder_links.json";
$shareLinks = [];
if (file_exists($shareFile)) {
$data = file_get_contents($shareFile);
$shareLinks = json_decode($data, true);
if (!is_array($shareLinks)) {
$shareLinks = [];
}
}
// Clean up expired share links.
$currentTime = time();
foreach ($shareLinks as $key => $link) {
if (isset($link["expires"]) && $link["expires"] < $currentTime) {
unset($shareLinks[$key]);
}
}
// Add the new share record.
$shareLinks[$token] = [
"folder" => $folder,
"expires" => $expires,
"password" => $hashedPassword,
"allowUpload" => $allowUpload
];
// Save the share links.
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT))) {
// Determine base URL.
if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) {
$baseUrl = rtrim(BASE_URL, '/');
} else {
// Prefer HTTP_HOST over SERVER_ADDR.
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
// Use HTTP_HOST if set; fallback to gethostbyname if needed.
$host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost');
$baseUrl = $protocol . "://" . $host;
}
// The share URL points to shareFolder.php.
$link = $baseUrl . "/shareFolder.php?token=" . urlencode($token);
echo json_encode(["token" => $token, "expires" => $expires, "link" => $link]);
} else {
echo json_encode(["error" => "Could not save share link."]);
}
?>

View File

@@ -9,13 +9,23 @@ if (!$input) {
exit;
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to create share files."]);
exit();
}
}
$folder = isset($input['folder']) ? trim($input['folder']) : "";
$file = isset($input['file']) ? basename($input['file']) : "";
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
$password = isset($input['password']) ? $input['password'] : "";
// Validate folder using regex.
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name."]);
exit;
}

View File

@@ -32,8 +32,8 @@ body {
@media (min-width: 1300px) {
.container-fluid {
padding-left: 40px !important;
padding-right: 40px !important;
padding-left: 30px !important;
padding-right: 30px !important;
}
}
@@ -69,7 +69,7 @@ body {
align-items: center;
justify-content: space-between;
width: 100%;
height: 80px;
height: 55px;
padding: 10px 20px;
background-color: #2196F3;
transition: background-color 0.3s ease;
@@ -82,28 +82,16 @@ body.dark-mode .header-container {
}
.header-logo {
max-height: 70px;
max-height: 50px;
width: auto;
display: block;
}
.header-logo svg {
height: 70px;
height: 50px;
width: auto;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 80px;
padding: 0 20px;
background-color: #2196F3;
transition: background-color 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
body.dark-mode header {
background-color: #1f1f1f;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
@@ -650,12 +638,15 @@ body.dark-mode .editor-header {
}
#uploadBtn {
margin-top: 20px;
font-size: 20px;
padding: 10px 22px;
align-items: center;
}
.card-body.d-flex.flex-column {
padding: 0.75rem !important;
}
#customChooseBtn {
background-color: #9E9E9E;
color: #fff;
@@ -849,7 +840,8 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
color: white;
}
.rename-btn .material-icons {
.rename-btn .material-icons,
#renameFolderBtn .material-icons {
color: black !important;
}
@@ -1580,39 +1572,6 @@ body.dark-mode .btn-secondary {
border-color: #6c757d;
}
#toggleViewBtn {
margin-bottom: 20px;
margin-left: 14px;
padding: 10px 20px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
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: 0 !important;
margin-right: auto !important;
display: block !important;
}
}
#toggleViewBtn:hover {
background: rgba(0, 0, 0, 0.8);
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.4);
}
body.dark-mode .btn-danger {
background-color: #dc3545;
color: #fff;
@@ -1725,21 +1684,6 @@ body.dark-mode .folder-help-icon {
}
body.dark-mode #searchIcon {
background-color: #444;
border: 1px solid #555;
border-radius: 4px;
color: #fff;
padding: 4px 8px;
}
body.dark-mode #searchInput {
background-color: #333;
color: #e0e0e0;
border: 1px solid #555;
}
body.dark-mode .CodeMirror {
background: #1e1e1e !important;
color: #ffffff !important;
@@ -1944,12 +1888,12 @@ body.dark-mode #folderContextMenu {
transition: transform 0.3s ease, opacity 0.3s ease;
width: 100%;
margin-bottom: 20px;
min-height: 353px;
min-height: 320px;
}
#uploadFolderRow.highlight {
min-height: 353px;
min-height: 320px;
margin-bottom: 20px;
}
@@ -2065,6 +2009,20 @@ body.dark-mode .admin-panel-content label {
animation: spin 1s linear infinite;
}
.download-spinner {
font-size: 48px;
animation: spin 2s linear infinite;
color: var(--download-spinner-color, #000);
}
body:not(.dark-mode) {
--download-spinner-color: #000;
}
body.dark-mode {
--download-spinner-color: #fff;
}
.rise-effect {
transform: translateY(-20px);
transition: transform 0.3s ease;
@@ -2120,3 +2078,80 @@ body.dark-mode .header-drop-zone.drag-active {
font-size: 10px;
color: #aaa;
}
/* Disable text selection on rows to prevent accidental copying when shift-clicking */
#fileList tbody tr.clickable-row {
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
}
#fileSummary {
color: black;
}
@media only screen and (max-width: 600px) {
#fileSummary {
float: none !important;
margin: 0 auto !important;
text-align: center !important;
}
}
body.dark-mode #fileSummary {
color: white;
}
#searchIcon {
border-radius: 4px;
padding: 4px 8px;
}
body.dark-mode #searchIcon {
background-color: #444;
border: 1px solid #555;
color: #fff;
}
body.dark-mode #searchInput {
background-color: #333;
color: #e0e0e0;
border: 1px solid #555;
}
.btn-icon {
background: transparent;
border: none;
padding: 6px 8px;
margin: 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.btn-icon .material-icons,
#searchIcon .material-icons {
font-size: 24px;
line-height: 1;
margin: 0;
padding: 0;
color: #333;
}
.btn-icon:hover,
.btn-icon:focus {
background: rgba(0, 0, 0, 0.1);
outline: none;
}
body.dark-mode .btn-icon .material-icons,
body.dark-mode #searchIcon .material-icons {
color: #fff;
}
body.dark-mode .btn-icon:hover,
body.dark-mode .btn-icon:focus {
background: rgba(255, 255, 255, 0.1);
}

View File

@@ -69,7 +69,7 @@ if (!isset($data['files']) || !is_array($data['files'])) {
$folder = isset($data['folder']) ? trim($data['folder']) : 'root';
// Validate folder: allow letters, numbers, underscores, dashes, spaces, and forward slashes
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
@@ -96,7 +96,7 @@ $movedFiles = [];
$errors = [];
// Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($data['files'] as $fileName) {
$basename = basename(trim($fileName));

View File

@@ -50,7 +50,7 @@ if ($folderName === 'root') {
}
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
if (!preg_match('/^[A-Za-z0-9_\- \/]+$/', $folderName)) {
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
exit;
}

View File

@@ -62,7 +62,7 @@ $deletedFiles = [];
$errors = [];
// Define a safe file name pattern.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($filesToDelete as $trashName) {
$trashName = trim($trashName);

View File

@@ -14,7 +14,7 @@ $file = isset($_GET['file']) ? basename($_GET['file']) : '';
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
// Validate file name (allowing letters, numbers, underscores, dashes, dots, and parentheses)
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $file)) {
if (!preg_match(REGEX_FILE_NAME, $file)) {
http_response_code(400);
echo json_encode(["error" => "Invalid file name."]);
exit;
@@ -80,10 +80,6 @@ if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
}
header('Content-Length: ' . filesize($realFilePath));
// Disable caching.
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
readfile($realFilePath);
exit;
?>

85
downloadSharedFile.php Normal file
View File

@@ -0,0 +1,85 @@
<?php
// downloadSharedFile.php
require_once 'config.php';
// Retrieve and sanitize token and file name from GET.
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
$file = filter_input(INPUT_GET, 'file', FILTER_SANITIZE_STRING);
if (empty($token) || empty($file)) {
http_response_code(400);
echo "Missing token or file parameter.";
exit;
}
// Load the share folder records.
$shareFile = META_DIR . "share_folder_links.json";
if (!file_exists($shareFile)) {
http_response_code(404);
echo "Share link not found.";
exit;
}
$shareLinks = json_decode(file_get_contents($shareFile), true);
if (!is_array($shareLinks) || !isset($shareLinks[$token])) {
http_response_code(404);
echo "Share link not found.";
exit;
}
$record = $shareLinks[$token];
// Check if the link has expired.
if (time() > $record['expires']) {
http_response_code(403);
echo "This share link has expired.";
exit;
}
// Get the shared folder from the record.
$folder = trim($record['folder'], "/\\ ");
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
$realFolderPath = realpath($folderPath);
$uploadDirReal = realpath(UPLOAD_DIR);
if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) {
http_response_code(404);
echo "Shared folder not found.";
exit;
}
// Sanitize the filename to prevent directory traversal.
if (strpos($file, "/") !== false || strpos($file, "\\") !== false) {
http_response_code(400);
echo "Invalid file name.";
exit;
}
$file = basename($file);
// Build the full file path and verify it is inside the shared folder.
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
$realFilePath = realpath($filePath);
if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) {
http_response_code(404);
echo "File not found.";
exit;
}
// Determine MIME type.
$mimeType = mime_content_type($realFilePath);
header("Content-Type: " . $mimeType);
// Set Content-Disposition header.
// Inline if the file is an image; attachment for others.
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
} else {
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
}
// Read and output the file.
readfile($realFilePath);
exit;
?>

View File

@@ -38,7 +38,7 @@ $files = $data['files'];
if ($folder !== "root") {
$parts = explode('/', $folder);
foreach ($parts as $part) {
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
if (empty($part) || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(["error" => "Invalid folder name."]);
@@ -76,7 +76,7 @@ if (empty($files)) {
}
foreach ($files as $fileName) {
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $fileName)) {
if (!preg_match(REGEX_FILE_NAME, $fileName)) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(["error" => "Invalid file name: " . $fileName]);

View File

@@ -50,7 +50,7 @@ if (empty($files)) {
if ($folder !== "root") {
$parts = explode('/', $folder);
foreach ($parts as $part) {
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
if (empty($part) || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) {
http_response_code(400);
echo json_encode(["error" => "Invalid folder name."]);
exit;
@@ -92,7 +92,7 @@ $destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($dest
$errors = [];
$allSuccess = true;
$extractedFiles = array(); // Array to collect names of extracted files
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
$safeFileNamePattern = REGEX_FILE_NAME;
// ---------- Process Each File ----------
foreach ($files as $zipFileName) {

View File

@@ -11,14 +11,24 @@ if (file_exists($configFile)) {
echo json_encode(['error' => 'Failed to decrypt configuration.']);
exit;
}
// Decode the configuration and ensure globalOtpauthUrl is set
// Decode the configuration and ensure required fields are set
$config = json_decode($decryptedContent, true);
// Ensure globalOtpauthUrl is set
if (!isset($config['globalOtpauthUrl'])) {
$config['globalOtpauthUrl'] = "";
}
// NEW: Ensure header_title is set.
if (!isset($config['header_title']) || empty($config['header_title'])) {
$config['header_title'] = "FileRise"; // default value
}
echo json_encode($config);
} else {
// If no config file exists, provide defaults
echo json_encode([
'header_title' => "FileRise",
'oidc' => [
'providerUrl' => 'https://your-oidc-provider.com',
'clientId' => 'YOUR_CLIENT_ID',

View File

@@ -1,8 +1,5 @@
<?php
require_once 'config.php';
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
header('Content-Type: application/json');
// Ensure user is authenticated
@@ -14,7 +11,7 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
// Allow only safe characters in the folder parameter (letters, numbers, underscores, dashes, spaces, and forward slashes).
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
@@ -28,11 +25,6 @@ if ($folder !== 'root') {
/**
* Helper: Generate the metadata file path for a given folder.
* For "root", returns "root_metadata.json". Otherwise, replaces slashes,
* backslashes, and spaces with dashes and appends "_metadata.json".
*
* @param string $folder The folder's relative path.
* @return string The full path to the folder's metadata file.
*/
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
@@ -53,7 +45,7 @@ $files = array_values(array_diff(scandir($directory), array('.', '..')));
$fileList = [];
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($files as $file) {
// Skip hidden files (those that begin with a dot)
@@ -72,7 +64,6 @@ foreach ($files as $file) {
// Since metadata is stored per folder, the key is simply the file name.
$metaKey = $file;
$fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown";
$fileUploadedDate = isset($metadata[$metaKey]["uploaded"]) ? $metadata[$metaKey]["uploaded"] : "Unknown";
$fileUploader = isset($metadata[$metaKey]["uploader"]) ? $metadata[$metaKey]["uploader"] : "Unknown";
@@ -88,7 +79,8 @@ foreach ($files as $file) {
$fileSizeFormatted = sprintf("%s bytes", number_format($fileSizeBytes));
}
$fileList[] = [
// Build the basic file entry.
$fileEntry = [
'name' => $file,
'modified' => $fileDateModified,
'uploaded' => $fileUploadedDate,
@@ -96,6 +88,14 @@ foreach ($files as $file) {
'uploader' => $fileUploader,
'tags' => isset($metadata[$metaKey]['tags']) ? $metadata[$metaKey]['tags'] : []
];
// Add file content for text-based files.
if (preg_match('/\.(txt|html|htm|md|js|css|json|xml|php|py|ini|conf|log)$/i', $file)) {
$content = file_get_contents($filePath);
$fileEntry['content'] = $content;
}
$fileList[] = $fileEntry;
}
// Load global tags from createdTags.json.

40
getFileTag.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
// getFileTag.php
require_once 'config.php';
// Set security and content headers
header('Content-Type: application/json; charset=utf-8');
$metadataPath = META_DIR . 'createdTags.json';
// Check if the metadata file exists and is readable
if (!file_exists($metadataPath) || !is_readable($metadataPath)) {
error_log('Metadata file does not exist or is not readable: ' . $metadataPath);
http_response_code(200); // Return empty array with HTTP 200 so the client can handle it gracefully
echo json_encode([]);
exit;
}
$data = file_get_contents($metadataPath);
if ($data === false) {
error_log('Failed to read metadata file: ' . $metadataPath);
http_response_code(500);
echo json_encode(["error" => "Unable to read metadata file."]);
exit;
}
// Decode the JSON data to check for validity
$jsonData = json_decode($data, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('Invalid JSON in metadata file: ' . $metadataPath . ' Error: ' . json_last_error_msg());
http_response_code(500);
echo json_encode(["error" => "Metadata file contains invalid JSON."]);
exit;
}
// Output the re-encoded JSON to ensure well-formed output
echo json_encode($jsonData);
exit;

View File

@@ -20,7 +20,7 @@ function getSubfolders($dir, $relative = '') {
$folders = [];
$items = scandir($dir);
// Allow letters, numbers, underscores, dashes, and spaces in folder names.
$safeFolderNamePattern = '/^[A-Za-z0-9_\- ]+$/';
$safeFolderNamePattern = REGEX_FOLDER_NAME;
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
if (!preg_match($safeFolderNamePattern, $item)) {

View File

@@ -17,7 +17,7 @@ if (file_exists($usersFile)) {
$parts = explode(':', trim($line));
if (count($parts) >= 3) {
// Validate username format:
if (preg_match('/^[A-Za-z0-9_\- ]+$/', $parts[0])) {
if (preg_match(REGEX_USER, $parts[0])) {
$users[] = [
"username" => $parts[0],
"role" => trim($parts[2])

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FileRise</title>
<title data-i18n-key="title">FileRise</title>
<script>
const params = new URLSearchParams(window.location.search);
if (params.get('logout') === '1') {
@@ -23,18 +23,34 @@
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js" integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js" integrity="sha384-xPpkMo5nDgD98fIcuRVYhxkZV6/9Y4L8s3p0J5c4MxgJkyKJ8BJr+xfRkq7kn6Tw" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js" integrity="sha384-to8njsu2GAiXQnY/aLGzz0DIY/SFSeSDodtvSl869n2NmsBdHOTZNNqbEBPYh7Pa" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js" integrity="sha384-kmQrbJf09Uo1WRLMDVGoVG3nM6F48frIhcj7f3FDUjeRzsiHwyBWDjMUIttnIeAf" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/resumable.js/1.1.0/resumable.min.js" integrity="sha384-EXTg7rRfdTPZWoKVCslusAAev2TYw76fm+Wox718iEtFQ+gdAdAc5Z/ndLHSo4mq" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js" integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js"
integrity="sha384-xPpkMo5nDgD98fIcuRVYhxkZV6/9Y4L8s3p0J5c4MxgJkyKJ8BJr+xfRkq7kn6Tw"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js"
integrity="sha384-to8njsu2GAiXQnY/aLGzz0DIY/SFSeSDodtvSl869n2NmsBdHOTZNNqbEBPYh7Pa"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"
integrity="sha384-kmQrbJf09Uo1WRLMDVGoVG3nM6F48frIhcj7f3FDUjeRzsiHwyBWDjMUIttnIeAf"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/resumable.js/1.1.0/resumable.min.js"
integrity="sha384-EXTg7rRfdTPZWoKVCslusAAev2TYw76fm+Wox718iEtFQ+gdAdAc5Z/ndLHSo4mq"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"
integrity="sha384-zPE55eyESN+FxCWGEnlNxGyAPJud6IZ6TtJmXb56OFRGhxZPN4akj9rjA3gw5Qqa"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="css/styles.css" />
</head>
<body>
<header class="header-container">
<div class="header-left">
<a href="index.html">
<div class="header-logo">
<svg version="1.1" id="filingCabinetLogo" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" xml:space="preserve">
@@ -98,49 +114,52 @@
</g>
</svg>
</div>
</a>
</div>
<div class="header-title">
<h1>FileRise</h1>
<h1 data-i18n-key="header_title">FileRise</h1>
</div>
<div class="header-right">
<div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;">
<!-- Your header drop zone -->
<div id="headerDropArea" class="header-drop-zone"></div>
<div class="header-buttons">
<button id="logoutBtn" title="Logout">
<button id="logoutBtn" data-i18n-title="logout">
<i class="material-icons">exit_to_app</i>
</button>
<button id="changePasswordBtn" title="Change Password" style="display: none;">
<button id="changePasswordBtn" data-i18n-title="change_password" style="display: none;">
<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">
<i class="material-icons orange-icon">restore_from_trash</i>
<span>Restore or</span>
<span data-i18n-key="restore_text">Restore or</span>
<i class="material-icons red-icon">delete_for_ever</i>
<span>Delete Trash Items</span>
<span data-i18n-key="delete_text">Delete Trash Items</span>
</h4>
<div id="restoreFilesList"
style="max-height:300px; overflow-y:auto; border:1px solid #ccc; padding:10px; margin-bottom:10px;">
<!-- Trash items will be loaded here -->
</div>
<div style="text-align: right;">
<button id="restoreSelectedBtn" class="btn btn-primary">Restore Selected</button>
<button id="restoreAllBtn" class="btn btn-secondary">Restore All</button>
<button id="deleteTrashSelectedBtn" class="btn btn-warning">Delete Selected</button>
<button id="deleteAllBtn" class="btn btn-danger">Delete All</button>
<button id="closeRestoreModal" class="btn btn-dark">Close</button>
<button id="restoreSelectedBtn" class="btn btn-primary" data-i18n-key="restore_selected">Restore
Selected</button>
<button id="restoreAllBtn" class="btn btn-secondary" data-i18n-key="restore_all">Restore All</button>
<button id="deleteTrashSelectedBtn" class="btn btn-warning" data-i18n-key="delete_selected_trash">Delete
Selected</button>
<button id="deleteAllBtn" class="btn btn-danger" data-i18n-key="delete_all">Delete All</button>
<button id="closeRestoreModal" class="btn btn-dark" data-i18n-key="close">Close</button>
</div>
</div>
</div>
<button id="addUserBtn" title="Add User" style="display: none;">
<button id="addUserBtn" data-i18n-title="add_user" style="display: none;">
<i class="material-icons">person_add</i>
</button>
<button id="removeUserBtn" title="Remove User" style="display: none;">
<button id="removeUserBtn" data-i18n-title="remove_user" style="display: none;">
<i class="material-icons">person_remove</i>
</button>
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
<button id="darkModeToggle" class="dark-mode-toggle" data-i18n-key="dark_mode_toggle">Dark Mode</button>
</div>
</div>
</div>
@@ -162,26 +181,27 @@
<div class="col-12">
<form id="authForm" method="post">
<div class="form-group">
<label for="loginUsername">User:</label>
<label for="loginUsername" data-i18n-key="user">User:</label>
<input type="text" class="form-control" id="loginUsername" name="username" required />
</div>
<div class="form-group">
<label for="loginPassword">Password:</label>
<label for="loginPassword" data-i18n-key="password">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>
<button type="submit" class="btn btn-primary btn-block btn-login" data-i18n-key="login">Login</button>
<div class="form-group remember-me-container">
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
<label for="rememberMeCheckbox">Remember me</label>
<label for="rememberMeCheckbox" data-i18n-key="remember_me">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>
<button id="oidcLoginBtn" class="btn btn-secondary" data-i18n-key="login_oidc">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>
<a href="login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic HTTP
Login</a>
</div>
</div>
</div>
@@ -194,20 +214,22 @@
<!-- 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-header" data-i18n-key="upload_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>
<span data-i18n-key="upload_instruction">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>
<button type="button" id="customChooseBtn" data-i18n-key="choose_files">Choose Files</button>
</div>
</div>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto"
data-i18n-key="upload">Upload</button>
<div id="uploadProgressContainer"></div>
</form>
</div>
@@ -217,8 +239,8 @@
<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"
<span data-i18n-key="folder_navigation">Folder Navigation &amp; Management</span>
<button id="folderHelpBtn" class="btn btn-link" data-i18n-title="folder_help"
style="padding: 0; border: none; background: none;">
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
</button>
@@ -228,42 +250,57 @@
<div id="folderTreeContainer"></div>
</div>
<div class="folder-actions mt-3">
<button id="createFolderBtn" class="btn btn-primary">Create Folder</button>
<button id="createFolderBtn" class="btn btn-primary" data-i18n-title="create_folder">
<i class="material-icons">create_new_folder</i>
</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"
<h4 data-i18n-key="create_folder_title">Create Folder</h4>
<input type="text" id="newFolderName" class="form-control"
data-i18n-placeholder="enter_folder_name" 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>
<button id="cancelCreateFolder" class="btn btn-secondary"
data-i18n-key="cancel">Cancel</button>
<button id="submitCreateFolder" class="btn btn-primary"
data-i18n-key="create">Create</button>
</div>
</div>
</div>
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
<button id="renameFolderBtn" class="btn btn-warning ml-2" data-i18n-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>
<h4 data-i18n-key="rename_folder_title">Rename Folder</h4>
<input type="text" id="newRenameFolderName" class="form-control"
placeholder="Enter new folder name" style="margin-top:10px;" />
data-i18n-placeholder="rename_folder_placeholder" 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>
<button id="cancelRenameFolder" class="btn btn-secondary"
data-i18n-key="cancel">Cancel</button>
<button id="submitRenameFolder" class="btn btn-primary"
data-i18n-key="rename">Rename</button>
</div>
</div>
</div>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
<button id="shareFolderBtn" class="btn btn-secondary ml-2" data-i18n-title="share_folder">
<i class="material-icons">share</i>
</button>
<button id="deleteFolderBtn" class="btn btn-danger ml-2" data-i18n-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>
<h4 data-i18n-key="delete_folder_title">Delete Folder</h4>
<p id="deleteFolderMessage" data-i18n-key="delete_folder_message">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>
<button id="cancelDeleteFolder" class="btn btn-secondary"
data-i18n-key="cancel">Cancel</button>
<button id="confirmDeleteFolder" class="btn btn-danger"
data-i18n-key="delete">Delete</button>
</div>
</div>
</div>
@@ -271,10 +308,12 @@
<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>
<li data-i18n-key="folder_help_item_1">Click on a folder in the tree to view its files.</li>
<li data-i18n-key="folder_help_item_2">Use [-] to collapse and [+] to expand folders.</li>
<li data-i18n-key="folder_help_item_3">Select a folder and click "Create Folder" to add a
subfolder.</li>
<li data-i18n-key="folder_help_item_4">To rename or delete a folder, select it and then click
the appropriate button.</li>
</ul>
</div>
</div>
@@ -286,53 +325,62 @@
<!-- File List Section -->
<div id="fileListContainer" style="display: none;">
<h2 id="fileListTitle">Files in (Root)</h2>
<h2 id="fileListTitle" data-i18n-key="file_list_title">Files in (Root)</h2>
<div id="fileListActions" class="file-list-actions">
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Files</button>
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;"
data-i18n-key="delete_files">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>
<h4 data-i18n-key="delete_selected_files_title">Delete Selected Files</h4>
<p id="deleteFilesMessage" data-i18n-key="delete_files_message">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>
<button id="cancelDeleteFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDeleteFiles" class="btn btn-danger" data-i18n-key="delete">Delete</button>
</div>
</div>
</div>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Files</button>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="copy_files">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>
<h4 data-i18n-key="copy_files_title">Copy Selected Files</h4>
<p id="copyFilesMessage" data-i18n-key="copy_files_message">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>
<button id="cancelCopyFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmCopyFiles" class="btn btn-primary" data-i18n-key="copy">Copy</button>
</div>
</div>
</div>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Files</button>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="move_files">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>
<h4 data-i18n-key="move_files_title">Move Selected Files</h4>
<p id="moveFilesMessage" data-i18n-key="move_files_message">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>
<button id="cancelMoveFiles" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmMoveFiles" class="btn btn-primary" data-i18n-key="move">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>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled
data-i18n-key="download_zip">Download ZIP</button>
<button id="extractZipBtn" class="btn btn-sm btn-info" data-i18n-title="extract_zip"
data-i18n-key="extract_zip_button">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" />
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
<p data-i18n-key="download_zip_prompt">Enter a name for the zip file:</p>
<input type="text" id="zipFileNameInput" class="form-control" data-i18n-placeholder="zip_placeholder"
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>
<button id="cancelDownloadZip" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="confirmDownloadZip" class="btn btn-primary" data-i18n-key="download">Download</button>
</div>
</div>
</div>
@@ -343,54 +391,82 @@
</div> <!-- end mainColumn -->
</div> <!-- end main-wrapper -->
<!-- Download Progress Modal -->
<div id="downloadProgressModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<!-- Material icon spinner with a dedicated class -->
<span class="material-icons download-spinner">autorenew</span>
<p data-i18n-key="preparing_download">Preparing your download...</p>
</div>
</div>
<!-- Single File Download Modal -->
<div id="downloadFileModal" class="modal" style="display: none;">
<div class="modal-content" style="text-align: center; padding: 20px;">
<h4 data-i18n-key="download_file">Download File</h4>
<p data-i18n-key="confirm_or_change_filename">Confirm or change the download file name:</p>
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename" placeholder="Filename" />
<div style="margin-top: 15px; text-align: right;">
<button id="cancelDownloadFile" class="btn btn-secondary"
onclick="document.getElementById('downloadFileModal').style.display = 'none';"
data-i18n-key="cancel">Cancel</button>
<button id="confirmSingleDownloadButton" class="btn btn-primary"
onclick="confirmSingleDownload()"
data-i18n-key="download">Download</button>
</div>
</div>
</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>
<span id="closeChangePasswordModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">&times;</span>
<h3 data-i18n-key="change_password_title">Change Password</h3>
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="newPassword" class="form-control" data-i18n-placeholder="new_password"
placeholder="New Password" style="width:100%; margin: 5px 0;" />
<input type="password" id="confirmPassword" class="form-control" data-i18n-placeholder="confirm_new_password"
placeholder="Confirm New Password" style="width:100%; margin: 5px 0;" />
<button id="saveNewPasswordBtn" class="btn btn-primary" data-i18n-key="save" style="width:100%;">Save</button>
</div>
</div>
<div id="addUserModal" class="modal">
<div class="modal-content">
<h3>Create New User</h3>
<label for="newUsername">Username:</label>
<h3 data-i18n-key="create_new_user_title">Create New User</h3>
<label for="newUsername" data-i18n-key="username">Username:</label>
<input type="text" id="newUsername" class="form-control" />
<label for="addUserPassword">Password:</label>
<label for="addUserPassword" data-i18n-key="password">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>
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
</div>
<div class="button-container">
<button id="cancelUserBtn" class="btn btn-secondary">Cancel</button>
<button id="saveUserBtn" class="btn btn-primary">Save User</button>
<button id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">Save User</button>
</div>
</div>
</div>
<div id="removeUserModal" class="modal">
<div class="modal-content">
<h3>Remove User</h3>
<label for="removeUsernameSelect">Select a user to remove:</label>
<h3 data-i18n-key="remove_user_title">Remove User</h3>
<label for="removeUsernameSelect" data-i18n-key="select_user_remove">Select a user to remove:</label>
<select id="removeUsernameSelect" class="form-control"></select>
<div class="button-container">
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button>
<button id="deleteUserBtn" class="btn btn-danger">Delete User</button>
<button id="cancelRemoveUserBtn" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="deleteUserBtn" class="btn btn-danger" data-i18n-key="delete_user">Delete User</button>
</div>
</div>
</div>
<div id="renameFileModal" class="modal">
<div class="modal-content">
<h4>Rename File</h4>
<input type="text" id="newFileName" class="form-control" placeholder="Enter new file name"
style="margin-top:10px;" />
<h4 data-i18n-key="rename_file_title">Rename File</h4>
<input type="text" id="newFileName" class="form-control" data-i18n-placeholder="rename_file_placeholder"
placeholder="Enter new file name" style="margin-top:10px;" />
<div style="margin-top:15px; text-align:right;">
<button id="cancelRenameFile" class="btn btn-secondary">Cancel</button>
<button id="submitRenameFile" class="btn btn-primary">Rename</button>
<button id="cancelRenameFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
<button id="submitRenameFile" class="btn btn-primary" data-i18n-key="rename">Rename</button>
</div>
</div>
</div>
@@ -398,8 +474,8 @@
<div class="modal-content">
<p id="confirmMessage"></p>
<div class="modal-actions">
<button id="confirmYesBtn" class="btn btn-primary">Yes</button>
<button id="confirmNoBtn" class="btn btn-secondary">No</button>
<button id="confirmYesBtn" class="btn btn-primary" data-i18n-key="yes">Yes</button>
<button id="confirmNoBtn" class="btn btn-secondary" data-i18n-key="no">No</button>
</div>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { sendRequest } from './networkUtils.js';
import { t, applyTranslations } from './i18n.js';
import {
toggleVisibility,
showToast as originalShowToast,
@@ -34,8 +35,9 @@ window.currentOIDCConfig = currentOIDCConfig;
window.pendingTOTP = new URLSearchParams(window.location.search).get('totp_required') === '1';
// override showToast to suppress the "Please log in to continue." toast during TOTP
function showToast(msg) {
if (window.pendingTOTP && msg === "Please log in to continue.") {
function showToast(msgKey) {
const msg = t(msgKey);
if (window.pendingTOTP && msgKey === "please_log_in_to_continue") {
return;
}
originalShowToast(msg);
@@ -95,18 +97,36 @@ function loadAdminConfigFunc() {
return fetch("getConfig.php", { credentials: "include" })
.then(response => response.json())
.then(config => {
// Save header_title into localStorage (if needed)
localStorage.setItem("headerTitle", config.header_title || "FileRise");
// Update login options and global OTPAuth URL as before
localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin);
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
// Update the UI for login options
updateLoginOptionsUIFromStorage();
const headerTitleElem = document.querySelector(".header-title h1");
if (headerTitleElem) {
headerTitleElem.textContent = config.header_title || "FileRise";
}
})
.catch(() => {
// Fallback defaults in case of error
localStorage.setItem("headerTitle", "FileRise");
localStorage.setItem("disableFormLogin", "false");
localStorage.setItem("disableBasicAuth", "false");
localStorage.setItem("disableOIDCLogin", "false");
localStorage.setItem("globalOtpauthUrl", "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
updateLoginOptionsUIFromStorage();
const headerTitleElem = document.querySelector(".header-title h1");
if (headerTitleElem) {
headerTitleElem.textContent = "FileRise";
}
});
}
@@ -130,10 +150,11 @@ function updateAuthenticatedUI(data) {
if (data.username) {
localStorage.setItem("username", data.username);
}
/*
if (typeof data.folderOnly !== "undefined") {
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
}
*/
const headerButtons = document.querySelector(".header-buttons");
const firstButton = headerButtons.firstElementChild;
@@ -143,7 +164,8 @@ function updateAuthenticatedUI(data) {
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>';
restoreBtn.setAttribute("data-i18n-title", "trash_restore_delete");
restoreBtn.innerHTML = '<i class="material-icons">restore_from_trash</i>';
if (firstButton) insertAfter(restoreBtn, firstButton);
else headerButtons.appendChild(restoreBtn);
}
@@ -154,7 +176,8 @@ function updateAuthenticatedUI(data) {
adminPanelBtn = document.createElement("button");
adminPanelBtn.id = "adminPanelBtn";
adminPanelBtn.classList.add("btn", "btn-info");
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
adminPanelBtn.setAttribute("data-i18n-title", "admin_panel");
adminPanelBtn.innerHTML = '<i class="material-icons">admin_panel_settings</i>';
insertAfter(adminPanelBtn, restoreBtn);
adminPanelBtn.addEventListener("click", openAdminPanel);
} else {
@@ -173,7 +196,9 @@ function updateAuthenticatedUI(data) {
userPanelBtn = document.createElement("button");
userPanelBtn.id = "userPanelBtn";
userPanelBtn.classList.add("btn", "btn-user");
userPanelBtn.innerHTML = '<i class="material-icons" title="User Panel">account_circle</i>';
userPanelBtn.setAttribute("data-i18n-title", "user_panel");
userPanelBtn.innerHTML = '<i class="material-icons">account_circle</i>';
const adminBtn = document.getElementById("adminPanelBtn");
if (adminBtn) insertAfter(userPanelBtn, adminBtn);
else if (firstButton) insertAfter(userPanelBtn, firstButton);
@@ -183,7 +208,7 @@ function updateAuthenticatedUI(data) {
userPanelBtn.style.display = "block";
}
}
applyTranslations();
updateItemsPerPageSelect();
updateLoginOptionsUIFromStorage();
}
@@ -225,11 +250,25 @@ function checkAuthentication(showLoginToast = true) {
function submitLogin(data) {
setLastLoginData(data);
window.__lastLoginData = data;
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
.then(response => {
if (response.success || response.status === "ok") {
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
// Fetch and update permissions, then reload.
sendRequest("getUserPermissions.php", "GET")
.then(permissionData => {
if (permissionData && typeof permissionData === "object") {
localStorage.setItem("folderOnly", permissionData.folderOnly ? "true" : "false");
localStorage.setItem("readOnly", permissionData.readOnly ? "true" : "false");
localStorage.setItem("disableUpload", permissionData.disableUpload ? "true" : "false");
}
})
.catch(() => {
// if fetching permissions fails.
})
.finally(() => {
window.location.reload();
});
} else if (response.totp_required) {
openTOTPLoginModal();
} else if (response.error && response.error.includes("Too many failed login attempts")) {
@@ -291,7 +330,7 @@ function loadUserList() {
closeRemoveUserModal();
}
})
.catch(() => {});
.catch(() => { });
}
window.loadUserList = loadUserList;
@@ -318,7 +357,7 @@ function initAuth() {
method: "POST",
credentials: "include",
headers: { "X-CSRF-Token": window.csrfToken }
}).then(() => window.location.reload(true)).catch(() => {});
}).then(() => window.location.reload(true)).catch(() => { });
});
document.getElementById("addUserBtn").addEventListener("click", function () {
resetUserForm();
@@ -384,7 +423,7 @@ function initAuth() {
showToast("Error: " + (data.error || "Could not remove user"));
}
})
.catch(() => {});
.catch(() => { });
});
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
document.getElementById("changePasswordBtn").addEventListener("click", function () {

View File

@@ -1,8 +1,10 @@
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
import { sendRequest } from './networkUtils.js';
import { t, applyTranslations, setLocale } from './i18n.js';
const version = "v1.0.9";
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
const version = "v1.1.3";
// Use t() for the admin panel title. (Make sure t("admin_panel") returns "Admin Panel" in English.)
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
let lastLoginData = null;
export function setLastLoginData(data) {
@@ -32,18 +34,18 @@ export function openTOTPLoginModal() {
<div style="background: ${modalBg}; padding:20px; border-radius:8px; text-align:center; position:relative; color:${textColor};">
<span id="closeTOTPLoginModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">&times;</span>
<div id="totpSection">
<h3>Enter TOTP Code</h3>
<h3>${t("enter_totp_code")}</h3>
<input type="text" id="totpLoginInput" maxlength="6"
style="font-size:24px; text-align:center; width:100%; padding:10px;"
placeholder="6-digit code" />
</div>
<a href="#" id="toggleRecovery" style="display:block; margin-top:10px; font-size:14px;">Use Recovery Code instead</a>
<a href="#" id="toggleRecovery" style="display:block; margin-top:10px; font-size:14px;">${t("use_recovery_code_instead")}</a>
<div id="recoverySection" style="display:none; margin-top:10px;">
<h3>Enter Recovery Code</h3>
<h3>${t("enter_recovery_code")}</h3>
<input type="text" id="recoveryInput"
style="font-size:24px; text-align:center; width:100%; padding:10px;"
placeholder="Recovery code" />
<button type="button" id="submitRecovery" class="btn btn-secondary" style="margin-top:10px;">Submit Recovery Code</button>
<button type="button" id="submitRecovery" class="btn btn-secondary" style="margin-top:10px;">${t("submit_recovery_code")}</button>
</div>
</div>
`;
@@ -65,12 +67,12 @@ export function openTOTPLoginModal() {
// Switch to recovery
totpSection.style.display = "none";
recoverySection.style.display = "block";
toggleLink.textContent = "Use TOTP Code instead";
toggleLink.textContent = t("use_totp_code_instead");
} else {
// Switch back to TOTP
recoverySection.style.display = "none";
totpSection.style.display = "block";
toggleLink.textContent = "Use Recovery Code instead";
toggleLink.textContent = t("use_recovery_code_instead");
}
});
@@ -78,7 +80,7 @@ export function openTOTPLoginModal() {
document.getElementById("submitRecovery").addEventListener("click", () => {
const recoveryCode = document.getElementById("recoveryInput").value.trim();
if (!recoveryCode) {
showToast("Please enter your recovery code.");
showToast(t("please_enter_recovery_code"));
return;
}
fetch("totp_recover.php", {
@@ -96,11 +98,11 @@ export function openTOTPLoginModal() {
// recovery succeeded → finalize login
window.location.href = "index.html";
} else {
showToast(json.message || "Recovery code verification failed");
showToast(json.message || t("recovery_code_verification_failed"));
}
})
.catch(() => {
showToast("Error verifying recovery code.");
showToast(t("error_verifying_recovery_code"));
});
});
@@ -124,14 +126,14 @@ export function openTOTPLoginModal() {
if (json.status === "ok") {
window.location.href = "index.html";
} else {
showToast(json.message || "TOTP verification failed");
showToast(json.message || t("totp_verification_failed"));
this.value = "";
totpLoginModal.style.display = "flex";
totpInput.focus();
}
})
.catch(() => {
showToast("TOTP verification failed");
showToast(t("totp_verification_failed"));
this.value = "";
totpLoginModal.style.display = "flex";
totpInput.focus();
@@ -161,13 +163,15 @@ export function openUserPanel() {
max-width: 600px;
width: 90%;
border-radius: 8px;
position: relative;
position: fixed;
overflow-y: auto;
max-height: 90vh;
max-height: 350px !important;
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
transform: none;
transition: none;
`;
// Retrieve the language setting from local storage, default to English ("en")
const savedLanguage = localStorage.getItem("language") || "en";
if (!userPanelModal) {
userPanelModal = document.createElement("div");
userPanelModal.id = "userPanelModal";
@@ -184,26 +188,41 @@ export function openUserPanel() {
z-index: 3000;
`;
userPanelModal.innerHTML = `
<div class="modal-content" style="${modalContentStyles}">
<div class="modal-content user-panel-content" style="${modalContentStyles}">
<span id="closeUserPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>User Panel (${username})</h3>
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">Change Password</button>
<h3>${t("user_panel")} (${username})</h3>
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">${t("change_password")}</button>
<fieldset style="margin-bottom: 15px;">
<legend>TOTP Settings</legend>
<legend>${t("totp_settings")}</legend>
<div class="form-group">
<label for="userTOTPEnabled">Enable TOTP:</label>
<label for="userTOTPEnabled">${t("enable_totp")}:</label>
<input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" />
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>${t("language")}</legend>
<div class="form-group">
<label for="languageSelector">${t("select_language")}:</label>
<select id="languageSelector">
<option value="en">${t("english")}</option>
<option value="es">${t("spanish")}</option>
<option value="fr">${t("french")}</option>
<option value="de">${t("german")}</option>
</select>
</div>
</fieldset>
</div>
`;
document.body.appendChild(userPanelModal);
// Close button handler
document.getElementById("closeUserPanel").addEventListener("click", () => {
userPanelModal.style.display = "none";
});
// Change Password button
document.getElementById("openChangePasswordModalBtn").addEventListener("click", () => {
document.getElementById("changePasswordModal").style.display = "block";
});
// TOTP checkbox behavior
const totpCheckbox = document.getElementById("userTOTPEnabled");
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
totpCheckbox.addEventListener("change", function () {
@@ -221,14 +240,24 @@ export function openUserPanel() {
.then(r => r.json())
.then(result => {
if (!result.success) {
showToast("Error updating TOTP setting: " + result.error);
showToast(t("error_updating_totp_setting") + ": " + result.error);
} else if (enabled) {
openTOTPModal();
}
})
.catch(() => { showToast("Error updating TOTP setting."); });
.catch(() => { showToast(t("error_updating_totp_setting")); });
});
// Language dropdown initialization
const languageSelector = document.getElementById("languageSelector");
languageSelector.value = savedLanguage;
languageSelector.addEventListener("change", function () {
const selectedLanguage = this.value;
localStorage.setItem("language", selectedLanguage);
setLocale(selectedLanguage);
applyTranslations();
});
} else {
// If the modal already exists, update its colors
userPanelModal.style.backgroundColor = overlayBackground;
const modalContent = userPanelModal.querySelector(".modal-content");
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
@@ -255,10 +284,10 @@ function showRecoveryCodeModal(recoveryCode) {
`;
recoveryModal.innerHTML = `
<div style="background: #fff; color: #000; padding: 20px; max-width: 400px; width: 90%; border-radius: 8px; text-align: center;">
<h3>Your Recovery Code</h3>
<p>Please save this code securely. It will not be shown again and can only be used once.</p>
<h3>${t("your_recovery_code")}</h3>
<p>${t("please_save_recovery_code")}</p>
<code style="display: block; margin: 10px 0; font-size: 20px;">${recoveryCode}</code>
<button type="button" id="closeRecoveryModal" class="btn btn-primary">OK</button>
<button type="button" id="closeRecoveryModal" class="btn btn-primary">${t("ok")}</button>
</div>
`;
document.body.appendChild(recoveryModal);
@@ -299,17 +328,19 @@ export function openTOTPModal() {
totpModal.innerHTML = `
<div class="modal-content" style="${modalContentStyles}">
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>TOTP Setup</h3>
<p>Scan this QR code with your authenticator app:</p>
<img src="totp_setup.php?csrf=${encodeURIComponent(window.csrfToken)}" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
<h3>${t("totp_setup")}</h3>
<p>${t("scan_qr_code")}</p>
<!-- Create an image placeholder without the CSRF token in the src -->
<img id="totpQRCodeImage" src="" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
<br/>
<p>Enter the 6-digit code from your app to confirm setup:</p>
<p>${t("enter_totp_confirmation")}</p>
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
<br/><br/>
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button>
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">${t("confirm")}</button>
</div>
`;
document.body.appendChild(totpModal);
loadTOTPQRCode();
document.getElementById("closeTOTPModal").addEventListener("click", () => {
closeTOTPModal(true);
@@ -318,7 +349,7 @@ export function openTOTPModal() {
document.getElementById("confirmTOTPBtn").addEventListener("click", function () {
const code = document.getElementById("totpConfirmInput").value.trim();
if (code.length !== 6) {
showToast("Please enter a valid 6-digit code.");
showToast(t("please_enter_valid_code"));
return;
}
fetch("totp_verify.php", {
@@ -333,7 +364,7 @@ export function openTOTPModal() {
.then(r => r.json())
.then(result => {
if (result.status === 'ok') {
showToast("TOTP successfully enabled.");
showToast(t("totp_enabled_successfully"));
// After successful TOTP verification, fetch the recovery code
fetch("totp_saveCode.php", {
method: "POST",
@@ -349,16 +380,16 @@ export function openTOTPModal() {
// Show the recovery code in a secure modal
showRecoveryCodeModal(data.recoveryCode);
} else {
showToast("Error generating recovery code: " + (data.message || "Unknown error."));
showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error")));
}
})
.catch(() => { showToast("Error generating recovery code."); });
.catch(() => { showToast(t("error_generating_recovery_code")); });
closeTOTPModal(false);
} else {
showToast("TOTP verification failed: " + (result.message || "Invalid code."));
showToast(t("totp_verification_failed") + ": " + (result.message || t("invalid_code")));
}
})
.catch(() => { showToast("Error verifying TOTP code."); });
.catch(() => { showToast(t("error_verifying_totp_code")); });
});
// Focus the input and attach enter key listener
@@ -378,6 +409,13 @@ export function openTOTPModal() {
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
// Clear any previous QR code src if needed and then load it:
const qrImg = document.getElementById("totpQRCodeImage");
if (qrImg) {
qrImg.src = "";
}
loadTOTPQRCode();
// Focus the input and attach enter key listener
const totpConfirmInput = document.getElementById("totpConfirmInput");
if (totpConfirmInput) {
@@ -391,6 +429,33 @@ export function openTOTPModal() {
}
}
function loadTOTPQRCode() {
fetch("totp_setup.php", {
method: "GET",
credentials: "include",
headers: {
"X-CSRF-Token": window.csrfToken // Send your CSRF token here
}
})
.then(response => {
if (!response.ok) {
throw new Error("Failed to fetch QR code. Status: " + response.status);
}
return response.blob();
})
.then(blob => {
const imageURL = URL.createObjectURL(blob);
const qrImg = document.getElementById("totpQRCodeImage");
if (qrImg) {
qrImg.src = imageURL;
}
})
.catch(error => {
console.error("Error loading TOTP QR code:", error);
showToast(t("error_loading_qr_code"));
});
}
// Updated closeTOTPModal function with a disable parameter
export function closeTOTPModal(disable = true) {
const totpModal = document.getElementById("totpModal");
@@ -415,17 +480,88 @@ export function closeTOTPModal(disable = true) {
.then(r => r.json())
.then(result => {
if (!result.success) {
showToast("Error disabling TOTP setting: " + result.error);
showToast(t("error_disabling_totp_setting") + ": " + result.error);
}
})
.catch(() => { showToast("Error disabling TOTP setting."); });
.catch(() => { showToast(t("error_disabling_totp_setting")); });
}
}
// Global variable to hold the initial state of the admin form.
let originalAdminConfig = {};
// Capture the initial state of the admin form fields.
function captureInitialAdminConfig() {
originalAdminConfig = {
headerTitle: document.getElementById("headerTitle").value.trim(),
oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(),
oidcClientId: document.getElementById("oidcClientId").value.trim(),
oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(),
oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(),
disableFormLogin: document.getElementById("disableFormLogin").checked,
disableBasicAuth: document.getElementById("disableBasicAuth").checked,
disableOIDCLogin: document.getElementById("disableOIDCLogin").checked,
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
};
}
// Compare current values to the captured initial state.
function hasUnsavedChanges() {
return (
document.getElementById("headerTitle").value.trim() !== originalAdminConfig.headerTitle ||
document.getElementById("oidcProviderUrl").value.trim() !== originalAdminConfig.oidcProviderUrl ||
document.getElementById("oidcClientId").value.trim() !== originalAdminConfig.oidcClientId ||
document.getElementById("oidcClientSecret").value.trim() !== originalAdminConfig.oidcClientSecret ||
document.getElementById("oidcRedirectUri").value.trim() !== originalAdminConfig.oidcRedirectUri ||
document.getElementById("disableFormLogin").checked !== originalAdminConfig.disableFormLogin ||
document.getElementById("disableBasicAuth").checked !== originalAdminConfig.disableBasicAuth ||
document.getElementById("disableOIDCLogin").checked !== originalAdminConfig.disableOIDCLogin ||
document.getElementById("globalOtpauthUrl").value.trim() !== originalAdminConfig.globalOtpauthUrl
);
}
// Use your custom confirmation modal.
function showCustomConfirmModal(message) {
return new Promise((resolve) => {
// Get modal elements from DOM.
const modal = document.getElementById("customConfirmModal");
const messageElem = document.getElementById("confirmMessage");
const yesBtn = document.getElementById("confirmYesBtn");
const noBtn = document.getElementById("confirmNoBtn");
// Set the message in the modal.
messageElem.textContent = message;
modal.style.display = "block";
// Define event handlers.
function onYes() {
cleanup();
resolve(true);
}
function onNo() {
cleanup();
resolve(false);
}
// Remove event listeners and hide modal after choice.
function cleanup() {
yesBtn.removeEventListener("click", onYes);
noBtn.removeEventListener("click", onNo);
modal.style.display = "none";
}
yesBtn.addEventListener("click", onYes);
noBtn.addEventListener("click", onNo);
});
}
export function openAdminPanel() {
fetch("getConfig.php", { credentials: "include" })
.then(response => response.json())
.then(config => {
if (config.header_title) {
document.querySelector(".header-title h1").textContent = config.header_title;
window.headerTitle = config.header_title || "FileRise";
}
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
const isDarkMode = document.body.classList.contains("dark-mode");
@@ -459,77 +595,84 @@ export function openAdminPanel() {
align-items: center;
z-index: 3000;
`;
// Added a version number next to "Admin Panel"
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>
<h3>${adminTitle}</h3>
</h3>
<form id="adminPanelForm">
<fieldset style="margin-bottom: 15px;">
<legend>User Management</legend>
<legend>${t("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>
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">User Permissions</button>
<button type="button" id="adminOpenAddUser" class="btn btn-success">${t("add_user")}</button>
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">${t("remove_user")}</button>
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>OIDC Configuration</legend>
<legend>Header Settings</legend>
<div class="form-group">
<label for="oidcProviderUrl">OIDC Provider URL:</label>
<label for="headerTitle">Header Title:</label>
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle}" />
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>${t("login_options")}</legend>
<div class="form-group">
<input type="checkbox" id="disableFormLogin" />
<label for="disableFormLogin">${t("disable_login_form")}</label>
</div>
<div class="form-group">
<input type="checkbox" id="disableBasicAuth" />
<label for="disableBasicAuth">${t("disable_basic_http_auth")}</label>
</div>
<div class="form-group">
<input type="checkbox" id="disableOIDCLogin" />
<label for="disableOIDCLogin">${t("disable_oidc_login")}</label>
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>${t("oidc_configuration")}</legend>
<div class="form-group">
<label for="oidcProviderUrl">${t("oidc_provider_url")}:</label>
<input type="text" id="oidcProviderUrl" class="form-control" value="${window.currentOIDCConfig.providerUrl}" />
</div>
<div class="form-group">
<label for="oidcClientId">OIDC Client ID:</label>
<label for="oidcClientId">${t("oidc_client_id")}:</label>
<input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" />
</div>
<div class="form-group">
<label for="oidcClientSecret">OIDC Client Secret:</label>
<label for="oidcClientSecret">${t("oidc_client_secret")}:</label>
<input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" />
</div>
<div class="form-group">
<label for="oidcRedirectUri">OIDC Redirect URI:</label>
<label for="oidcRedirectUri">${t("oidc_redirect_uri")}:</label>
<input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" />
</div>
</fieldset>
<fieldset style="margin-bottom: 15px;">
<legend>Global TOTP Settings</legend>
<legend>${t("global_totp_settings")}</legend>
<div class="form-group">
<label for="globalOtpauthUrl">Global OTPAuth URL:</label>
<label for="globalOtpauthUrl">${t("global_otpauth_url")}:</label>
<input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" />
</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>
<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>
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">${t("cancel")}</button>
<button type="button" id="saveAdminSettings" class="btn btn-primary">${t("save_settings")}</button>
</div>
</form>
</div>
`;
document.body.appendChild(adminModal);
// Bind closing events that will use our enhanced close function.
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
adminModal.addEventListener("click", (e) => {
if (e.target === adminModal) closeAdminPanel();
});
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
// Bind other buttons.
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
toggleVisibility("addUserModal", true);
document.getElementById("newUsername").focus();
@@ -540,7 +683,6 @@ export function openAdminPanel() {
}
toggleVisibility("removeUserModal", true);
});
// New event binding for the User Permissions button:
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
openUserPermissionsModal();
});
@@ -550,7 +692,7 @@ export function openAdminPanel() {
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.");
showToast(t("at_least_one_login_method"));
disableOIDCLoginCheckbox.checked = false;
localStorage.setItem("disableOIDCLogin", "false");
if (typeof window.updateLoginOptionsUI === "function") {
@@ -562,6 +704,7 @@ export function openAdminPanel() {
}
return;
}
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
const newOIDCConfig = {
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
clientId: document.getElementById("oidcClientId").value.trim(),
@@ -573,6 +716,7 @@ export function openAdminPanel() {
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
sendRequest("updateConfig.php", "POST", {
header_title: newHeaderTitle,
oidc: newOIDCConfig,
disableFormLogin,
disableBasicAuth,
@@ -581,27 +725,31 @@ export function openAdminPanel() {
}, { "X-CSRF-Token": window.csrfToken })
.then(response => {
if (response.success) {
showToast("Settings updated successfully.");
showToast(t("settings_updated_successfully"));
localStorage.setItem("disableFormLogin", disableFormLogin);
localStorage.setItem("disableBasicAuth", disableBasicAuth);
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
if (typeof window.updateLoginOptionsUI === "function") {
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
}
// Update the captured initial state since the changes have now been saved.
captureInitialAdminConfig();
closeAdminPanel();
} else {
showToast("Error updating settings: " + (response.error || "Unknown error"));
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
}
})
.catch(() => { });
});
// Enforce login option constraints.
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.");
showToast(t("at_least_one_login_method"));
changedCheckbox.checked = false;
}
}
@@ -612,6 +760,9 @@ export function openAdminPanel() {
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
// Capture initial state after the modal loads.
captureInitialAdminConfig();
} else {
adminModal.style.backgroundColor = overlayBackground;
const modalContent = adminModal.querySelector(".modal-content");
@@ -629,6 +780,7 @@ export function openAdminPanel() {
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
adminModal.style.display = "flex";
captureInitialAdminConfig();
}
})
.catch(() => {
@@ -650,19 +802,25 @@ export function openAdminPanel() {
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
adminModal.style.display = "flex";
captureInitialAdminConfig();
} else {
openAdminPanel();
}
});
}
export function closeAdminPanel() {
export async function closeAdminPanel() {
if (hasUnsavedChanges()) {
const userConfirmed = await showCustomConfirmModal(t("unsaved_changes_confirm"));
if (!userConfirmed) {
return;
}
}
const adminModal = document.getElementById("adminPanelModal");
if (adminModal) adminModal.style.display = "none";
}
// --- New: User Permissions Modal ---
export function openUserPermissionsModal() {
let userPermissionsModal = document.getElementById("userPermissionsModal");
const isDarkMode = document.body.classList.contains("dark-mode");
@@ -695,13 +853,13 @@ export function openUserPermissionsModal() {
userPermissionsModal.innerHTML = `
<div class="modal-content" style="${modalContentStyles}">
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
<h3>User Permissions</h3>
<h3>${t("user_permissions")}</h3>
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
<!-- User rows will be loaded here -->
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">Cancel</button>
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">Save Permissions</button>
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">${t("cancel")}</button>
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">${t("save_permissions")}</button>
</div>
</div>
`;
@@ -732,14 +890,14 @@ export function openUserPermissionsModal() {
sendRequest("updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
.then(response => {
if (response.success) {
showToast("User permissions updated successfully.");
showToast(t("user_permissions_updated_successfully"));
userPermissionsModal.style.display = "none";
} else {
showToast("Error updating permissions: " + (response.error || "Unknown error"));
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
}
})
.catch(() => {
showToast("Error updating permissions.");
showToast(t("error_updating_permissions"));
});
});
} else {
@@ -764,20 +922,26 @@ function loadUserPermissionsList() {
.then(usersData => {
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
if (users.length === 0) {
listContainer.innerHTML = "<p>No users found.</p>";
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
return;
}
users.forEach(user => {
// Skip admin users.
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
// Use stored permissions if available; otherwise fall back to localStorage defaults.
// Use stored permissions if available; otherwise fall back to defaults.
const defaultPerm = {
folderOnly: localStorage.getItem("folderOnly") === "true",
readOnly: localStorage.getItem("readOnly") === "true",
disableUpload: localStorage.getItem("disableUpload") === "true"
folderOnly: false,
readOnly: false,
disableUpload: false,
};
const userPerm = (permissionsData && typeof permissionsData === "object" && permissionsData[user.username]) || defaultPerm;
// Normalize the username key to match server storage (e.g., lowercase)
const usernameKey = user.username.toLowerCase();
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
? permissionsData[usernameKey]
: defaultPerm;
// Create a row for the user.
const row = document.createElement("div");
@@ -789,15 +953,15 @@ function loadUserPermissionsList() {
<div style="display: flex; flex-direction: column; gap: 5px;">
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
User Folder Only
${t("user_folder_only")}
</label>
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
Read Only
${t("read_only")}
</label>
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
Disable Upload
${t("disable_upload")}
</label>
</div>
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
@@ -807,6 +971,6 @@ function loadUserPermissionsList() {
});
})
.catch(() => {
listContainer.innerHTML = "<p>Error loading users.</p>";
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
});
}

View File

@@ -1,4 +1,6 @@
// domUtils.js
import { t } from './i18n.js';
import { openDownloadModal } from './fileActions.js';
// Basic DOM Helpers
export function toggleVisibility(elementId, shouldShow) {
@@ -6,7 +8,7 @@ export function toggleVisibility(elementId, shouldShow) {
if (element) {
element.style.display = shouldShow ? "block" : "none";
} else {
console.error(`Element with id "${elementId}" not found.`);
console.error(t("element_not_found", { id: elementId }));
}
}
@@ -88,23 +90,36 @@ export function showToast(message, duration = 3000) {
export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) {
const safeSearchTerm = escapeHTML(searchTerm);
// Choose the placeholder text based on advanced search mode
const placeholderText = window.advancedSearchEnabled
? t("search_placeholder_advanced")
: t("search_placeholder");
return `
<div class="row align-items-center mb-3">
<div class="col-12 col-md-8 mb-2 mb-md-0">
<div class="input-group">
<!-- Advanced Search Toggle Button -->
<div class="input-group-prepend">
<button id="advancedSearchToggle" class="btn btn-outline-secondary btn-icon" onclick="toggleAdvancedSearch()" title="${window.advancedSearchEnabled ? t("basic_search_tooltip") : t("advanced_search_tooltip")}">
<i class="material-icons">${window.advancedSearchEnabled ? "filter_alt_off" : "filter_alt"}</i>
</button>
</div>
<!-- Search Icon -->
<div class="input-group-prepend">
<span class="input-group-text" id="searchIcon">
<i class="material-icons">search</i>
</span>
</div>
<input type="text" id="searchInput" class="form-control" placeholder="Search files or tag..." value="${safeSearchTerm}" aria-describedby="searchIcon">
<!-- Search Input -->
<input type="text" id="searchInput" class="form-control" placeholder="${placeholderText}" value="${safeSearchTerm}" aria-describedby="searchIcon">
</div>
</div>
<div class="col-12 col-md-4 text-left">
<div class="d-flex justify-content-center justify-content-md-start align-items-center">
<button class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">Prev</button>
<span class="page-indicator">Page ${currentPage} of ${totalPages || 1}</span>
<button class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">Next</button>
<button class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">${t("prev")}</button>
<span class="page-indicator">${t("page")} ${currentPage} ${t("of")} ${totalPages || 1}</span>
<button class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">${t("next")}</button>
</div>
</div>
</div>
@@ -117,12 +132,12 @@ export function buildFileTableHeader(sortOrder) {
<thead>
<tr>
<th class="checkbox-col"><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th>
<th data-column="name" class="sortable-col">File Name ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="modified" class="hide-small sortable-col">Date Modified ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploaded" class="hide-small hide-medium sortable-col">Upload Date ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="size" class="hide-small sortable-col">File Size ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploader" class="hide-small hide-medium sortable-col">Uploader ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th>Actions</th>
<th data-column="name" class="sortable-col">${t("file_name")} ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="modified" class="hide-small sortable-col">${t("date_modified")} ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploaded" class="hide-small hide-medium sortable-col">${t("upload_date")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="size" class="hide-small sortable-col">${t("file_size")} ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploader" class="hide-small hide-medium sortable-col">${t("uploader")} ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th>${t("actions")}</th>
</tr>
</thead>
`;
@@ -164,38 +179,40 @@ export function buildFileTableRow(file, folderPath) {
<td class="hide-small hide-medium nowrap">${safeUploader}</td>
<td>
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
<a class="btn btn-sm btn-success download-btn"
href="download.php?folder=${encodeURIComponent(file.folder || 'root')}&file=${encodeURIComponent(file.name)}"
title="Download">
<button type="button" class="btn btn-sm btn-success download-btn"
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
title="${t('download')}">
<i class="material-icons">file_download</i>
</a>
</button>
${file.editable ? `
<button class="btn btn-sm edit-btn"
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
title="Edit">
title="${t('edit')}">
<i class="material-icons">edit</i>
</button>
` : ""}
${previewButton}
<button class="btn btn-sm btn-warning rename-btn"
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
title="Rename">
title="${t('rename')}">
<i class="material-icons">drive_file_rename_outline</i>
</button>
</div>
</td>
</tr>
`;
`;
}
export function buildBottomControls(itemsPerPageSetting) {
return `
<div class="d-flex align-items-center mt-3 bottom-controls">
<label class="label-inline mr-2 mb-0">Show</label>
<label class="label-inline mr-2 mb-0">${t("show")}</label>
<select class="form-control bottom-select" onchange="changeItemsPerPage(this.value)">
${[10, 20, 50, 100].map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`).join("")}
${[10, 20, 50, 100]
.map(num => `<option value="${num}" ${num === itemsPerPageSetting ? "selected" : ""}>${num}</option>`)
.join("")}
</select>
<span class="items-per-page-text ml-2 mb-0">items per page</span>
<span class="items-per-page-text ml-2 mb-0">${t("items_per_page")}</span>
</div>
`;
}
@@ -221,15 +238,63 @@ export function updateRowHighlight(checkbox) {
}
export function toggleRowSelection(event, fileName) {
// Prevent default text selection when shift is held.
if (event.shiftKey) {
event.preventDefault();
}
// Ignore clicks on interactive elements.
const targetTag = event.target.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button' || targetTag === 'input') {
if (["a", "button", "input"].includes(targetTag)) {
return;
}
// Get the clicked row and its checkbox.
const row = event.currentTarget;
const checkbox = row.querySelector('.file-checkbox');
const checkbox = row.querySelector(".file-checkbox");
if (!checkbox) return;
// Get all rows in the current file list view.
const allRows = Array.from(document.querySelectorAll("#fileList tbody tr"));
// Helper: clear all selections (not used in this updated version).
const clearAllSelections = () => {
allRows.forEach(r => {
const cb = r.querySelector(".file-checkbox");
if (cb) {
cb.checked = false;
updateRowHighlight(cb);
}
});
};
// If the user is holding the Shift key, perform range selection.
if (event.shiftKey) {
// Use the last clicked row as the anchor.
const lastRow = window.lastSelectedFileRow || row;
const currentIndex = allRows.indexOf(row);
const lastIndex = allRows.indexOf(lastRow);
const start = Math.min(currentIndex, lastIndex);
const end = Math.max(currentIndex, lastIndex);
// If neither CTRL nor Meta is pressed, you might choose
// to clear existing selections. For this example we leave existing selections intact.
for (let i = start; i <= end; i++) {
const cb = allRows[i].querySelector(".file-checkbox");
if (cb) {
cb.checked = true;
updateRowHighlight(cb);
}
}
}
// Otherwise, for all non-shift clicks simply toggle the selected state.
else {
checkbox.checked = !checkbox.checked;
updateRowHighlight(checkbox);
}
// Update the anchor row to the row that was clicked.
window.lastSelectedFileRow = row;
updateFileActionButtons();
}
@@ -239,7 +304,7 @@ export function attachEnterKeyListener(modalId, buttonId) {
// Make the modal focusable
modal.setAttribute("tabindex", "-1");
modal.focus();
modal.addEventListener("keydown", function(e) {
modal.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
const btn = document.getElementById(buttonId);

View File

@@ -299,7 +299,7 @@ export function loadSidebarOrder() {
modal = document.createElement('div');
modal.className = 'header-card-modal';
modal.style.position = 'fixed';
modal.style.top = '80px';
modal.style.top = '55px';
modal.style.right = '80px';
modal.style.zIndex = '11000';
// Render the modal but initially keep it hidden.

View File

@@ -2,18 +2,20 @@
import { showToast, attachEnterKeyListener } from './domUtils.js';
import { loadFileList } from './fileListView.js';
import { formatFolderName } from './fileListView.js';
import { t } from './i18n.js';
export function handleDeleteSelected(e) {
e.preventDefault();
e.stopImmediatePropagation();
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
if (checkboxes.length === 0) {
showToast("No files selected.");
showToast("no_files_selected");
return;
}
window.filesToDelete = Array.from(checkboxes).map(chk => chk.value);
document.getElementById("deleteFilesMessage").textContent =
"Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?";
const count = window.filesToDelete.length;
document.getElementById("deleteFilesMessage").textContent = t("confirm_delete_files", { count: count });
document.getElementById("deleteFilesModal").style.display = "block";
attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles");
}
@@ -74,6 +76,81 @@ export function handleDownloadZipSelected(e) {
}, 100);
};
export function openDownloadModal(fileName, folder) {
// Store file details globally for the download confirmation function.
window.singleFileToDownload = fileName;
window.currentFolder = folder || "root";
// Optionally pre-fill the file name input in the modal.
const input = document.getElementById("downloadFileNameInput");
if (input) {
input.value = fileName; // Use file name as-is (or modify if desired)
}
// Show the single file download modal (a new modal element).
document.getElementById("downloadFileModal").style.display = "block";
// Optionally focus the input after a short delay.
setTimeout(() => {
if (input) input.focus();
}, 100);
}
export function confirmSingleDownload() {
// Get the file name from the modal. Users can change it if desired.
let fileName = document.getElementById("downloadFileNameInput").value.trim();
if (!fileName) {
showToast("Please enter a name for the file.");
return;
}
// Hide the download modal.
document.getElementById("downloadFileModal").style.display = "none";
// Show the progress modal (same as in your ZIP download flow).
document.getElementById("downloadProgressModal").style.display = "block";
// Build the URL for download.php using GET parameters.
const folder = window.currentFolder || "root";
const downloadURL = "download.php?folder=" + encodeURIComponent(folder) +
"&file=" + encodeURIComponent(window.singleFileToDownload);
fetch(downloadURL, {
method: "GET",
credentials: "include"
})
.then(response => {
if (!response.ok) {
return response.text().then(text => {
throw new Error("Failed to download file: " + text);
});
}
return response.blob();
})
.then(blob => {
if (!blob || blob.size === 0) {
throw new Error("Received empty file.");
}
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
// Hide the progress modal.
document.getElementById("downloadProgressModal").style.display = "none";
showToast("Download started.");
})
.catch(error => {
// Hide progress modal and show error.
document.getElementById("downloadProgressModal").style.display = "none";
console.error("Error downloading file:", error);
showToast("Error downloading file: " + error.message);
});
}
export function handleExtractZipSelected(e) {
if (e) {
e.preventDefault();
@@ -91,6 +168,16 @@ export function handleExtractZipSelected(e) {
showToast("No zip files selected.");
return;
}
// Change progress modal text to "Extracting files..."
const progressText = document.querySelector("#downloadProgressModal p");
if (progressText) {
progressText.textContent = "Extracting files...";
}
// Show the progress modal.
document.getElementById("downloadProgressModal").style.display = "block";
fetch("extractZip.php", {
method: "POST",
credentials: "include",
@@ -105,6 +192,8 @@ export function handleExtractZipSelected(e) {
})
.then(response => response.json())
.then(data => {
// Hide the progress modal once the request has completed.
document.getElementById("downloadProgressModal").style.display = "none";
if (data.success) {
let toastMessage = "Zip file(s) extracted successfully!";
if (data.extractedFiles && Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
@@ -117,6 +206,8 @@ export function handleExtractZipSelected(e) {
}
})
.catch(error => {
// Hide the progress modal on error.
document.getElementById("downloadProgressModal").style.display = "none";
console.error("Error extracting zip files:", error);
showToast("Error extracting zip files.");
});
@@ -136,6 +227,7 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
// This part remains in your confirmDownloadZip event handler:
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
if (confirmDownloadZip) {
confirmDownloadZip.addEventListener("click", function () {
@@ -147,7 +239,11 @@ document.addEventListener("DOMContentLoaded", function () {
if (!zipName.toLowerCase().endsWith(".zip")) {
zipName += ".zip";
}
// Hide the ZIP name input modal
document.getElementById("downloadZipModal").style.display = "none";
// Show the progress modal here only on confirm
console.log("Download confirmed. Showing progress modal.");
document.getElementById("downloadProgressModal").style.display = "block";
const folder = window.currentFolder || "root";
fetch("downloadZip.php", {
method: "POST",
@@ -179,9 +275,13 @@ document.addEventListener("DOMContentLoaded", function () {
a.click();
window.URL.revokeObjectURL(url);
a.remove();
// Hide the progress modal after download starts
document.getElementById("downloadProgressModal").style.display = "none";
showToast("Download started.");
})
.catch(error => {
// Hide the progress modal on error
document.getElementById("downloadProgressModal").style.display = "none";
console.error("Error downloading zip:", error);
showToast("Error downloading selected files as zip: " + error.message);
});

View File

@@ -1,4 +1,4 @@
// dragDrop.js
// fileDragDrop.js
import { showToast } from './domUtils.js';
import { loadFileList } from './fileListView.js';

View File

@@ -1,6 +1,7 @@
// editor.js
// fileEditor.js
import { escapeHTML, showToast } from './domUtils.js';
import { loadFileList } from './fileListView.js';
import { t } from './i18n.js';
function getModeForFile(fileName) {
const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase();
@@ -73,17 +74,17 @@ export function editFile(fileName, folder) {
modal.classList.add("modal", "editor-modal");
modal.innerHTML = `
<div class="editor-header">
<h3 class="editor-title">Editing: ${escapeHTML(fileName)}</h3>
<h3 class="editor-title">${t("editing")}: ${escapeHTML(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>
<button id="decreaseFont" class="btn btn-sm btn-secondary">${t("decrease_font")}</button>
<button id="increaseFont" class="btn btn-sm btn-secondary">${t("increase_font")}</button>
</div>
<button id="closeEditorX" class="editor-close-btn">&times;</button>
</div>
<textarea id="fileEditor" class="editor-textarea">${escapeHTML(content)}</textarea>
<div class="editor-footer">
<button id="saveBtn" class="btn btn-primary">Save</button>
<button id="closeBtn" class="btn btn-secondary">Close</button>
<button id="saveBtn" class="btn btn-primary">${t("save")}</button>
<button id="closeBtn" class="btn btn-secondary">${t("close")}</button>
</div>
`;
document.body.appendChild(modal);

View File

@@ -12,8 +12,10 @@ import {
toggleRowSelection,
attachEnterKeyListener
} from './domUtils.js';
import { t } from './i18n.js';
import { bindFileListContextMenu } from './fileMenu.js';
import { openDownloadModal } from './fileActions.js';
import { openTagModal, openMultiTagModal } from './fileTags.js';
export let fileData = [];
export let sortOrder = { column: "uploaded", ascending: true };
@@ -22,29 +24,146 @@ window.itemsPerPage = window.itemsPerPage || 10;
window.currentPage = window.currentPage || 1;
window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery"
// -----------------------------
// VIEW MODE TOGGLE BUTTON & Helpers
// -----------------------------
// Global flag for advanced search mode.
window.advancedSearchEnabled = false;
/**
* --- Helper Functions ---
*/
/**
* Convert a file size string (e.g. "456.9KB", "1.2 MB", "1024") into bytes.
*/
function parseSizeToBytes(sizeStr) {
if (!sizeStr) return 0;
let s = sizeStr.trim();
let value = parseFloat(s);
let upper = s.toUpperCase();
if (upper.includes("KB")) {
value *= 1024;
} else if (upper.includes("MB")) {
value *= 1024 * 1024;
} else if (upper.includes("GB")) {
value *= 1024 * 1024 * 1024;
}
return value;
}
/**
* Format the total bytes as a human-readable string.
*/
function formatSize(totalBytes) {
if (totalBytes < 1024) {
return totalBytes + " Bytes";
} else if (totalBytes < 1024 * 1024) {
return (totalBytes / 1024).toFixed(2) + " KB";
} else if (totalBytes < 1024 * 1024 * 1024) {
return (totalBytes / (1024 * 1024)).toFixed(2) + " MB";
} else {
return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB";
}
}
/**
* Build the folder summary HTML using the filtered file list.
*/
function buildFolderSummary(filteredFiles) {
const totalFiles = filteredFiles.length;
const totalBytes = filteredFiles.reduce((sum, file) => {
return sum + parseSizeToBytes(file.size);
}, 0);
const sizeStr = formatSize(totalBytes);
return `<strong>${t('total_files')}:</strong> ${totalFiles} &nbsp;|&nbsp; <strong>${t('total_size')}:</strong> ${sizeStr}`;
}
/**
* --- Advanced Search Toggle ---
* Toggles advanced search mode. When enabled, the search will include additional keys (e.g. "content").
*/
function toggleAdvancedSearch() {
window.advancedSearchEnabled = !window.advancedSearchEnabled;
const advancedBtn = document.getElementById("advancedSearchToggle");
if (advancedBtn) {
advancedBtn.textContent = window.advancedSearchEnabled ? "Basic Search" : "Advanced Search";
}
// Re-run the file table rendering with updated search settings.
renderFileTable(window.currentFolder);
}
/**
* --- Fuse.js Search Helper ---
* Uses Fuse.js to perform a fuzzy search on fileData.
* By default, searches over file name, uploader, and tag names.
* When advanced search is enabled, it also includes the 'content' property.
*/
function searchFiles(searchTerm) {
if (!searchTerm) return fileData;
// Define search keys.
let keys = [
{ name: 'name', weight: 0.1 },
{ name: 'uploader', weight: 0.1 },
{ name: 'tags.name', weight: 0.1 }
];
if (window.advancedSearchEnabled) {
keys.push({ name: 'content', weight: 0.7 });
}
const options = {
keys: keys,
threshold: 0.4,
minMatchCharLength: 2,
ignoreLocation: true
};
const fuse = new Fuse(fileData, options);
let results = fuse.search(searchTerm);
return results.map(result => result.item);
}
/**
* --- VIEW MODE TOGGLE BUTTON & Helpers ---
*/
export function createViewToggleButton() {
let toggleBtn = document.getElementById("toggleViewBtn");
if (!toggleBtn) {
toggleBtn = document.createElement("button");
toggleBtn.id = "toggleViewBtn";
toggleBtn.classList.add("btn", "btn-secondary");
const titleElem = document.getElementById("fileListTitle");
if (titleElem) {
titleElem.parentNode.insertBefore(toggleBtn, titleElem.nextSibling);
toggleBtn.classList.add("btn", "btn-toggleview");
// Set initial icon and tooltip based on current view mode.
if (window.viewMode === "gallery") {
toggleBtn.innerHTML = '<i class="material-icons">view_list</i>';
toggleBtn.title = t("switch_to_table_view");
} else {
toggleBtn.innerHTML = '<i class="material-icons">view_module</i>';
toggleBtn.title = t("switch_to_gallery_view");
}
// Insert the button before the last button in the header.
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons && headerButtons.lastElementChild) {
headerButtons.insertBefore(toggleBtn, headerButtons.lastElementChild);
} else if (headerButtons) {
headerButtons.appendChild(toggleBtn);
}
}
toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View";
toggleBtn.onclick = () => {
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
localStorage.setItem("viewMode", window.viewMode);
loadFileList(window.currentFolder);
toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View";
if (window.viewMode === "gallery") {
toggleBtn.innerHTML = '<i class="material-icons">view_list</i>';
toggleBtn.title = t("switch_to_table_view");
} else {
toggleBtn.innerHTML = '<i class="material-icons">view_module</i>';
toggleBtn.title = t("switch_to_gallery_view");
}
};
return toggleBtn;
}
}
export function formatFolderName(folder) {
if (folder === "root") return "(Root)";
@@ -57,11 +176,9 @@ export function formatFolderName(folder) {
window.toggleRowSelection = toggleRowSelection;
window.updateRowHighlight = updateRowHighlight;
import { openTagModal, openMultiTagModal } from './fileTags.js';
// -----------------------------
// FILE LIST & VIEW RENDERING
// -----------------------------
/**
* --- FILE LIST & VIEW RENDERING ---
*/
export function loadFileList(folderParam) {
const folder = folderParam || "root";
const fileListContainer = document.getElementById("fileList");
@@ -79,8 +196,16 @@ export function loadFileList(folderParam) {
return response.json();
})
.then(data => {
fileListContainer.innerHTML = "";
if (data.files && data.files.length > 0) {
fileListContainer.innerHTML = ""; // Clear loading message.
if (data.files && Object.keys(data.files).length > 0) {
// If the returned "files" is an object instead of an array, transform it.
if (!Array.isArray(data.files)) {
data.files = Object.entries(data.files).map(([name, meta]) => {
meta.name = name;
return meta;
});
}
// Process each file add computed properties.
data.files = data.files.map(file => {
file.fullName = (file.path || file.name).trim().toLowerCase();
file.editable = canEditFile(file.name);
@@ -88,16 +213,42 @@ export function loadFileList(folderParam) {
if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
file.type = "image";
}
// OPTIONAL: For text documents, preload content (if available from backend)
// Example: if (/\.txt|html|md|js|css|json|xml$/i.test(file.name)) { file.content = file.content || ""; }
return file;
});
fileData = data.files;
// Update file summary.
const actionsContainer = document.getElementById("fileListActions");
if (actionsContainer) {
let summaryElem = document.getElementById("fileSummary");
if (!summaryElem) {
summaryElem = document.createElement("div");
summaryElem.id = "fileSummary";
summaryElem.style.float = "right";
summaryElem.style.marginLeft = "auto";
summaryElem.style.marginRight = "60px";
summaryElem.style.fontSize = "0.9em";
actionsContainer.appendChild(summaryElem);
} else {
summaryElem.style.display = "block";
}
summaryElem.innerHTML = buildFolderSummary(fileData);
}
// Render view based on the view mode.
if (window.viewMode === "gallery") {
renderGalleryView(folder);
} else {
renderFileTable(folder);
}
} else {
fileListContainer.textContent = "No files found.";
fileListContainer.textContent = t("no_files_found");
const summaryElem = document.getElementById("fileSummary");
if (summaryElem) {
summaryElem.style.display = "none";
}
updateFileActionButtons();
}
return data.files || [];
@@ -114,17 +265,17 @@ export function loadFileList(folderParam) {
});
}
export function renderFileTable(folder) {
const fileListContainer = document.getElementById("fileList");
/**
* Update renderFileTable so it writes its content into the provided container.
*/
export function renderFileTable(folder, container) {
const fileListContent = container || document.getElementById("fileList");
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
let currentPage = window.currentPage || 1;
const filteredFiles = fileData.filter(file => {
const nameMatch = file.name.toLowerCase().includes(searchTerm);
const tagMatch = file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
return nameMatch || tagMatch;
});
// Use Fuse.js search via our helper function.
const filteredFiles = searchFiles(searchTerm);
const totalFiles = filteredFiles.length;
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
@@ -132,21 +283,23 @@ export function renderFileTable(folder) {
currentPage = totalPages > 0 ? totalPages : 1;
window.currentPage = currentPage;
}
const folderPath = folder === "root"
? "uploads/"
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
// Build the top controls and append the advanced search toggle button.
const topControlsHTML = buildSearchAndPaginationControls({
currentPage,
totalPages,
searchTerm: window.currentSearchTerm || ""
});
const combinedTopHTML = topControlsHTML;
let headerHTML = buildFileTableHeader(sortOrder);
const startIndex = (currentPage - 1) * itemsPerPageSetting;
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
let rowsHTML = "<tbody>";
if (totalFiles > 0) {
filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => {
let rowHTML = buildFileTableRow(file, folderPath);
@@ -160,15 +313,12 @@ export function renderFileTable(folder) {
});
tagBadgesHTML += "</div>";
}
rowHTML = rowHTML.replace(/(<td class="file-name-cell">)(.*?)(<\/td>)/, (match, p1, p2, p3) => {
return p1 + p2 + tagBadgesHTML + p3;
});
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="Share">
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="${t('share')}">
<i class="material-icons">share</i>
</button>$1`);
rowsHTML += rowHTML;
});
} else {
@@ -176,16 +326,18 @@ export function renderFileTable(folder) {
}
rowsHTML += "</tbody></table>";
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
fileListContainer.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML;
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
createViewToggleButton();
// Setup event listeners.
const newSearchInput = document.getElementById("searchInput");
if (newSearchInput) {
newSearchInput.addEventListener("input", debounce(function () {
window.currentSearchTerm = newSearchInput.value;
window.currentPage = 1;
renderFileTable(folder);
renderFileTable(folder, container);
setTimeout(() => {
const freshInput = document.getElementById("searchInput");
if (freshInput) {
@@ -196,21 +348,18 @@ export function renderFileTable(folder) {
}, 0);
}, 300));
}
document.querySelectorAll("table.table thead th[data-column]").forEach(cell => {
cell.addEventListener("click", function () {
const column = this.getAttribute("data-column");
sortFiles(column, folder);
});
});
document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => {
checkbox.addEventListener("change", function (e) {
updateRowHighlight(e.target);
updateFileActionButtons();
});
});
document.querySelectorAll(".share-btn").forEach(btn => {
btn.addEventListener("click", function (e) {
e.stopPropagation();
@@ -223,40 +372,32 @@ export function renderFileTable(folder) {
}
});
});
updateFileActionButtons();
// Add drag-and-drop support for each table row.
document.querySelectorAll("#fileList tbody tr").forEach(row => {
row.setAttribute("draggable", "true");
import('./fileDragDrop.js').then(module => {
row.addEventListener("dragstart", module.fileDragStartHandler);
});
});
// Prevent clicks on these buttons from selecting the row
document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => {
btn.addEventListener("click", e => e.stopPropagation());
});
// rebind context menu
bindFileListContextMenu();
}
export function renderGalleryView(folder) {
const fileListContainer = document.getElementById("fileList");
/**
* Similarly, update renderGalleryView to accept an optional container.
*/
export function renderGalleryView(folder, container) {
const fileListContent = container || document.getElementById("fileList");
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
const filteredFiles = fileData.filter(file => {
return file.name.toLowerCase().includes(searchTerm) ||
(file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm)));
});
// Use Fuse.js search for gallery view as well.
const filteredFiles = searchFiles(searchTerm);
const folderPath = folder === "root"
? "uploads/"
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
const gridStyle = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; padding: 10px;";
let galleryHTML = `<div class="gallery-container" style="${gridStyle}">`;
filteredFiles.forEach((file) => {
let thumbnail;
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
@@ -266,7 +407,6 @@ export function renderGalleryView(folder) {
} else {
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
}
let tagBadgesHTML = "";
if (file.tags && file.tags.length > 0) {
tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`;
@@ -275,44 +415,38 @@ export function renderGalleryView(folder) {
});
tagBadgesHTML += `</div>`;
}
galleryHTML += `<div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;">
<div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')">
${thumbnail}
</div>
<div class="gallery-info" style="margin-top: 5px;">
<span class="gallery-file-name" style="display: block;">${escapeHTML(file.name)}</span>
<span class="gallery-file-name" style="display: block; white-space: normal; overflow-wrap: break-word; word-wrap: break-word;">${escapeHTML(file.name)}</span>
${tagBadgesHTML}
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
<a class="btn btn-sm btn-success download-btn"
href="download.php?folder=${encodeURIComponent(file.folder || 'root')}&file=${encodeURIComponent(file.name)}"
title="Download">
<button type="button" class="btn btn-sm btn-success download-btn"
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
title="${t('download')}">
<i class="material-icons">file_download</i>
</a>
</button>
${file.editable ? `
<button class="btn btn-sm edit-btn" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="Edit">
<button class="btn btn-sm edit-btn" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="${t('Edit')}">
<i class="material-icons">edit</i>
</button>
` : ""}
<button class="btn btn-sm btn-warning rename-btn" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="Rename">
<button class="btn btn-sm btn-warning rename-btn" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="${t('rename')}">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<button class="btn btn-sm btn-secondary share-btn" data-file="${escapeHTML(file.name)}" title="Share">
<button class="btn btn-sm btn-secondary share-btn" data-file="${escapeHTML(file.name)}" title="${t('share')}">
<i class="material-icons">share</i>
</button>
</div>
</div>
</div>`;
});
galleryHTML += "</div>";
fileListContainer.innerHTML = galleryHTML;
fileListContent.innerHTML = galleryHTML;
createViewToggleButton();
updateFileActionButtons();
// Bind share button clicks
document.querySelectorAll(".share-btn").forEach(btn => {
btn.addEventListener("click", e => {
e.stopPropagation();
@@ -412,8 +546,8 @@ window.changeItemsPerPage = function (newCount) {
};
// fileListView.js (bottom)
window.loadFileList = loadFileList;
window.renderFileTable = renderFileTable;
window.renderGalleryView = renderGalleryView;
window.sortFiles = sortFiles;
window.toggleAdvancedSearch = toggleAdvancedSearch;

View File

@@ -1,10 +1,11 @@
// contextMenu.js
// fileMenu.js
import { updateRowHighlight, showToast } from './domUtils.js';
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile } from './fileActions.js';
import { previewFile } from './filePreview.js';
import { editFile } from './fileEditor.js';
import { canEditFile, fileData } from './fileListView.js';
import { openTagModal, openMultiTagModal } from './fileTags.js';
import { t } from './i18n.js';
export function showFileContextMenu(x, y, menuItems) {
let menu = document.getElementById("fileContextMenu");
@@ -74,22 +75,22 @@ export function fileListContextMenuHandler(e) {
const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value);
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")); } }
{ label: t("delete_selected"), action: () => { handleDeleteSelected(new Event("click")); } },
{ label: t("copy_selected"), action: () => { handleCopySelected(new Event("click")); } },
{ label: t("move_selected"), action: () => { handleMoveSelected(new Event("click")); } },
{ label: t("download_zip"), action: () => { handleDownloadZipSelected(new Event("click")); } }
];
if (selected.some(name => name.toLowerCase().endsWith(".zip"))) {
menuItems.push({
label: "Extract Zip",
label: t("extract_zip"),
action: () => { handleExtractZipSelected(new Event("click")); }
});
}
if (selected.length > 1) {
menuItems.push({
label: "Tag Selected",
label: t("tag_selected"),
action: () => {
const files = fileData.filter(f => selected.includes(f.name));
openMultiTagModal(files);
@@ -100,7 +101,7 @@ export function fileListContextMenuHandler(e) {
const file = fileData.find(f => f.name === selected[0]);
menuItems.push({
label: "Preview",
label: t("preview"),
action: () => {
const folder = window.currentFolder || "root";
const folderPath = folder === "root"
@@ -112,18 +113,18 @@ export function fileListContextMenuHandler(e) {
if (canEditFile(file.name)) {
menuItems.push({
label: "Edit",
label: t("edit"),
action: () => { editFile(selected[0], window.currentFolder); }
});
}
menuItems.push({
label: "Rename",
label: t("rename"),
action: () => { renameFile(selected[0], window.currentFolder); }
});
menuItems.push({
label: "Tag File",
label: t("tag_file"),
action: () => { openTagModal(file); }
});
}

View File

@@ -1,6 +1,7 @@
// filePreview.js
import { escapeHTML, showToast } from './domUtils.js';
import { fileData } from './fileListView.js';
import { t } from './i18n.js';
export function openShareModal(file, folder) {
const existing = document.getElementById("shareModal");
@@ -12,11 +13,11 @@ export function openShareModal(file, folder) {
modal.innerHTML = `
<div class="modal-content share-modal-content" style="width: 600px; max-width:90vw;">
<div class="modal-header">
<h3>Share File: ${escapeHTML(file.name)}</h3>
<h3>${t("share_file")}: ${escapeHTML(file.name)}</h3>
<span class="close-image-modal" id="closeShareModal" title="Close">&times;</span>
</div>
<div class="modal-body">
<p>Set Expiration:</p>
<p>${t("set_expiration")}</p>
<select id="shareExpiration">
<option value="30">30 minutes</option>
<option value="60" selected>60 minutes</option>
@@ -25,14 +26,14 @@ export function openShareModal(file, folder) {
<option value="240">240 minutes</option>
<option value="1440">1 Day</option>
</select>
<p>Password (optional):</p>
<input type="text" id="sharePassword" placeholder="No password by default" style="width: 100%;"/>
<p>${t("password_optional")}</p>
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
<br>
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">Generate Share Link</button>
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
<p>Shareable Link:</p>
<p>${t("shareable_link")}</p>
<input type="text" id="shareLinkInput" readonly style="width:100%;"/>
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">Copy Link</button>
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">${t("copy_link")}</button>
</div>
</div>
</div>

View File

@@ -4,6 +4,7 @@
// updating the file row display with tag badges,
// filtering the file list by tag, and persisting tag data.
import { escapeHTML } from './domUtils.js';
import { t } from './i18n.js';
export function openTagModal(file) {
// Create the modal element.
@@ -13,14 +14,14 @@ export function openTagModal(file) {
modal.innerHTML = `
<div class="modal-content" style="width: 400px; max-width:90vw;">
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
<h3 style="margin:0;">Tag File: ${file.name}</h3>
<h3 style="margin:0;">${t("tag_file")}: ${file.name}</h3>
<span id="closeTagModal" style="cursor:pointer; font-size:24px;">&times;</span>
</div>
<div class="modal-body" style="margin-top:10px;">
<label for="tagNameInput">Tag Name:</label>
<label for="tagNameInput">${t("tag_name")}</label>
<input type="text" id="tagNameInput" placeholder="Enter tag name" style="width:100%; padding:5px;"/>
<br><br>
<label for="tagColorInput">Tag Color:</label>
<label for="tagColorInput">${t("tag_name")}</label>
<input type="color" id="tagColorInput" value="#ff0000" style="width:100%; padding:5px;"/>
<br><br>
<div id="customTagDropdown" style="max-height:150px; overflow-y:auto; border:1px solid #ccc; margin-top:5px; padding:5px;">
@@ -28,7 +29,7 @@ export function openTagModal(file) {
</div>
<br>
<div style="text-align:right;">
<button id="saveTagBtn" class="btn btn-primary">Save Tag</button>
<button id="saveTagBtn" class="btn btn-primary">${t("save_tag")}</button>
</div>
<div id="currentTags" style="margin-top:10px; font-size:0.9em;">
<!-- Existing tags will be listed here -->
@@ -304,7 +305,7 @@ if (localStorage.getItem('globalTags')) {
// New function to load global tags from the server's persistent JSON.
export function loadGlobalTags() {
fetch("metadata/createdTags.json", { credentials: "include" })
fetch("getFileTag.php", { credentials: "include" })
.then(response => {
if (!response.ok) {
// If the file doesn't exist, assume there are no global tags.

View File

@@ -2,6 +2,8 @@
import { loadFileList } from './fileListView.js';
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
import { t } from './i18n.js';
import { openFolderShareModal } from './folderShareModal.js';
/* ----------------------
Helper Functions (Data/State)
@@ -112,7 +114,7 @@ function breadcrumbClickHandler(e) {
// Update the container with sanitized breadcrumbs.
const container = document.getElementById("fileListTitle");
const sanitizedBreadcrumb = DOMPurify.sanitize(renderBreadcrumb(folder));
container.innerHTML = "Files in (" + sanitizedBreadcrumb + ")";
container.innerHTML = t("files_in") + " (" + sanitizedBreadcrumb + ")";
expandTreePath(folder);
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
@@ -417,7 +419,7 @@ export async function loadFolderTree(selectedFolder) {
localStorage.setItem("lastOpenedFolder", window.currentFolder);
const titleEl = document.getElementById("fileListTitle");
titleEl.innerHTML = "Files in (" + renderBreadcrumb(window.currentFolder) + ")";
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")";
setupBreadcrumbDelegation();
loadFileList(window.currentFolder);
@@ -441,7 +443,7 @@ export async function loadFolderTree(selectedFolder) {
window.currentFolder = selected;
localStorage.setItem("lastOpenedFolder", selected);
const titleEl = document.getElementById("fileListTitle");
titleEl.innerHTML = "Files in (" + renderBreadcrumb(selected) + ")";
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(selected) + ")";
setupBreadcrumbDelegation();
loadFileList(selected);
});
@@ -734,18 +736,22 @@ function folderManagerContextMenuHandler(e) {
target.classList.add("selected");
const menuItems = [
{
label: "Create Folder",
label: t("create_folder"),
action: () => {
document.getElementById("createFolderModal").style.display = "block";
document.getElementById("newFolderName").focus();
}
},
{
label: "Rename Folder",
label: t("rename_folder"),
action: () => { openRenameFolderModal(); }
},
{
label: "Delete Folder",
label: t("folder_share"),
action: () => { openFolderShareModal(); }
},
{
label: t("delete_folder"),
action: () => { openDeleteFolderModal(); }
}
];
@@ -784,4 +790,21 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
document.addEventListener("DOMContentLoaded", function () {
const shareFolderBtn = document.getElementById("shareFolderBtn");
if (shareFolderBtn) {
shareFolderBtn.addEventListener("click", () => {
const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to share.");
return;
}
// Call the folder share modal from the module.
openFolderShareModal(selectedFolder);
});
} else {
console.warn("shareFolderBtn element not found in the DOM.");
}
});
bindFolderManagerContextMenu();

107
js/folderShareModal.js Normal file
View File

@@ -0,0 +1,107 @@
// folderShareModal.js
import { escapeHTML, showToast } from './domUtils.js';
import { t } from './i18n.js';
export function openFolderShareModal(folder) {
// Remove any existing folder share modal
const existing = document.getElementById("folderShareModal");
if (existing) existing.remove();
// Create the modal container
const modal = document.createElement("div");
modal.id = "folderShareModal";
modal.classList.add("modal");
modal.innerHTML = `
<div class="modal-content share-modal-content" style="width: 600px; max-width: 90vw;">
<div class="modal-header">
<h3>${t("share_folder")}: ${escapeHTML(folder)}</h3>
<span class="close-image-modal" id="closeFolderShareModal" title="Close">&times;</span>
</div>
<div class="modal-body">
<p>${t("set_expiration")}</p>
<select id="folderShareExpiration">
<option value="30">30 ${t("minutes")}</option>
<option value="60" selected>60 ${t("minutes")}</option>
<option value="120">120 ${t("minutes")}</option>
<option value="180">180 ${t("minutes")}</option>
<option value="240">240 ${t("minutes")}</option>
<option value="1440">1 ${t("day")}</option>
</select>
<p>${t("password_optional")}</p>
<input type="text" id="folderSharePassword" placeholder="${t("enter_password")}" style="width: 100%;"/>
<br>
<label>
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
</label>
<br><br>
<button id="generateFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 10px;">${t("generate_share_link")}</button>
<div id="folderShareLinkDisplay" style="margin-top: 10px; display: none;">
<p>${t("shareable_link")}</p>
<input type="text" id="folderShareLinkInput" readonly style="width: 100%;"/>
<button id="copyFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 5px;">${t("copy_link")}</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = "block";
// Close button handler
document.getElementById("closeFolderShareModal").addEventListener("click", () => {
modal.remove();
});
// Handler for generating the share link
document.getElementById("generateFolderShareLinkBtn").addEventListener("click", () => {
const expiration = document.getElementById("folderShareExpiration").value;
const password = document.getElementById("folderSharePassword").value;
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
// Retrieve the CSRF token from the meta tag.
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
if (!csrfToken) {
showToast(t("csrf_error"));
return;
}
// Post to the createFolderShareLink endpoint.
fetch("/createFolderShareLink.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({
folder: folder,
expirationMinutes: parseInt(expiration, 10),
password: password,
allowUpload: allowUpload
})
})
.then(response => response.json())
.then(data => {
if (data.token && data.link) {
const shareUrl = data.link;
const displayDiv = document.getElementById("folderShareLinkDisplay");
const inputField = document.getElementById("folderShareLinkInput");
inputField.value = shareUrl;
displayDiv.style.display = "block";
showToast(t("share_link_generated"));
} else {
showToast(t("error_generating_share_link") + ": " + (data.error || t("unknown_error")));
}
})
.catch(err => {
console.error("Error generating folder share link:", err);
showToast(t("error_generating_share_link") + ": " + (err.error || t("unknown_error")));
});
});
// Copy share link button handler
document.getElementById("copyFolderShareLinkBtn").addEventListener("click", () => {
const input = document.getElementById("folderShareLinkInput");
input.select();
document.execCommand("copy");
showToast(t("link_copied"));
});
}

962
js/i18n.js Normal file
View File

@@ -0,0 +1,962 @@
/* i18n.js */
const translations = {
en: {
"please_log_in_to_continue": "Please log in to continue.",
"no_files_selected": "No files selected.",
"confirm_delete_files": "Are you sure you want to delete {count} selected file(s)?",
"element_not_found": "Element with id \"{id}\" not found.",
"search_placeholder": "Search files, tags, & uploader...",
"search_placeholder_advanced": "Advanced Search: files, tags, uploader & content...",
"basic_search_tooltip": "Basic Search: Search by file name, tags, and uploader.",
"advanced_search_tooltip": "Advanced Search: Includes file content, in addition to file name, tags, and uploader.",
"file_name": "File Name",
"date_modified": "Date Modified",
"upload_date": "Upload Date",
"file_size": "File Size",
"uploader": "Uploader",
"enter_totp_code": "Enter TOTP Code",
"use_recovery_code_instead": "Use Recovery Code instead",
"enter_recovery_code": "Enter Recovery Code",
"editing": "Editing",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Save",
"close": "Close",
"no_files_found": "No files found.",
"switch_to_table_view": "Switch to Table View",
"switch_to_gallery_view": "Switch to Gallery View",
"share_file": "Share File",
"set_expiration": "Set Expiration:",
"password_optional": "Password (optional):",
"generate_share_link": "Generate Share Link",
"shareable_link": "Shareable Link:",
"copy_link": "Copy Link",
"tag_file": "Tag File",
"tag_name": "Tag Name:",
"tag_color": "Tag Color:",
"save_tag": "Save Tag",
"light_mode": "Light Mode",
"dark_mode": "Dark Mode",
"upload_instruction": "Drop files/folders here or click 'Choose files'",
"no_files_selected_default": "No files selected",
"choose_files": "Choose files",
"delete_selected": "Delete Selected",
"copy_selected": "Copy Selected",
"move_selected": "Move Selected",
"tag_selected": "Tag Selected",
"download_zip": "Download Zip",
"extract_zip": "Extract Zip",
"preview": "Preview",
"edit": "Edit",
"rename": "Rename",
"trash_empty": "Trash is empty.",
"no_trash_selected": "No trash items selected for restore.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Logout",
"change_password": "Change Password",
"restore_text": "Restore or",
"delete_text": "Delete Trash Items",
"restore_selected": "Restore Selected",
"restore_all": "Restore All",
"delete_selected_trash": "Delete Selected",
"delete_all": "Delete All",
"upload_header": "Upload Files/Folders",
// Folder Management keys:
"folder_navigation": "Folder Navigation & Management",
"create_folder": "Create Folder",
"create_folder_title": "Create Folder",
"enter_folder_name": "Enter folder name",
"cancel": "Cancel",
"create": "Create",
"rename_folder": "Rename Folder",
"rename_folder_title": "Rename Folder",
"rename_folder_placeholder": "Enter new folder name",
"delete_folder": "Delete Folder",
"delete_folder_title": "Delete Folder",
"delete_folder_message": "Are you sure you want to delete this folder?",
"folder_help": "Folder Help",
"folder_help_item_1": "Click on a folder in the tree to view its files.",
"folder_help_item_2": "Use [-] to collapse and [+] to expand folders.",
"folder_help_item_3": "Select a folder and click \"Create Folder\" to add a subfolder.",
"folder_help_item_4": "To rename or delete a folder, select it and then click the appropriate button.",
// File List keys:
"actions": "Actions",
"file_list_title": "Files in (Root)",
"files_in": "Files in",
"delete_files": "Delete Files",
"delete_selected_files_title": "Delete Selected Files",
"delete_files_message": "Are you sure you want to delete the selected files?",
"copy_files": "Copy Files",
"copy_files_title": "Copy Selected Files",
"copy_files_message": "Select a target folder for copying the selected files:",
"move_files": "Move Files",
"move_files_title": "Move Selected Files",
"move_files_message": "Select a target folder for moving the selected files:",
"move": "Move",
"extract_zip_button": "Extract Zip",
"download_zip_title": "Download Selected Files as Zip",
"download_zip_prompt": "Enter a name for the zip file:",
"zip_placeholder": "files.zip",
"share": "Share",
"total_files": "Total Files",
"total_size": "Total Size",
"prev": "Prev",
"next": "Next",
"page": "Page",
"of": "of",
// Login Form keys:
"login": "Login",
"remember_me": "Remember me",
"login_oidc": "Login with OIDC",
"basic_http_login": "Use Basic HTTP Login",
// Change Password keys:
"change_password_title": "Change Password",
"old_password": "Old Password",
"new_password": "New Password",
"confirm_new_password": "Confirm New Password",
// Add User keys:
"create_new_user_title": "Create New User",
"username": "Username:",
"password": "Password:",
"enter_password": "Password",
"preparing_download": "Preparing your download...",
"download_file": "Download File",
"confirm_or_change_filename": "Confirm or change the download file name:",
"filename": "Filename",
"cancel": "Cancel",
"download": "Download",
"grant_admin": "Grant Admin Access",
"save_user": "Save User",
// Remove User keys:
"remove_user_title": "Remove User",
"select_user_remove": "Select a user to remove:",
"delete_user": "Delete User",
// Rename File keys:
"rename_file_title": "Rename File",
"rename_file_placeholder": "Enter new file name",
// Folder Share
"share_folder": "Share Folder",
"allow_uploads": "Allow Uploads",
"share_link_generated": "Share Link Generated",
"error_generating_share_link": "Error Generating Share Link",
// Folder
"folder_share": "Share Folder",
// Custom Confirm Modal keys:
"yes": "Yes",
"no": "No",
"unsaved_changes_confirm": "You have unsaved changes. Are you sure you want to close without saving?",
"delete": "Delete",
"download": "Download",
"upload": "Upload",
"copy": "Copy",
"extract": "Extract",
"user": "User:",
"unknown_error": "Unknown Error",
"link_copied": "Link Copied to Clipboard",
"minutes": "minutes",
"hours": "hours",
"days": "days",
"weeks": "weeks",
"months": "months",
"seconds": "seconds",
// Dark Mode Toggle
"dark_mode_toggle": "Dark Mode",
"light_mode_toggle": "Light Mode",
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
"admin_panel": "Admin Panel",
"user_panel": "User Panel",
"trash_restore_delete": "Trash Restore/Delete",
"totp_settings": "TOTP Settings",
"enable_totp": "Enable TOTP",
"language": "Language",
"select_language": "Select Language",
"english": "English",
"spanish": "Spanish",
"french": "French",
"german": "German",
"use_totp_code_instead": "Use TOTP Code instead",
"submit_recovery_code": "Submit Recovery Code",
"please_enter_recovery_code": "Please enter your recovery code.",
"recovery_code_verification_failed": "Recovery code verification failed",
"error_verifying_recovery_code": "Error verifying recovery code",
"totp_verification_failed": "TOTP verification failed",
"error_verifying_totp_code": "Error verifying TOTP code",
"totp_setup": "TOTP Setup",
"scan_qr_code": "Scan this QR code with your authenticator app.",
"enter_totp_confirmation": "Enter the 6-digit code from your app to confirm setup:",
"confirm": "Confirm",
"please_enter_valid_code": "Please enter a valid 6-digit code.",
"totp_enabled_successfully": "TOTP successfully enabled.",
"error_generating_recovery_code": "Error generating recovery code",
"error_loading_qr_code": "Error loading QR code.",
"error_disabling_totp_setting": "Error disabling TOTP setting",
"user_management": "User Management",
"add_user": "Add User",
"remove_user": "Remove User",
"user_permissions": "User Permissions",
"oidc_configuration": "OIDC Configuration",
"oidc_provider_url": "OIDC Provider URL",
"oidc_client_id": "OIDC Client ID",
"oidc_client_secret": "OIDC Client Secret",
"oidc_redirect_uri": "OIDC Redirect URI",
"global_totp_settings": "Global TOTP Settings",
"global_otpauth_url": "Global OTPAuth URL",
"login_options": "Login Options",
"disable_login_form": "Disable Login Form",
"disable_basic_http_auth": "Disable Basic HTTP Auth",
"disable_oidc_login": "Disable OIDC Login",
"save_settings": "Save Settings",
"at_least_one_login_method": "At least one login method must remain enabled.",
"settings_updated_successfully": "Settings updated successfully.",
"error_updating_settings": "Error updating settings",
"user_permissions_updated_successfully": "User permissions updated successfully.",
"error_updating_permissions": "Error updating permissions",
"no_users_found": "No users found.",
"user_folder_only": "User Folder Only",
"read_only": "Read Only",
"disable_upload": "Disable Upload",
"error_loading_users": "Error loading users",
"save_permissions": "Save Permissions",
"your_recovery_code": "Your Recovery Code",
"please_save_recovery_code": "Please save this code securely. It will not be shown again and can only be used once.",
"ok": "OK",
"show": "Show",
"items_per_page": "items per page"
},
es: {
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
"no_files_selected": "No se han seleccionado archivos.",
"confirm_delete_files": "¿Está seguro de que desea eliminar {count} archivo(s) seleccionado(s)?",
"element_not_found": "Elemento con id \"{id}\" no encontrado.",
"search_placeholder": "Buscar archivos, etiquetas y cargador...",
"search_placeholder_advanced": "Búsqueda avanzada: archivos, etiquetas, cargador y contenido...",
"basic_search_tooltip": "Búsqueda básica: Buscar por nombre de archivo, etiquetas y cargador.",
"advanced_search_tooltip": "Búsqueda avanzada: Incluye el contenido del archivo, además del nombre, etiquetas y cargador.",
"file_name": "Nombre del archivo",
"date_modified": "Fecha de modificación",
"upload_date": "Fecha de carga",
"file_size": "Tamaño del archivo",
"uploader": "Cargado por",
"enter_totp_code": "Ingrese el código TOTP",
"use_recovery_code_instead": "Usar código de recuperación en su lugar",
"enter_recovery_code": "Ingrese el código de recuperación",
"editing": "Editando",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Guardar",
"close": "Cerrar",
"no_files_found": "No se encontraron archivos.",
"switch_to_table_view": "Cambiar a vista de tabla",
"switch_to_gallery_view": "Cambiar a vista de galería",
"share_file": "Compartir archivo",
"set_expiration": "Establecer vencimiento:",
"password_optional": "Contraseña (opcional):",
"generate_share_link": "Generar enlace para compartir",
"shareable_link": "Enlace para compartir:",
"copy_link": "Copiar enlace",
"tag_file": "Etiquetar archivo",
"tag_name": "Nombre de la etiqueta:",
"tag_color": "Color de la etiqueta:",
"save_tag": "Guardar etiqueta",
"light_mode": "Modo claro",
"dark_mode": "Modo oscuro",
"upload_instruction": "Suelte archivos/carpetas aquí o haga clic en 'Elegir archivos'",
"no_files_selected_default": "No se han seleccionado archivos",
"choose_files": "Elegir archivos",
"delete_selected": "Eliminar seleccionados",
"copy_selected": "Copiar seleccionados",
"move_selected": "Mover seleccionados",
"tag_selected": "Etiquetar seleccionados",
"download_zip": "Descargar Zip",
"extract_zip": "Extraer Zip",
"preview": "Vista previa",
"edit": "Editar",
"rename": "Renombrar",
"trash_empty": "La papelera está vacía.",
"no_trash_selected": "No se han seleccionado elementos de la papelera para restaurar.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Cerrar sesión",
"change_password": "Cambiar contraseña",
"restore_text": "Restaurar o",
"delete_text": "Eliminar elementos de la papelera",
"restore_selected": "Restaurar seleccionados",
"restore_all": "Restaurar todo",
"delete_selected_trash": "Eliminar seleccionados",
"delete_all": "Eliminar todo",
"upload_header": "Cargar archivos/carpetas",
// Folder Management keys:
"folder_navigation": "Navegación y gestión de carpetas",
"create_folder": "Crear carpeta",
"create_folder_title": "Crear carpeta",
"enter_folder_name": "Ingrese el nombre de la carpeta",
"cancel": "Cancelar",
"create": "Crear",
"rename_folder": "Renombrar carpeta",
"rename_folder_title": "Renombrar carpeta",
"rename_folder_placeholder": "Ingrese el nuevo nombre de la carpeta",
"delete_folder": "Eliminar carpeta",
"delete_folder_title": "Eliminar carpeta",
"delete_folder_message": "¿Está seguro de que desea eliminar esta carpeta?",
"folder_help": "Ayuda de carpetas",
"folder_help_item_1": "Haga clic en una carpeta en el árbol para ver sus archivos.",
"folder_help_item_2": "Utilice [-] para contraer y [+] para expandir las carpetas.",
"folder_help_item_3": "Seleccione una carpeta y haga clic en \"Crear carpeta\" para agregar una subcarpeta.",
"folder_help_item_4": "Para renombrar o eliminar una carpeta, selecciónela y luego haga clic en el botón correspondiente.",
// File List keys:
"file_list_title": "Archivos en (Raíz)",
"files_in": "Archivos en",
"delete_files": "Eliminar archivos",
"delete_selected_files_title": "Eliminar archivos seleccionados",
"delete_files_message": "¿Está seguro de que desea eliminar los archivos seleccionados?",
"copy_files": "Copiar archivos",
"copy_files_title": "Copiar archivos seleccionados",
"copy_files_message": "Seleccione una carpeta destino para copiar los archivos seleccionados:",
"move_files": "Mover archivos",
"move_files_title": "Mover archivos seleccionados",
"move_files_message": "Seleccione una carpeta destino para mover los archivos seleccionados:",
"move": "Mover",
"extract_zip_button": "Extraer Zip",
"download_zip_title": "Descargar archivos seleccionados en un Zip",
"download_zip_prompt": "Ingrese un nombre para el archivo Zip:",
"zip_placeholder": "files.zip",
// Login Form keys:
"login": "Iniciar sesión",
"remember_me": "Recuérdame",
"login_oidc": "Iniciar sesión con OIDC",
"basic_http_login": "Usar autenticación HTTP básica",
// Change Password keys:
"change_password_title": "Cambiar contraseña",
"old_password": "Contraseña antigua",
"new_password": "Nueva contraseña",
"confirm_new_password": "Confirmar nueva contraseña",
// Add User keys:
"create_new_user_title": "Crear nuevo usuario",
"username": "Usuario:",
"password": "Contraseña:",
"enter_password": "Contraseña",
"preparing_download": "Preparando su descarga...",
"download_file": "Descargar Archivo",
"confirm_or_change_filename": "Confirme o cambie el nombre del archivo a descargar:",
"filename": "Nombre de archivo",
"cancel": "Cancelar",
"download": "Descargar",
"grant_admin": "Otorgar acceso de administrador",
"save_user": "Guardar usuario",
// Remove User keys:
"remove_user_title": "Eliminar usuario",
"select_user_remove": "Seleccione un usuario para eliminar:",
"delete_user": "Eliminar usuario",
// Rename File keys:
"rename_file_title": "Renombrar archivo",
"rename_file_placeholder": "Ingrese el nuevo nombre del archivo",
// Folder Share
"share_folder": "Compartir carpeta",
"allow_uploads": "Permitir cargas",
"share_link_generated": "Enlace para compartir generado",
"error_generating_share_link": "Error al generar el enlace para compartir",
// Folder
"folder_share": "Compartir carpeta",
// Custom Confirm Modal keys:
"yes": "Sí",
"no": "No",
"unsaved_changes_confirm": "Tiene cambios sin guardar. ¿Está seguro de que desea cerrar sin guardar?",
"delete": "Eliminar",
"download": "Descargar",
"upload": "Cargar",
"copy": "Copiar",
"extract": "Extraer",
"user": "Usuario:",
"unknown_error": "Error desconocido",
"link_copied": "Enlace copiado al portapapeles",
"minutes": "minutos",
"hours": "horas",
"days": "días",
"weeks": "semanas",
"months": "meses",
"seconds": "segundos",
// Dark Mode Toggle
"dark_mode_toggle": "Modo oscuro",
"light_mode_toggle": "Modo claro",
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
"admin_panel": "Panel de Administración",
"user_panel": "Panel de Usuario",
"totp_settings": "Configuración TOTP",
"enable_totp": "Activar TOTP",
"language": "Idioma",
"select_language": "Seleccionar idioma",
"english": "Inglés",
"spanish": "Español",
"french": "Francés",
"german": "Alemán",
"use_totp_code_instead": "Usar código TOTP en su lugar",
"submit_recovery_code": "Enviar código de recuperación",
"please_enter_recovery_code": "Por favor, ingrese su código de recuperación.",
"recovery_code_verification_failed": "La verificación del código de recuperación falló",
"error_verifying_recovery_code": "Error al verificar el código de recuperación",
"totp_verification_failed": "La verificación TOTP falló",
"error_verifying_totp_code": "Error al verificar el código TOTP",
"totp_setup": "Configuración TOTP",
"scan_qr_code": "Escanee este código QR con su aplicación de autenticación.",
"enter_totp_confirmation": "Ingrese el código de 6 dígitos de su aplicación para confirmar la configuración:",
"confirm": "Confirmar",
"please_enter_valid_code": "Por favor, ingrese un código válido de 6 dígitos.",
"totp_enabled_successfully": "TOTP activado con éxito.",
"error_generating_recovery_code": "Error al generar el código de recuperación",
"error_loading_qr_code": "Error al cargar el código QR.",
"error_disabling_totp_setting": "Error al desactivar la configuración TOTP",
"user_management": "Gestión de Usuarios",
"add_user": "Agregar usuario",
"remove_user": "Eliminar usuario",
"user_permissions": "Permisos de Usuario",
"oidc_configuration": "Configuración OIDC",
"oidc_provider_url": "URL del Proveedor OIDC",
"oidc_client_id": "ID del Cliente OIDC",
"oidc_client_secret": "Secreto del Cliente OIDC",
"oidc_redirect_uri": "URI de Redirección OIDC",
"global_totp_settings": "Configuración Global TOTP",
"global_otpauth_url": "URL Global OTPAuth",
"login_options": "Opciones de inicio de sesión",
"disable_login_form": "Desactivar formulario de inicio de sesión",
"disable_basic_http_auth": "Desactivar autenticación HTTP básica",
"disable_oidc_login": "Desactivar inicio de sesión OIDC",
"save_settings": "Guardar configuración",
"at_least_one_login_method": "Al menos un método de inicio de sesión debe permanecer habilitado.",
"settings_updated_successfully": "Configuración actualizada con éxito.",
"error_updating_settings": "Error al actualizar la configuración",
"user_permissions_updated_successfully": "Permisos de usuario actualizados con éxito.",
"error_updating_permissions": "Error al actualizar los permisos",
"no_users_found": "No se encontraron usuarios.",
"user_folder_only": "Solo carpeta de usuario",
"read_only": "Solo lectura",
"disable_upload": "Desactivar carga",
"error_loading_users": "Error al cargar usuarios",
"save_permissions": "Guardar permisos",
"your_recovery_code": "Su código de recuperación",
"please_save_recovery_code": "Por favor, guarde este código de forma segura. No se mostrará de nuevo y solo podrá usarse una vez.",
"ok": "OK"
},
fr: {
"please_log_in_to_continue": "Veuillez vous connecter pour continuer.",
"no_files_selected": "Aucun fichier sélectionné.",
"confirm_delete_files": "Êtes-vous sûr de vouloir supprimer {count} fichier(s) sélectionné(s) ?",
"element_not_found": "Élément avec l'id \"{id}\" non trouvé.",
"search_placeholder": "Rechercher des fichiers, des balises et l'uploader...",
"search_placeholder_advanced": "Recherche avancée : fichiers, balises, uploader et contenu...",
"basic_search_tooltip": "Recherche basique : rechercher par nom de fichier, balises et uploader.",
"advanced_search_tooltip": "Recherche avancée : inclut le contenu du fichier, en plus du nom, des balises et de l'uploader.",
"file_name": "Nom du fichier",
"date_modified": "Date de modification",
"upload_date": "Date de téléchargement",
"file_size": "Taille du fichier",
"uploader": "Uploader",
"enter_totp_code": "Entrez le code TOTP",
"use_recovery_code_instead": "Utilisez le code de récupération à la place",
"enter_recovery_code": "Entrez le code de récupération",
"editing": "Modification",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Enregistrer",
"close": "Fermer",
"no_files_found": "Aucun fichier trouvé.",
"switch_to_table_view": "Passer en vue tableau",
"switch_to_gallery_view": "Passer en vue galerie",
"share_file": "Partager le fichier",
"set_expiration": "Définir l'expiration :",
"password_optional": "Mot de passe (facultatif) :",
"generate_share_link": "Générer le lien de partage",
"shareable_link": "Lien partageable :",
"copy_link": "Copier le lien",
"tag_file": "Étiqueter le fichier",
"tag_name": "Nom de l'étiquette :",
"tag_color": "Couleur de l'étiquette :",
"save_tag": "Enregistrer l'étiquette",
"light_mode": "Mode clair",
"dark_mode": "Mode sombre",
"upload_instruction": "Déposez des fichiers/dossiers ici ou cliquez sur 'Choisir des fichiers'",
"no_files_selected_default": "Aucun fichier sélectionné",
"choose_files": "Choisir des fichiers",
"delete_selected": "Supprimer la sélection",
"copy_selected": "Copier la sélection",
"move_selected": "Déplacer la sélection",
"tag_selected": "Étiqueter la sélection",
"download_zip": "Télécharger le Zip",
"extract_zip": "Extraire le Zip",
"preview": "Aperçu",
"edit": "Modifier",
"rename": "Renommer",
"trash_empty": "La corbeille est vide.",
"no_trash_selected": "Aucun élément de la corbeille sélectionné pour restauration.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Déconnexion",
"change_password": "Changer le mot de passe",
"restore_text": "Restaurer ou",
"delete_text": "Supprimer les éléments de la corbeille",
"restore_selected": "Restaurer la sélection",
"restore_all": "Restaurer tout",
"delete_selected_trash": "Supprimer la sélection",
"delete_all": "Supprimer tout",
"upload_header": "Téléverser des fichiers/dossiers",
// Folder Management keys:
"folder_navigation": "Navigation et gestion des dossiers",
"create_folder": "Créer un dossier",
"create_folder_title": "Créer un dossier",
"enter_folder_name": "Entrez le nom du dossier",
"cancel": "Annuler",
"create": "Créer",
"rename_folder": "Renommer le dossier",
"rename_folder_title": "Renommer le dossier",
"rename_folder_placeholder": "Entrez le nouveau nom du dossier",
"delete_folder": "Supprimer le dossier",
"delete_folder_title": "Supprimer le dossier",
"delete_folder_message": "Êtes-vous sûr de vouloir supprimer ce dossier ?",
"folder_help": "Aide des dossiers",
"folder_help_item_1": "Cliquez sur un dossier dans l'arborescence pour voir ses fichiers.",
"folder_help_item_2": "Utilisez [-] pour réduire et [+] pour développer les dossiers.",
"folder_help_item_3": "Sélectionnez un dossier et cliquez sur \"Créer un dossier\" pour ajouter un sous-dossier.",
"folder_help_item_4": "Pour renommer ou supprimer un dossier, sélectionnez-le puis cliquez sur le bouton approprié.",
// File List keys:
"file_list_title": "Fichiers dans (Racine)",
"files_in": "Fichiers dans",
"delete_files": "Supprimer les fichiers",
"delete_selected_files_title": "Supprimer les fichiers sélectionnés",
"delete_files_message": "Êtes-vous sûr de vouloir supprimer les fichiers sélectionnés ?",
"copy_files": "Copier les fichiers",
"copy_files_title": "Copier les fichiers sélectionnés",
"copy_files_message": "Sélectionnez un dossier de destination pour copier les fichiers sélectionnés :",
"move_files": "Déplacer les fichiers",
"move_files_title": "Déplacer les fichiers sélectionnés",
"move_files_message": "Sélectionnez un dossier de destination pour déplacer les fichiers sélectionnés :",
"move": "Déplacer",
"extract_zip_button": "Extraire le Zip",
"download_zip_title": "Télécharger les fichiers sélectionnés en Zip",
"download_zip_prompt": "Entrez un nom pour le fichier Zip :",
"zip_placeholder": "files.zip",
// Login Form keys:
"login": "Connexion",
"remember_me": "Se souvenir de moi",
"login_oidc": "Se connecter avec OIDC",
"basic_http_login": "Utiliser l'authentification HTTP basique",
// Change Password keys:
"change_password_title": "Changer le mot de passe",
"old_password": "Ancien mot de passe",
"new_password": "Nouveau mot de passe",
"confirm_new_password": "Confirmer le nouveau mot de passe",
// Add User keys:
"create_new_user_title": "Créer un nouvel utilisateur",
"username": "Nom d'utilisateur :",
"password": "Mot de passe :",
"enter_password": "Mot de passe",
"preparing_download": "Préparation de votre téléchargement...",
"download_file": "Télécharger le fichier",
"confirm_or_change_filename": "Confirmez ou modifiez le nom du fichier à télécharger :",
"filename": "Nom du fichier",
"cancel": "Annuler",
"download": "Télécharger",
"grant_admin": "Accorder l'accès administrateur",
"save_user": "Enregistrer l'utilisateur",
// Remove User keys:
"remove_user_title": "Supprimer un utilisateur",
"select_user_remove": "Sélectionnez un utilisateur à supprimer :",
"delete_user": "Supprimer l'utilisateur",
// Rename File keys:
"rename_file_title": "Renommer le fichier",
"rename_file_placeholder": "Entrez le nouveau nom du fichier",
// Folder Share
"share_folder": "Partager le dossier",
"allow_uploads": "Autoriser les téléchargements",
"share_link_generated": "Lien de partage généré",
"error_generating_share_link": "Erreur lors de la génération du lien de partage",
// Folder
"folder_share": "Partager le dossier",
// Custom Confirm Modal keys:
"yes": "Oui",
"no": "Non",
"unsaved_changes_confirm": "Vous avez des modifications non enregistrées. Êtes-vous sûr de vouloir fermer sans enregistrer ?",
"delete": "Supprimer",
"download": "Télécharger",
"upload": "Téléverser",
"copy": "Copier",
"extract": "Extraire",
"user": "Utilisateur :",
"unknown_error": "Erreur inconnue",
"link_copied": "Lien copié dans le presse-papiers",
"minutes": "minutes",
"hours": "heures",
"days": "jours",
"weeks": "semaines",
"months": "mois",
"seconds": "secondes",
// Dark Mode Toggle
"dark_mode_toggle": "Mode sombre",
"light_mode_toggle": "Mode clair",
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
"admin_panel": "Panneau d'administration",
"user_panel": "Panneau utilisateur",
"totp_settings": "Paramètres TOTP",
"enable_totp": "Activer TOTP",
"language": "Langue",
"select_language": "Sélectionnez la langue",
"english": "Anglais",
"spanish": "Espagnol",
"french": "Français",
"german": "Allemand",
"use_totp_code_instead": "Utiliser le code TOTP à la place",
"submit_recovery_code": "Soumettre le code de récupération",
"please_enter_recovery_code": "Veuillez entrer votre code de récupération.",
"recovery_code_verification_failed": "La vérification du code de récupération a échoué",
"error_verifying_recovery_code": "Erreur lors de la vérification du code de récupération",
"totp_verification_failed": "La vérification TOTP a échoué",
"error_verifying_totp_code": "Erreur lors de la vérification du code TOTP",
"totp_setup": "Configuration TOTP",
"scan_qr_code": "Scannez ce QR code avec votre application d'authentification.",
"enter_totp_confirmation": "Entrez le code à 6 chiffres de votre application pour confirmer la configuration :",
"confirm": "Confirmer",
"please_enter_valid_code": "Veuillez entrer un code valide à 6 chiffres.",
"totp_enabled_successfully": "TOTP activé avec succès.",
"error_generating_recovery_code": "Erreur lors de la génération du code de récupération",
"error_loading_qr_code": "Erreur lors du chargement du QR code.",
"error_disabling_totp_setting": "Erreur lors de la désactivation des paramètres TOTP",
"user_management": "Gestion des utilisateurs",
"add_user": "Ajouter un utilisateur",
"remove_user": "Supprimer un utilisateur",
"user_permissions": "Permissions des utilisateurs",
"oidc_configuration": "Configuration OIDC",
"oidc_provider_url": "URL du fournisseur OIDC",
"oidc_client_id": "ID du client OIDC",
"oidc_client_secret": "Secret du client OIDC",
"oidc_redirect_uri": "URI de redirection OIDC",
"global_totp_settings": "Paramètres globaux TOTP",
"global_otpauth_url": "URL globale OTPAuth",
"login_options": "Options de connexion",
"disable_login_form": "Désactiver le formulaire de connexion",
"disable_basic_http_auth": "Désactiver l'authentification HTTP basique",
"disable_oidc_login": "Désactiver la connexion OIDC",
"save_settings": "Enregistrer les paramètres",
"at_least_one_login_method": "Au moins une méthode de connexion doit rester activée.",
"settings_updated_successfully": "Paramètres mis à jour avec succès.",
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
"user_permissions_updated_successfully": "Permissions des utilisateurs mises à jour avec succès.",
"error_updating_permissions": "Erreur lors de la mise à jour des permissions",
"no_users_found": "Aucun utilisateur trouvé.",
"user_folder_only": "Uniquement le dossier utilisateur",
"read_only": "Lecture seule",
"disable_upload": "Désactiver le téléchargement",
"error_loading_users": "Erreur lors du chargement des utilisateurs",
"save_permissions": "Enregistrer les permissions",
"your_recovery_code": "Votre code de récupération",
"please_save_recovery_code": "Veuillez sauvegarder ce code en toute sécurité. Il ne sera plus affiché et ne pourra être utilisé qu'une seule fois.",
"ok": "OK"
},
de: {
"please_log_in_to_continue": "Bitte melden Sie sich an, um fortzufahren.",
"no_files_selected": "Keine Dateien ausgewählt.",
"confirm_delete_files": "Sind Sie sicher, dass Sie {count} ausgewählte Datei(en) löschen möchten?",
"element_not_found": "Element mit der ID \"{id}\" wurde nicht gefunden.",
"search_placeholder": "Dateien, Tags und Uploader suchen...",
"search_placeholder_advanced": "Erweiterte Suche: Dateien, Tags, Uploader & Inhalt...",
"basic_search_tooltip": "Einfache Suche: Nach Dateiname, Tags und Uploader suchen.",
"advanced_search_tooltip": "Erweiterte Suche: Beinhaltet Dateiinhalte zusätzlich zum Dateinamen, Tags und Uploader.",
"file_name": "Dateiname",
"date_modified": "Änderungsdatum",
"upload_date": "Hochladedatum",
"file_size": "Dateigröße",
"uploader": "Uploader",
"enter_totp_code": "Geben Sie den TOTP-Code ein",
"use_recovery_code_instead": "Verwenden Sie stattdessen den Wiederherstellungscode",
"enter_recovery_code": "Geben Sie den Wiederherstellungscode ein",
"editing": "Bearbeitung",
"decrease_font": "A-",
"increase_font": "A+",
"save": "Speichern",
"close": "Schließen",
"no_files_found": "Keine Dateien gefunden.",
"switch_to_table_view": "Zur Tabellenansicht wechseln",
"switch_to_gallery_view": "Zur Galerieansicht wechseln",
"share_file": "Datei teilen",
"set_expiration": "Ablauf festlegen:",
"password_optional": "Passwort (optional):",
"generate_share_link": "Freigabelink generieren",
"shareable_link": "Freigabelink:",
"copy_link": "Link kopieren",
"tag_file": "Datei taggen",
"tag_name": "Tagname:",
"tag_color": "Tagfarbe:",
"save_tag": "Tag speichern",
"light_mode": "Heller Modus",
"dark_mode": "Dunkler Modus",
"upload_instruction": "Ziehen Sie Dateien/Ordner hierher oder klicken Sie auf 'Dateien auswählen'",
"no_files_selected_default": "Keine Dateien ausgewählt",
"choose_files": "Dateien auswählen",
"delete_selected": "Ausgewählte löschen",
"copy_selected": "Ausgewählte kopieren",
"move_selected": "Ausgewählte verschieben",
"tag_selected": "Ausgewählte taggen",
"download_zip": "Zip herunterladen",
"extract_zip": "Zip entpacken",
"preview": "Vorschau",
"edit": "Bearbeiten",
"rename": "Umbenennen",
"trash_empty": "Papierkorb ist leer.",
"no_trash_selected": "Keine Papierkorbeinträge zur Wiederherstellung ausgewählt.",
// Additional keys for HTML translations:
"title": "FileRise",
"header_title": "FileRise",
"logout": "Abmelden",
"change_password": "Passwort ändern",
"restore_text": "Wiederherstellen oder",
"delete_text": "Papierkorbeinträge löschen",
"restore_selected": "Ausgewählte wiederherstellen",
"restore_all": "Alle wiederherstellen",
"delete_selected_trash": "Ausgewählte löschen",
"delete_all": "Alle löschen",
"upload_header": "Dateien/Ordner hochladen",
// Folder Management keys:
"folder_navigation": "Ordnernavigation & Verwaltung",
"create_folder": "Ordner erstellen",
"create_folder_title": "Ordner erstellen",
"enter_folder_name": "Geben Sie den Ordnernamen ein",
"cancel": "Abbrechen",
"create": "Erstellen",
"rename_folder": "Ordner umbenennen",
"rename_folder_title": "Ordner umbenennen",
"rename_folder_placeholder": "Neuen Ordnernamen eingeben",
"delete_folder": "Ordner löschen",
"delete_folder_title": "Ordner löschen",
"delete_folder_message": "Sind Sie sicher, dass Sie diesen Ordner löschen möchten?",
"folder_help": "Ordnerhilfe",
"folder_help_item_1": "Klicken Sie auf einen Ordner im Baum, um dessen Dateien anzuzeigen.",
"folder_help_item_2": "Verwenden Sie [-] zum Einklappen und [+] zum Ausklappen der Ordner.",
"folder_help_item_3": "Wählen Sie einen Ordner aus und klicken Sie auf \"Ordner erstellen\", um einen Unterordner hinzuzufügen.",
"folder_help_item_4": "Um einen Ordner umzubenennen oder zu löschen, wählen Sie ihn aus und klicken Sie auf den entsprechenden Button.",
// File List keys:
"actions": "Aktionen",
"file_list_title": "Dateien in (Root)",
"files_in": "Dateien in",
"delete_files": "Dateien löschen",
"delete_selected_files_title": "Ausgewählte Dateien löschen",
"delete_files_message": "Sind Sie sicher, dass Sie die ausgewählten Dateien löschen möchten?",
"copy_files": "Dateien kopieren",
"copy_files_title": "Ausgewählte Dateien kopieren",
"copy_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu kopieren:",
"move_files": "Dateien verschieben",
"move_files_title": "Ausgewählte Dateien verschieben",
"move_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu verschieben:",
"move": "Verschieben",
"extract_zip_button": "Zip entpacken",
"download_zip_title": "Ausgewählte Dateien als Zip herunterladen",
"download_zip_prompt": "Geben Sie einen Namen für die Zip-Datei ein:",
"zip_placeholder": "files.zip",
"share": "Teilen",
"total_files": "Gesamtanzahl",
"total_size": "Gesamtgröße",
"prev": "Zurück",
"next": "Weiter",
"page": "Seite",
"of": "von",
// Login Form keys:
"login": "Anmelden",
"remember_me": "Angemeldet bleiben",
"login_oidc": "Mit OIDC anmelden",
"basic_http_login": "HTTP-Basisauthentifizierung verwenden",
// Change Password keys:
"change_password_title": "Passwort ändern",
"old_password": "Altes Passwort",
"new_password": "Neues Passwort",
"confirm_new_password": "Neues Passwort bestätigen",
// Add User keys:
"create_new_user_title": "Neuen Benutzer erstellen",
"username": "Benutzername:",
"password": "Passwort:",
"enter_password": "Passwort",
"preparing_download": "Bereite Ihren Download vor...",
"download_file": "Datei herunterladen",
"confirm_or_change_filename": "Bestätigen oder ändern Sie den Dateinamen zum Download:",
"filename": "Dateiname",
"cancel": "Abbrechen",
"download": "Herunterladen",
"grant_admin": "Admin-Rechte vergeben",
"save_user": "Benutzer speichern",
// Remove User keys:
"remove_user_title": "Benutzer entfernen",
"select_user_remove": "Wählen Sie einen Benutzer zum Entfernen aus:",
"delete_user": "Benutzer löschen",
// Rename File keys:
"rename_file_title": "Datei umbenennen",
"rename_file_placeholder": "Geben Sie den neuen Dateinamen ein",
// Folder Share
"share_folder": "Ordner teilen",
"allow_uploads": "Uploads erlauben",
"share_link_generated": "Freigabelink generiert",
"error_generating_share_link": "Fehler beim Generieren des Freigabelinks",
// Folder
"folder_share": "Ordner teilen",
// Custom Confirm Modal keys:
"yes": "Ja",
"no": "Nein",
"unsaved_changes_confirm": "Sie haben ungespeicherte Änderungen. Sind Sie sicher, dass Sie schließen möchten, ohne zu speichern?",
"delete": "Löschen",
"download": "Herunterladen",
"upload": "Hochladen",
"copy": "Kopieren",
"extract": "Entpacken",
"user": "Benutzer:",
"unknown_error": "Unbekannter Fehler",
"link_copied": "Link in die Zwischenablage kopiert",
"minutes": "Minuten",
"hours": "Stunden",
"days": "Tage",
"weeks": "Wochen",
"months": "Monate",
"seconds": "Sekunden",
// Dark Mode Toggle
"dark_mode_toggle": "Dunkler Modus",
"light_mode_toggle": "Heller Modus",
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
"admin_panel": "Administrationsbereich",
"user_panel": "Benutzerbereich",
"trash_restore_delete": "Papierkorb wiederherstellen/löschen",
"totp_settings": "TOTP-Einstellungen",
"enable_totp": "TOTP aktivieren",
"language": "Sprache",
"select_language": "Sprache auswählen",
"english": "Englisch",
"spanish": "Spanisch",
"french": "Französisch",
"german": "Deutsch",
"use_totp_code_instead": "Stattdessen TOTP-Code verwenden",
"submit_recovery_code": "Wiederherstellungscode absenden",
"please_enter_recovery_code": "Bitte geben Sie Ihren Wiederherstellungscode ein.",
"recovery_code_verification_failed": "Überprüfung des Wiederherstellungscodes fehlgeschlagen",
"error_verifying_recovery_code": "Fehler bei der Überprüfung des Wiederherstellungscodes",
"totp_verification_failed": "TOTP-Überprüfung fehlgeschlagen",
"error_verifying_totp_code": "Fehler bei der Überprüfung des TOTP-Codes",
"totp_setup": "TOTP-Einrichtung",
"scan_qr_code": "Scannen Sie diesen QR-Code mit Ihrer Authenticator-App.",
"enter_totp_confirmation": "Geben Sie den 6-stelligen Code aus Ihrer App zur Bestätigung ein:",
"confirm": "Bestätigen",
"please_enter_valid_code": "Bitte geben Sie einen gültigen 6-stelligen Code ein.",
"totp_enabled_successfully": "TOTP wurde erfolgreich aktiviert.",
"error_generating_recovery_code": "Fehler beim Generieren des Wiederherstellungscodes",
"error_loading_qr_code": "Fehler beim Laden des QR-Codes.",
"error_disabling_totp_setting": "Fehler beim Deaktivieren der TOTP-Einstellungen",
"user_management": "Benutzerverwaltung",
"add_user": "Benutzer hinzufügen",
"remove_user": "Benutzer entfernen",
"user_permissions": "Benutzerberechtigungen",
"oidc_configuration": "OIDC-Konfiguration",
"oidc_provider_url": "OIDC-Anbieter-URL",
"oidc_client_id": "OIDC-Client-ID",
"oidc_client_secret": "OIDC-Client-Geheimnis",
"oidc_redirect_uri": "OIDC-Umleitungs-URI",
"global_totp_settings": "Globale TOTP-Einstellungen",
"global_otpauth_url": "Globale OTPAuth-URL",
"login_options": "Anmeldeoptionen",
"disable_login_form": "Anmeldeformular deaktivieren",
"disable_basic_http_auth": "HTTP-Basisauthentifizierung deaktivieren",
"disable_oidc_login": "OIDC-Anmeldung deaktivieren",
"save_settings": "Einstellungen speichern",
"at_least_one_login_method": "Mindestens eine Anmeldemethode muss aktiviert bleiben.",
"settings_updated_successfully": "Einstellungen wurden erfolgreich aktualisiert.",
"error_updating_settings": "Fehler beim Aktualisieren der Einstellungen",
"user_permissions_updated_successfully": "Benutzerberechtigungen wurden erfolgreich aktualisiert.",
"error_updating_permissions": "Fehler beim Aktualisieren der Berechtigungen",
"no_users_found": "Keine Benutzer gefunden.",
"user_folder_only": "Nur Benutzerordner",
"read_only": "Nur Lesen",
"disable_upload": "Upload deaktivieren",
"error_loading_users": "Fehler beim Laden der Benutzer",
"save_permissions": "Berechtigungen speichern",
"your_recovery_code": "Ihr Wiederherstellungscode",
"please_save_recovery_code": "Bitte speichern Sie diesen Code sicher. Er wird nicht erneut angezeigt und kann nur einmal verwendet werden.",
"ok": "OK",
"show": "Zeige",
"items_per_page": "elemente pro seite"
}
};
let currentLocale = 'en';
export function setLocale(locale) {
currentLocale = locale;
}
export function t(key, placeholders) {
const localeTranslations = translations[currentLocale] || {};
let translation = localeTranslations[key] || key;
if (placeholders) {
Object.keys(placeholders).forEach(ph => {
translation = translation.replace(`{${ph}}`, placeholders[ph]);
});
}
return translation;
}
export function applyTranslations() {
document.querySelectorAll('[data-i18n-key]').forEach(el => {
el.innerText = t(el.getAttribute('data-i18n-key'));
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.setAttribute('placeholder', t(el.getAttribute('data-i18n-placeholder')));
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.setAttribute('title', t(el.getAttribute('data-i18n-title')));
});
}

View File

@@ -8,10 +8,12 @@ import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDro
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
import { displayFilePreview } from './filePreview.js';
import { loadFileList } from './fileListView.js';
import { initFileActions, renameFile } from './fileActions.js';
import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload } from './fileActions.js';
import { editFile, saveFile } from './fileEditor.js';
import { t, applyTranslations, setLocale } from './i18n.js';
function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
// Remove the retry logic version and just use loadCsrfToken directly:
function loadCsrfToken() {
return fetch('token.php', { credentials: 'include' })
.then(response => {
if (!response.ok) {
@@ -20,11 +22,9 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
return response.json();
})
.then(data => {
// Set global variables.
window.csrfToken = data.csrf_token;
window.SHARE_URL = data.share_url;
// Update (or create) the CSRF meta tag.
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
if (!metaCSRF) {
metaCSRF = document.createElement('meta');
@@ -33,7 +33,6 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
}
metaCSRF.setAttribute('content', data.csrf_token);
// Update (or create) the share URL meta tag.
let metaShare = document.querySelector('meta[name="share-url"]');
if (!metaShare) {
metaShare = document.createElement('meta');
@@ -43,18 +42,10 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
metaShare.setAttribute('content', data.share_url);
return data;
})
.catch(error => {
if (retries > 0) {
console.warn(`CSRF token load failed. Retrying in ${delay}ms... (${retries} retries left)`, error);
return new Promise(resolve => setTimeout(resolve, delay))
.then(() => loadCsrfTokenWithRetry(retries - 1, delay * 2));
}
console.error("Failed to load CSRF token after retries.", error);
throw error;
});
}
// Expose functions for inline handlers.
window.sendRequest = sendRequest;
window.toggleVisibility = toggleVisibility;
@@ -62,13 +53,21 @@ window.toggleAllCheckboxes = toggleAllCheckboxes;
window.editFile = editFile;
window.saveFile = saveFile;
window.renameFile = renameFile;
window.confirmSingleDownload = confirmSingleDownload;
window.openDownloadModal = openDownloadModal;
// Global variable for the current folder.
window.currentFolder = "root";
document.addEventListener("DOMContentLoaded", function () {
// Retrieve the saved language from localStorage; default to "en"
const savedLanguage = localStorage.getItem("language") || "en";
// Set the locale based on the saved language
setLocale(savedLanguage);
// Apply the translations to update the UI
applyTranslations();
// First, load the CSRF token (with retry).
loadCsrfTokenWithRetry().then(() => {
loadCsrfToken().then(() => {
// Once CSRF token is loaded, initialize authentication.
initAuth();
@@ -104,7 +103,7 @@ document.addEventListener("DOMContentLoaded", function () {
// Other DOM initialization that can happen after CSRF is ready.
const newPasswordInput = document.getElementById("newPassword");
if (newPasswordInput) {
newPasswordInput.addEventListener("input", function() {
newPasswordInput.addEventListener("input", function () {
console.log("newPassword input event:", this.value);
});
} else {
@@ -129,18 +128,18 @@ document.addEventListener("DOMContentLoaded", function () {
if (darkModeToggle) {
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
? "Light Mode"
: "Dark Mode";
? t("light_mode")
: t("dark_mode");
darkModeToggle.addEventListener("click", function () {
if (document.body.classList.contains("dark-mode")) {
document.body.classList.remove("dark-mode");
localStorage.setItem("darkMode", "false");
darkModeToggle.textContent = "Dark Mode";
darkModeToggle.textContent = t("dark_mode");
} else {
document.body.classList.add("dark-mode");
localStorage.setItem("darkMode", "true");
darkModeToggle.textContent = "Light Mode";
darkModeToggle.textContent = t("light_mode");
}
});
}
@@ -149,10 +148,10 @@ document.addEventListener("DOMContentLoaded", function () {
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
if (event.matches) {
document.body.classList.add("dark-mode");
if (darkModeToggle) darkModeToggle.textContent = "Light Mode";
if (darkModeToggle) darkModeToggle.textContent = t("light_mode");
} else {
document.body.classList.remove("dark-mode");
if (darkModeToggle) darkModeToggle.textContent = "Dark Mode";
if (darkModeToggle) darkModeToggle.textContent = t("dark_mode");
}
});
}

View File

@@ -3,6 +3,7 @@ import { sendRequest } from './networkUtils.js';
import { toggleVisibility, showToast } from './domUtils.js';
import { loadFileList } from './fileListView.js';
import { loadFolderTree } from './folderManager.js';
import { t } from './i18n.js';
function showConfirm(message, onConfirm) {
const modal = document.getElementById("customConfirmModal");
@@ -65,7 +66,7 @@ export function setupTrashRestoreDelete() {
const files = Array.from(selected).map(chk => chk.value);
console.log("Restore Selected clicked, files:", files);
if (files.length === 0) {
showToast("No trash items selected for restore.");
showToast(t("no_trash_selected"));
return;
}
fetch("restoreFiles.php", {
@@ -105,7 +106,7 @@ export function setupTrashRestoreDelete() {
const files = Array.from(allChk).map(chk => chk.value);
console.log("Restore All clicked, files:", files);
if (files.length === 0) {
showToast("Trash is empty.");
showToast(t("trash_empty"));
return;
}
fetch("restoreFiles.php", {

View File

@@ -3,6 +3,7 @@ import { displayFilePreview } from './filePreview.js';
import { showToast, escapeHTML } from './domUtils.js';
import { loadFolderTree } from './folderManager.js';
import { loadFileList } from './fileListView.js';
import { t } from './i18n.js';
/* -----------------------------------------------------
Helpers for DragandDrop Folder Uploads (Original Code)
@@ -47,22 +48,19 @@ function getFilesFromDataTransferItems(items) {
return Promise.all(promises).then(results => results.flat());
}
/* -----------------------------------------------------
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'
${t("upload_instruction")}
</div>
<div id="uploadFileRow" class="upload-file-row">
<button id="customChooseBtn" type="button">Choose files</button>
<button id="customChooseBtn" type="button">${t("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>
<span id="fileInfoDefault"> ${t("no_files_selected_default")}</span>
</div>
</div>
<!-- File input for file picker (files only) -->

View File

@@ -81,7 +81,7 @@ $username = trim($_SERVER['PHP_AUTH_USER']);
$password = trim($_SERVER['PHP_AUTH_PW']);
// Validate username format (optional)
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
if (!preg_match(REGEX_USER, $username)) {
header('WWW-Authenticate: Basic realm="FileRise Login"');
header('HTTP/1.0 401 Unauthorized');
echo 'Invalid username format';

View File

@@ -1,9 +1,6 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
// --- CSRF Protection ---
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
@@ -45,7 +42,7 @@ $sourceFolder = trim($data['source']) ?: 'root';
$destinationFolder = trim($data['destination']) ?: 'root';
// Allow only letters, numbers, underscores, dashes, spaces, and forward slashes in folder names.
$folderPattern = '/^[A-Za-z0-9_\- \/]+$/';
$folderPattern = REGEX_FOLDER_NAME;
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
echo json_encode(["error" => "Invalid source folder name."]);
exit;
@@ -111,7 +108,7 @@ $srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMet
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
$errors = [];
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($data['files'] as $fileName) {
// Save the original name for metadata lookup.

View File

@@ -17,9 +17,9 @@ if (!isset($_POST['folder'])) {
exit;
}
$folder = $_POST['folder'];
// Validate the folder name (only alphanumerics, dashes allowed)
if (!preg_match('/^resumable_[A-Za-z0-9\-]+$/', $folder)) {
$folder = urldecode($_POST['folder']);
$regex = "/^resumable_" . PATTERN_FOLDER_NAME . "$/u"; // full regex pattern
if (!preg_match($regex, $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
http_response_code(400);
exit;

View File

@@ -30,7 +30,7 @@ if (!$usernameToRemove) {
}
// Optional: Validate the username format (allow letters, numbers, underscores, dashes, and spaces)
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $usernameToRemove)) {
if (!preg_match(REGEX_USER, $usernameToRemove)) {
echo json_encode(["error" => "Invalid username format"]);
exit;
}

View File

@@ -40,7 +40,7 @@ if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($dat
$folder = trim($data['folder']) ?: 'root';
// For subfolders, allow letters, numbers, underscores, dashes, spaces, and forward slashes.
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
exit;
}
@@ -49,7 +49,7 @@ $oldName = basename(trim($data['oldName']));
$newName = basename(trim($data['newName']));
// Validate file names: allow letters, numbers, underscores, dashes, dots, parentheses, and spaces.
if (!preg_match('/^[A-Za-z0-9_\-\. \(\)]+$/', $oldName) || !preg_match('/^[A-Za-z0-9_\-\. \(\)]+$/', $newName)) {
if (!preg_match(REGEX_FILE_NAME, $oldName) || !preg_match(REGEX_FILE_NAME, $newName)) {
echo json_encode(["error" => "Invalid file name."]);
exit;
}

View File

@@ -1,9 +1,6 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
// Ensure user is authenticated
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
@@ -48,7 +45,7 @@ $oldFolder = trim($input['oldFolder']);
$newFolder = trim($input['newFolder']);
// Validate folder names
if (!preg_match('/^[A-Za-z0-9_\- \/]+$/', $oldFolder) || !preg_match('/^[A-Za-z0-9_\- \/]+$/', $newFolder)) {
if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) {
echo json_encode(['success' => false, 'error' => 'Invalid folder name(s).']);
exit;
}

View File

@@ -53,7 +53,7 @@ if (!isset($data['files']) || !is_array($data['files'])) {
}
// Define a safe file name pattern.
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
$safeFileNamePattern = REGEX_FILE_NAME;
$restoredItems = [];
$errors = [];

View File

@@ -48,7 +48,7 @@ $folder = isset($data["folder"]) ? trim($data["folder"]) : "root";
// If a subfolder is provided, validate it.
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
if ($folder !== "root" && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
if ($folder !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
exit;
}

View File

@@ -13,11 +13,21 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
}
// CSRF Protection: validate token from header.
$headers = getallheaders();
if (!isset($headers['X-CSRF-Token']) || $headers['X-CSRF-Token'] !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token."]);
http_response_code(403);
exit;
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
respond('error', 403, 'Invalid CSRF token');
}
$username = $_SESSION['username'] ?? '';
$userPermissions = loadUserPermissions($username);
if ($username) {
$userPermissions = loadUserPermissions($username);
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
echo json_encode(["error" => "Read-only users are not allowed to file tags"]);
exit();
}
}
// Retrieve and sanitize input.
@@ -77,7 +87,7 @@ if ($file === "global") {
}
// Validate folder name.
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name."]);
exit;
}

356
shareFolder.php Normal file
View File

@@ -0,0 +1,356 @@
<?php
// shareFolder.php
require_once 'config.php';
// Retrieve token and optional password from GET.
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
$providedPass = filter_input(INPUT_GET, 'pass', FILTER_SANITIZE_STRING);
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT);
if ($page === false || $page < 1) {
$page = 1;
}
if (empty($token)) {
http_response_code(400);
echo json_encode(["error" => "Missing token."]);
exit;
}
// Load share folder records.
$shareFile = META_DIR . "share_folder_links.json";
if (!file_exists($shareFile)) {
http_response_code(404);
echo json_encode(["error" => "Share link not found."]);
exit;
}
$shareLinks = json_decode(file_get_contents($shareFile), true);
if (!is_array($shareLinks) || !isset($shareLinks[$token])) {
http_response_code(404);
echo json_encode(["error" => "Share link not found."]);
exit;
}
$record = $shareLinks[$token];
// Check expiration.
if (time() > $record['expires']) {
http_response_code(403);
echo json_encode(["error" => "This link has expired."]);
exit;
}
// If password protection is enabled and no password is provided, show password form.
if (!empty($record['password']) && empty($providedPass)) {
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Enter Password</title>
<style>
body {
background-color: #f7f7f7;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
}
.container {
max-width: 400px;
margin: 80px auto;
background: #fff;
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 {
margin-top: 0;
font-size: 1.5rem;
text-align: center;
color: #333;
}
label, input, button {
display: block;
width: 100%;
}
input[type="password"] {
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
background-color: #007BFF;
border: none;
color: #fff;
padding: 10px;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h2>Folder Protected</h2>
<p>This folder is protected by a password.</p>
<form method="get" action="shareFolder.php">
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
<label for="pass">Password:</label>
<input type="password" name="pass" id="pass" required>
<button type="submit">Submit</button>
</form>
</div>
</body>
</html>
<?php
exit;
}
// Validate the provided password if required.
if (!empty($record['password'])) {
if (!password_verify($providedPass, $record['password'])) {
http_response_code(403);
echo json_encode(["error" => "Invalid password."]);
exit;
}
}
// Determine the folder path.
$folder = trim($record['folder'], "/\\ ");
$folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
$realFolderPath = realpath($folderPath);
$uploadDirReal = realpath(UPLOAD_DIR);
// Validate that the folder exists and is within UPLOAD_DIR.
if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) {
http_response_code(404);
echo json_encode(["error" => "Folder not found."]);
exit;
}
// Scan and sort files.
$allFiles = array_values(array_filter(scandir($realFolderPath), function($item) use ($realFolderPath) {
return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item);
}));
sort($allFiles);
// Pagination variables.
$itemsPerPage = 10;
$totalFiles = count($allFiles);
$totalPages = max(1, ceil($totalFiles / $itemsPerPage));
$currentPage = min($page, $totalPages);
$startIndex = ($currentPage - 1) * $itemsPerPage;
$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
/**
* Convert file size in bytes into a human-readable string.
*
* @param int $bytes The file size in bytes.
* @return string The formatted size string.
*/
function formatBytes($bytes) {
if ($bytes < 1024) {
return $bytes . " B";
} elseif ($bytes < 1024 * 1024) {
return round($bytes / 1024, 2) . " KB";
} elseif ($bytes < 1024 * 1024 * 1024) {
return round($bytes / (1024 * 1024), 2) . " MB";
} else {
return round($bytes / (1024 * 1024 * 1024), 2) . " GB";
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Shared Folder: <?php echo htmlspecialchars($folder, ENT_QUOTES, 'UTF-8'); ?></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
background: #f2f2f2;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 20px;
color: #333;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
margin: 0;
font-size: 2rem;
}
.container {
max-width: 800px;
margin: 0 auto;
background: #fff;
border-radius: 4px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
border-bottom: 1px solid #ddd;
text-align: left;
}
th {
background: #007BFF;
color: #fff;
font-weight: normal;
}
tr:hover {
background: #f9f9f9;
}
a {
color: #007BFF;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Simple download icon style */
.download-icon {
margin-left: 8px;
font-weight: bold;
color: #007BFF;
}
/* Pagination styles */
.pagination {
text-align: center;
margin-top: 20px;
}
.pagination a, .pagination span {
margin: 0 5px;
padding: 8px 12px;
text-decoration: none;
background: #007BFF;
color: #fff;
border-radius: 4px;
}
.pagination span.current {
background: #0056b3;
}
.upload-container {
margin-top: 30px;
text-align: center;
}
.upload-container h3 {
font-size: 1.4rem;
margin-bottom: 10px;
}
.upload-container form {
display: inline-block;
margin-top: 10px;
}
.upload-container button {
background-color: #28a745;
border: none;
color: #fff;
padding: 10px 20px;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
}
.upload-container button:hover {
background-color: #218838;
}
.footer {
text-align: center;
margin-top: 40px;
font-size: 0.9rem;
color: #777;
}
</style>
</head>
<body>
<div class="header">
<h1>Shared Folder: <?php echo htmlspecialchars($folder, ENT_QUOTES, 'UTF-8'); ?></h1>
</div>
<div class="container">
<?php if (empty($filesOnPage)): ?>
<p style="text-align:center;">This folder is empty.</p>
<?php else: ?>
<table>
<thead>
<tr>
<th>Filename</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<?php foreach ($filesOnPage as $file):
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
$fileSize = formatBytes(filesize($filePath));
// Build download link using share token and file name.
$downloadLink = "downloadSharedFile.php?token=" . urlencode($token) . "&file=" . urlencode($file);
?>
<tr>
<td>
<a href="<?php echo htmlspecialchars($downloadLink, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($file, ENT_QUOTES, 'UTF-8'); ?>
<span class="download-icon">&#x21E9;</span>
</a>
</td>
<td><?php echo $fileSize; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- Pagination Controls -->
<div class="pagination">
<?php if ($currentPage > 1): ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $currentPage - 1; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>">Prev</a>
<?php else: ?>
<span>Prev</span>
<?php endif; ?>
<?php
// Display up to 5 page links centered around the current page.
$startPage = max(1, $currentPage - 2);
$endPage = min($totalPages, $currentPage + 2);
for ($i = $startPage; $i <= $endPage; $i++): ?>
<?php if ($i == $currentPage): ?>
<span class="current"><?php echo $i; ?></span>
<?php else: ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $i; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($currentPage < $totalPages): ?>
<a href="shareFolder.php?token=<?php echo urlencode($token); ?>&page=<?php echo $currentPage + 1; ?><?php echo !empty($providedPass) ? "&pass=" . urlencode($providedPass) : ""; ?>">Next</a>
<?php else: ?>
<span>Next</span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($record['allowUpload']) : ?>
<div class="upload-container">
<h3>Upload File (50mb max size)</h3>
<form action="uploadToSharedFolder.php" method="post" enctype="multipart/form-data">
<!-- Passing token so the upload endpoint can verify the share link. -->
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="file" name="fileToUpload" required>
<br><br>
<button type="submit">Upload</button>
</form>
</div>
<?php endif; ?>
</div>
<div class="footer">
&copy; <?php echo date("Y"); ?> FileRise. All rights reserved.
</div>
</body>
</html>

View File

@@ -11,11 +11,11 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
}
// Verify CSRF token from request headers.
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
http_response_code(403);
echo json_encode(["error" => "Invalid CSRF token"]);
exit;
respond('error', 403, 'Invalid CSRF token');
}
header('Content-Type: application/json');

View File

@@ -13,11 +13,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
}
// ——— 2) CSRF check ———
if (empty($_SERVER['HTTP_X_CSRF_TOKEN'])
|| $_SERVER['HTTP_X_CSRF_TOKEN'] !== ($_SESSION['csrf_token'] ?? '')) {
http_response_code(403);
error_log("Invalid CSRF token on recovery for IP {$_SERVER['REMOTE_ADDR']}");
exit(json_encode(['status'=>'error','message'=>'Invalid CSRF token']));
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
respond('error', 403, 'Invalid CSRF token');
}
// ——— 3) Identify user to recover ———
@@ -32,7 +32,7 @@ if (!$userId) {
}
// ——— Validate userId format ———
if (!preg_match('/^[A-Za-z0-9_\-]+$/', $userId)) {
if (!preg_match(REGEX_USER, $userId)) {
http_response_code(400);
error_log("Invalid userId format: {$userId}");
exit(json_encode(['status'=>'error','message'=>'Invalid user identifier']));

View File

@@ -13,11 +13,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
}
// 2) CSRF check
if (empty($_SERVER['HTTP_X_CSRF_TOKEN'])
|| $_SERVER['HTTP_X_CSRF_TOKEN'] !== ($_SESSION['csrf_token'] ?? '')) {
http_response_code(403);
error_log("totp_saveCode: invalid CSRF token from IP {$_SERVER['REMOTE_ADDR']}");
exit(json_encode(['status'=>'error','message'=>'Invalid CSRF token']));
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
respond('error', 403, 'Invalid CSRF token');
}
// 3) Must be logged in
@@ -29,7 +29,7 @@ if (empty($_SESSION['username'])) {
// 4) Validate username format
$userId = $_SESSION['username'];
if (!preg_match('/^[A-Za-z0-9_\-]+$/', $userId)) {
if (!preg_match(REGEX_USER, $userId)) {
http_response_code(400);
error_log("totp_saveCode: invalid username format: {$userId}");
exit(json_encode(['status'=>'error','message'=>'Invalid user identifier']));

View File

@@ -6,19 +6,35 @@ require_once 'config.php';
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
// For debugging purposes, you might enable error reporting temporarily:
// ini_set('display_errors', 1);
// error_reporting(E_ALL);
// Define the respond() helper if not already defined.
if (!function_exists('respond')) {
function respond($status, $code, $message, $data = []) {
http_response_code($code);
echo json_encode([
'status' => $status,
'code' => $code,
'message' => $message,
'data' => $data
]);
exit;
}
}
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
// Allow access if the user is authenticated or pending TOTP.
if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) {
http_response_code(403);
exit;
}
// Verify CSRF token provided as a GET parameter.
if (!isset($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
// Retrieve CSRF token from GET parameter or request headers.
$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;
}
@@ -108,7 +124,13 @@ function getGlobalOtpauthUrl() {
return "";
}
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
$tfa = new \RobThree\Auth\TwoFactorAuth(
new GoogleChartsQrCodeProvider(), // QR code provider
'FileRise', // issuer
6, // number of digits
30, // period in seconds
Algorithm::Sha1 // enum case from your Algorithm enum
);
// Retrieve the current TOTP secret for the user.
$totpSecret = getUserTOTPSecret($username);
@@ -120,8 +142,6 @@ if (!$totpSecret) {
}
// Determine the otpauth URL to use.
// If a global OTPAuth URL template is defined, replace placeholders {label} and {secret}.
// Otherwise, use the default method.
$globalOtpauthUrl = getGlobalOtpauthUrl();
if (!empty($globalOtpauthUrl)) {
$label = "FileRise:" . $username;
@@ -140,7 +160,6 @@ if (!empty($globalOtpauthUrl)) {
$result = Builder::create()
->writer(new PngWriter())
->data($otpauthUrl)
->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
->build();
header('Content-Type: ' . $result->getMimeType());

View File

@@ -8,6 +8,9 @@ require_once 'config.php';
header('Content-Type: application/json');
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
try {
// standardized error helper
function respond($status, $code, $message, $data = []) {
@@ -54,8 +57,9 @@ try {
respond('error', 403, 'Not authenticated');
}
// CSRF check
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
respond('error', 403, 'Invalid CSRF token');
}
@@ -71,7 +75,13 @@ try {
if (isset($_SESSION['pending_login_user'])) {
$username = $_SESSION['pending_login_user'];
$totpSecret = $_SESSION['pending_login_secret'];
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
$tfa = new \RobThree\Auth\TwoFactorAuth(
new GoogleChartsQrCodeProvider(), // QR code provider
'FileRise', // issuer
6, // number of digits
30, // period in seconds
Algorithm::Sha1 // Correct enum case name from your enum
);
if (!$tfa->verifyCode($totpSecret, $code)) {
$_SESSION['totp_failures']++;
@@ -117,7 +127,14 @@ try {
respond('error', 500, 'TOTP secret not found. Please set up TOTP again.');
}
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
$tfa = new \RobThree\Auth\TwoFactorAuth(
new GoogleChartsQrCodeProvider(), // QR code provider
'FileRise', // issuer
6, // number of digits
30, // period in seconds
Algorithm::Sha1 // Correct enum case name from your enum
);
if (!$tfa->verifyCode($totpSecret, $code)) {
$_SESSION['totp_failures']++;
respond('error', 400, 'Invalid TOTP code');

View File

@@ -33,6 +33,9 @@ if (!is_array($data)) {
exit;
}
// Retrieve new header title, sanitize if necessary.
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
// Validate and sanitize OIDC configuration.
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
@@ -54,8 +57,9 @@ $disableOIDCLogin = isset($data['disableOIDCLogin']) ? filter_var($data['disable
// Retrieve the global OTPAuth URL (new field). If not provided, default to an empty string.
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
// Prepare configuration array.
// Prepare configuration array including the header title.
$configUpdate = [
'header_title' => $headerTitle, // New field for the header title
'oidc' => [
'providerUrl' => $oidcProviderUrl,
'clientId' => $oidcClientId,
@@ -79,15 +83,10 @@ $encryptedContent = encryptData($plainTextConfig, $encryptionKey);
// Attempt to write the new configuration.
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
// Log the error.
error_log("updateConfig.php: Initial write failed, attempting to delete the old configuration file.");
// Delete the old file.
if (file_exists($configFile)) {
unlink($configFile);
}
// Try writing again.
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
error_log("updateConfig.php: Failed to write configuration even after deletion.");
http_response_code(500);

View File

@@ -40,16 +40,39 @@ if (file_exists($permissionsFile)) {
$existingPermissions = [];
}
// Load user roles from the users file (similar to getUsers.php)
$usersFile = USERS_DIR . USERS_FILE;
$userRoles = [];
if (file_exists($usersFile)) {
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(':', trim($line));
if (count($parts) >= 3) {
// Validate username format:
if (preg_match(REGEX_USER, $parts[0])) {
// Use a lowercase key for consistency.
$userRoles[strtolower($parts[0])] = trim($parts[2]);
}
}
}
}
// Loop through each permission update.
foreach ($permissions as $perm) {
// Ensure username is provided.
if (!isset($perm['username'])) continue;
$username = $perm['username'];
// Look up the user's role from the users file.
$role = isset($userRoles[strtolower($username)]) ? $userRoles[strtolower($username)] : null;
// Skip updating permissions for admin users.
if (strtolower($username) === "admin") continue;
if ($role === "1") {
continue;
}
// Update permissions: default any missing value to false.
$existingPermissions[$username] = [
$existingPermissions[strtolower($username)] = [
'folderOnly' => isset($perm['folderOnly']) ? (bool)$perm['folderOnly'] : false,
'readOnly' => isset($perm['readOnly']) ? (bool)$perm['readOnly'] : false,
'disableUpload' => isset($perm['disableUpload']) ? (bool)$perm['disableUpload'] : false

View File

@@ -65,14 +65,16 @@ if (isset($_POST['resumableChunkNumber'])) {
$resumableFilename = $_POST['resumableFilename'];
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
http_response_code(400); // Set an error HTTP status code
// First, strip directory components.
$resumableFilename = urldecode(basename($_POST['resumableFilename']));
if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) {
http_response_code(400);
echo json_encode(["error" => "Invalid file name: " . $resumableFilename]);
exit;
}
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
exit;
}
@@ -173,7 +175,7 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
// ------------- 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)) {
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
exit;
}
@@ -195,10 +197,12 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
$metadataCollection = []; // key: folder path, value: metadata array
$metadataChanged = []; // key: folder path, value: boolean
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
// Use a Unicode-enabled pattern to allow special characters.
$safeFileNamePattern = REGEX_FILE_NAME;
foreach ($_FILES["file"]["name"] as $index => $fileName) {
$safeFileName = basename($fileName);
// First, ensure we only work with the base filename to avoid traversal issues.
$safeFileName = trim(urldecode(basename($fileName)));
if (!preg_match($safeFileNamePattern, $safeFileName)) {
echo json_encode(["error" => "Invalid file name: " . $fileName]);
exit;
@@ -224,6 +228,7 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
. str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR;
}
// Reapply basename to the relativePath to get the final safe file name.
$safeFileName = basename($relativePath);
}
// --- End Minimal Folder/Subfolder Logic ---

151
uploadToSharedFolder.php Normal file
View File

@@ -0,0 +1,151 @@
<?php
// uploadToSharedFolder.php
require_once 'config.php';
// Only accept POST requests.
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(["error" => "Method not allowed."]);
exit;
}
// Ensure the share token is provided.
if (empty($_POST['token'])) {
http_response_code(400);
echo json_encode(["error" => "Missing share token."]);
exit;
}
$token = trim($_POST['token']);
// Load the share folder records.
$shareFile = META_DIR . "share_folder_links.json";
if (!file_exists($shareFile)) {
http_response_code(404);
echo json_encode(["error" => "Share record not found."]);
exit;
}
$shareLinks = json_decode(file_get_contents($shareFile), true);
if (!is_array($shareLinks) || !isset($shareLinks[$token])) {
http_response_code(404);
echo json_encode(["error" => "Invalid share token."]);
exit;
}
$record = $shareLinks[$token];
// Check if the share link is expired.
if (time() > $record['expires']) {
http_response_code(403);
echo json_encode(["error" => "This share link has expired."]);
exit;
}
// Ensure that uploads are allowed for this share.
if (empty($record['allowUpload']) || $record['allowUpload'] != 1) {
http_response_code(403);
echo json_encode(["error" => "File uploads are not allowed for this share."]);
exit;
}
// Check that a file was uploaded.
if (!isset($_FILES['fileToUpload'])) {
http_response_code(400);
echo json_encode(["error" => "No file was uploaded."]);
exit;
}
$fileUpload = $_FILES['fileToUpload'];
// Check for upload errors.
if ($fileUpload['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(["error" => "File upload error. Code: " . $fileUpload['error']]);
exit;
}
// Enforce a maximum file size (e.g. 50MB).
$maxSize = 50 * 1024 * 1024; // 50MB
if ($fileUpload['size'] > $maxSize) {
http_response_code(400);
echo json_encode(["error" => "File size exceeds allowed limit."]);
exit;
}
// Define allowed file extensions.
$allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3'];
$uploadedName = basename($fileUpload['name']);
$ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExtensions)) {
http_response_code(400);
echo json_encode(["error" => "File type not allowed."]);
exit;
}
// Determine the target folder from the share record.
$folder = trim($record['folder'], "/\\");
$targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
$realTargetFolder = realpath($targetFolder);
$uploadDirReal = realpath(UPLOAD_DIR);
if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) {
http_response_code(404);
echo json_encode(["error" => "Shared folder not found."]);
exit;
}
// Generate a new filename to avoid collisions.
// A unique prefix (using uniqid) is prepended to help with uniqueness and traceability.
$newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName);
$targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename;
// Move the uploaded file securely.
if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) {
http_response_code(500);
echo json_encode(["error" => "Failed to move the uploaded file."]);
exit;
}
// --- Metadata Update for Shared Upload ---
$metadataKey = ($folder === '' || $folder === 'root') ? "root" : $folder;
// Sanitize the metadata file name.
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
$metadataFile = META_DIR . $metadataFileName;
// Load existing metadata if available.
$metadataCollection = [];
if (file_exists($metadataFile)) {
$data = file_get_contents($metadataFile);
$metadataCollection = json_decode($data, true);
if (!is_array($metadataCollection)) {
$metadataCollection = [];
}
}
// Set upload date using your defined format.
$uploadedDate = date(DATE_TIME_FORMAT);
// Since there is no logged-in user for public share uploads,
$uploader = "Outside Share";
// Update metadata for the new file.
if (!isset($metadataCollection[$newFilename])) {
$metadataCollection[$newFilename] = [
"uploaded" => $uploadedDate,
"uploader" => $uploader
];
}
// Save the metadata.
file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT));
// --- End Metadata Update ---
// Optionally, set a flash message in session.
$_SESSION['upload_message'] = "File uploaded successfully.";
// Redirect back to the shared folder view, refreshing the file listing.
header("Location: shareFolder.php?token=" . urlencode($token));
exit;
?>