Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9c4200827 | ||
|
|
97559873dc | ||
|
|
0683b27534 | ||
|
|
49c42e8096 | ||
|
|
ed39e112a9 | ||
|
|
25edab923a | ||
|
|
b8ae3c4402 | ||
|
|
fb537b1d61 | ||
|
|
90439022e3 | ||
|
|
b4c8738b8a | ||
|
|
e193bf9b13 | ||
|
|
a70d8fc2c7 | ||
|
|
d9f69d7917 | ||
|
|
28ac23c2f6 | ||
|
|
b06c49f213 | ||
|
|
8553efabc1 | ||
|
|
81a08ffd5b | ||
|
|
296dae96a5 | ||
|
|
337f529afd | ||
|
|
4360f2830a | ||
|
|
894cc938a5 | ||
|
|
01801ba950 | ||
|
|
5b592575a4 |
65
CHANGELOG.md
65
CHANGELOG.md
@@ -1,6 +1,67 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Shift Key Multi‑Selection Changes 4/10/2025
|
## 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 client‑side 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 isn’t 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 in‑array .filter(). This ensures that every search—real‑time by user input—is powered by Fuse.js’s 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 Unicode‑enabled 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 Multi‑Selection Changes 4/10/2025 v1.1.1
|
||||||
|
|
||||||
- **Implemented Range Selection:**
|
- **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.
|
- 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.
|
||||||
@@ -30,7 +91,7 @@
|
|||||||
- `shareFolder.php` updated to display format size.
|
- `shareFolder.php` updated to display format size.
|
||||||
- Fix to prevent the filename text from overflowing its container in the gallery view.
|
- Fix to prevent the filename text from overflowing its container in the gallery view.
|
||||||
- Reduced header height.
|
- Reduced header height.
|
||||||
- Create Folder changed to Material Icon `edit`
|
- Create Folder changed to Material Icon `create_new_folder`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -22,7 +22,7 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
|||||||
|
|
||||||
- 📝 **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.
|
- 📝 **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:** 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.
|
- 🔒 **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.
|
||||||
|
|
||||||
@@ -177,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
|
## License
|
||||||
|
|
||||||
This project is open-source under the MIT License. That means you’re free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!
|
This project is open-source under the MIT License. That means you’re free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ if (!$newUsername || !$newPassword) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate username using preg_match (allow letters, numbers, underscores, dashes, and spaces).
|
// 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."]);
|
echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
14
auth.php
14
auth.php
@@ -2,7 +2,9 @@
|
|||||||
require_once 'vendor/autoload.php';
|
require_once 'vendor/autoload.php';
|
||||||
require_once 'config.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');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Global exception handler: logs errors and returns a generic error message.
|
// Global exception handler: logs errors and returns a generic error message.
|
||||||
@@ -177,7 +179,7 @@ if (!$username || !$password) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
if (!preg_match(REGEX_USER, $username)) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||||
exit();
|
exit();
|
||||||
@@ -197,7 +199,13 @@ if ($user !== false) {
|
|||||||
]);
|
]);
|
||||||
exit();
|
exit();
|
||||||
} else {
|
} 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']);
|
$providedCode = trim($data['totp_code']);
|
||||||
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
|
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
|
||||||
echo json_encode(["error" => "Invalid TOTP code"]);
|
echo json_encode(["error" => "Invalid TOTP code"]);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"jumbojett/openid-connect-php": "^1.0.0",
|
"jumbojett/openid-connect-php": "^1.0.0",
|
||||||
"phpseclib/phpseclib": "~3.0.7",
|
"phpseclib/phpseclib": "~3.0.7",
|
||||||
"robthree/twofactorauth": "^1.7",
|
"robthree/twofactorauth": "^3.0",
|
||||||
"endroid/qr-code": "^4.0"
|
"endroid/qr-code": "^5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
74
composer.lock
generated
74
composer.lock
generated
@@ -4,32 +4,32 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c9857f23364f2280ef4b71cdc72d3f78",
|
"content-hash": "6b70aec0c1830ebb2b8f9bb625b04a22",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
"version": "2.0.8",
|
"version": "v3.0.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Bacon/BaconQrCode.git",
|
"url": "https://github.com/Bacon/BaconQrCode.git",
|
||||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
|
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
|
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f",
|
||||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
|
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"dasprid/enum": "^1.0.3",
|
"dasprid/enum": "^1.0.3",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"php": "^7.1 || ^8.0"
|
"php": "^8.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phly/keep-a-changelog": "^2.1",
|
"phly/keep-a-changelog": "^2.12",
|
||||||
"phpunit/phpunit": "^7 | ^8 | ^9",
|
"phpunit/phpunit": "^10.5.11 || 11.0.4",
|
||||||
"spatie/phpunit-snapshot-assertions": "^4.2.9",
|
"spatie/phpunit-snapshot-assertions": "^5.1.5",
|
||||||
"squizlabs/php_codesniffer": "^3.4"
|
"squizlabs/php_codesniffer": "^3.9"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-imagick": "to generate QR code images"
|
"ext-imagick": "to generate QR code images"
|
||||||
@@ -56,9 +56,9 @@
|
|||||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/Bacon/BaconQrCode/issues",
|
"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",
|
"name": "dasprid/enum",
|
||||||
@@ -112,29 +112,26 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "endroid/qr-code",
|
"name": "endroid/qr-code",
|
||||||
"version": "4.8.5",
|
"version": "5.1.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/endroid/qr-code.git",
|
"url": "https://github.com/endroid/qr-code.git",
|
||||||
"reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77"
|
"reference": "393fec6c4cbdc1bd65570ac9d245704428010122"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/endroid/qr-code/zipball/0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
|
"url": "https://api.github.com/repos/endroid/qr-code/zipball/393fec6c4cbdc1bd65570ac9d245704428010122",
|
||||||
"reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
|
"reference": "393fec6c4cbdc1bd65570ac9d245704428010122",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"bacon/bacon-qr-code": "^2.0.5",
|
"bacon/bacon-qr-code": "^3.0",
|
||||||
"php": "^8.1"
|
"php": "^8.1"
|
||||||
},
|
},
|
||||||
"conflict": {
|
|
||||||
"khanamiryan/qrcode-detector-decoder": "^1.0.6"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"endroid/quality": "dev-master",
|
"endroid/quality": "dev-main",
|
||||||
"ext-gd": "*",
|
"ext-gd": "*",
|
||||||
"khanamiryan/qrcode-detector-decoder": "^1.0.4||^2.0.2",
|
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
|
||||||
"setasign/fpdf": "^1.8.2"
|
"setasign/fpdf": "^1.8.2"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
@@ -146,7 +143,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "4.x-dev"
|
"dev-main": "5.x-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -175,7 +172,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/endroid/qr-code/issues",
|
"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": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -183,7 +180,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-09-29T14:03:20+00:00"
|
"time": "2024-09-08T08:52:55+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "jumbojett/openid-connect-php",
|
"name": "jumbojett/openid-connect-php",
|
||||||
@@ -456,24 +453,25 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "robthree/twofactorauth",
|
"name": "robthree/twofactorauth",
|
||||||
"version": "1.8.2",
|
"version": "v3.0.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/RobThree/TwoFactorAuth.git",
|
"url": "https://github.com/RobThree/TwoFactorAuth.git",
|
||||||
"reference": "65681de5a324eae05140ac58b08648a60212afc0"
|
"reference": "6d70f9ca8e25568f163a7b3b3ff77bd8ea743978"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/65681de5a324eae05140ac58b08648a60212afc0",
|
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/6d70f9ca8e25568f163a7b3b3ff77bd8ea743978",
|
||||||
"reference": "65681de5a324eae05140ac58b08648a60212afc0",
|
"reference": "6d70f9ca8e25568f163a7b3b3ff77bd8ea743978",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.6.0"
|
"php": ">=8.2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
"friendsofphp/php-cs-fixer": "^3.13",
|
||||||
"phpunit/phpunit": "@stable"
|
"phpstan/phpstan": "^1.9",
|
||||||
|
"phpunit/phpunit": "^9"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
|
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
|
||||||
@@ -494,6 +492,16 @@
|
|||||||
"name": "Rob Janssen",
|
"name": "Rob Janssen",
|
||||||
"homepage": "http://robiii.me",
|
"homepage": "http://robiii.me",
|
||||||
"role": "Developer"
|
"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",
|
"description": "Two Factor Authentication",
|
||||||
@@ -522,7 +530,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2022-03-22T16:11:07+00:00"
|
"time": "2024-10-24T15:14:25+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
|
|||||||
28
config.php
28
config.php
@@ -1,5 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
// config.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 constants.
|
||||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||||
@@ -11,6 +26,10 @@ define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
|||||||
define('TIMEZONE', 'America/New_York');
|
define('TIMEZONE', 'America/New_York');
|
||||||
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
||||||
define('TOTAL_UPLOAD_SIZE', '5G');
|
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);
|
date_default_timezone_set(TIMEZONE);
|
||||||
|
|
||||||
@@ -48,9 +67,12 @@ function decryptData($encryptedData, $encryptionKey)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load encryption key from environment (override in production).
|
// Load encryption key from environment (override in production).
|
||||||
$encryptionKey = getenv('PERSISTENT_TOKENS_KEY') ?: 'default_please_change_this_key';
|
$envKey = getenv('PERSISTENT_TOKENS_KEY');
|
||||||
if (!$encryptionKey) {
|
if ($envKey === false || $envKey === '') {
|
||||||
die('Encryption key for persistent tokens is not set.');
|
$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)
|
function loadUserPermissions($username)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ $destinationFolder = trim($data['destination']);
|
|||||||
$files = $data['files'];
|
$files = $data['files'];
|
||||||
|
|
||||||
// Validate folder names: allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
// 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)) {
|
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
|
||||||
echo json_encode(["error" => "Invalid source folder name."]);
|
echo json_encode(["error" => "Invalid source folder name."]);
|
||||||
exit;
|
exit;
|
||||||
@@ -104,7 +104,7 @@ $destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($dest
|
|||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
// 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) {
|
foreach ($files as $fileName) {
|
||||||
// Save the original name for metadata lookup.
|
// Save the original name for metadata lookup.
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ $folderName = trim($input['folderName']);
|
|||||||
$parent = isset($input['parent']) ? trim($input['parent']) : "";
|
$parent = isset($input['parent']) ? trim($input['parent']) : "";
|
||||||
|
|
||||||
// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces in folderName
|
// 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.']);
|
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally, sanitize the parent folder if needed.
|
// 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.']);
|
echo json_encode(['success' => false, 'error' => 'Invalid parent folder name.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ if (!$input) {
|
|||||||
exit;
|
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']) : "";
|
$folder = isset($input['folder']) ? trim($input['folder']) : "";
|
||||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||||
$password = isset($input['password']) ? $input['password'] : "";
|
$password = isset($input['password']) ? $input['password'] : "";
|
||||||
@@ -17,7 +27,7 @@ $allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0;
|
|||||||
|
|
||||||
// Validate folder name using regex.
|
// Validate folder name using regex.
|
||||||
// Allow letters, numbers, underscores, hyphens, spaces and slashes.
|
// Allow letters, numbers, underscores, hyphens, spaces and 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."]);
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,23 @@ if (!$input) {
|
|||||||
exit;
|
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']) : "";
|
$folder = isset($input['folder']) ? trim($input['folder']) : "";
|
||||||
$file = isset($input['file']) ? basename($input['file']) : "";
|
$file = isset($input['file']) ? basename($input['file']) : "";
|
||||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||||
$password = isset($input['password']) ? $input['password'] : "";
|
$password = isset($input['password']) ? $input['password'] : "";
|
||||||
|
|
||||||
// Validate folder using regex.
|
// 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."]);
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
124
css/styles.css
124
css/styles.css
@@ -32,8 +32,8 @@ body {
|
|||||||
|
|
||||||
@media (min-width: 1300px) {
|
@media (min-width: 1300px) {
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
padding-left: 40px !important;
|
padding-left: 30px !important;
|
||||||
padding-right: 40px !important;
|
padding-right: 30px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 65px;
|
height: 55px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: #2196F3;
|
background-color: #2196F3;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
@@ -82,28 +82,16 @@ body.dark-mode .header-container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
max-height: 60px;
|
max-height: 50px;
|
||||||
width: auto;
|
width: auto;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-logo svg {
|
.header-logo svg {
|
||||||
height: 60px;
|
height: 50px;
|
||||||
width: auto;
|
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 {
|
body.dark-mode header {
|
||||||
background-color: #1f1f1f;
|
background-color: #1f1f1f;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
@@ -1584,39 +1572,6 @@ body.dark-mode .btn-secondary {
|
|||||||
border-color: #6c757d;
|
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 {
|
body.dark-mode .btn-danger {
|
||||||
background-color: #dc3545;
|
background-color: #dc3545;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -1729,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 {
|
body.dark-mode .CodeMirror {
|
||||||
background: #1e1e1e !important;
|
background: #1e1e1e !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
@@ -2161,3 +2101,57 @@ body.dark-mode .header-drop-zone.drag-active {
|
|||||||
body.dark-mode #fileSummary {
|
body.dark-mode #fileSummary {
|
||||||
color: white;
|
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);
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ if (!isset($data['files']) || !is_array($data['files'])) {
|
|||||||
$folder = isset($data['folder']) ? trim($data['folder']) : 'root';
|
$folder = isset($data['folder']) ? trim($data['folder']) : 'root';
|
||||||
|
|
||||||
// Validate folder: allow letters, numbers, underscores, dashes, spaces, and forward slashes
|
// 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."]);
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ $movedFiles = [];
|
|||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
// Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces.
|
// 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) {
|
foreach ($data['files'] as $fileName) {
|
||||||
$basename = basename(trim($fileName));
|
$basename = basename(trim($fileName));
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ if ($folderName === 'root') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
// 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.']);
|
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ $deletedFiles = [];
|
|||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
// Define a safe file name pattern.
|
// Define a safe file name pattern.
|
||||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||||
|
|
||||||
foreach ($filesToDelete as $trashName) {
|
foreach ($filesToDelete as $trashName) {
|
||||||
$trashName = trim($trashName);
|
$trashName = trim($trashName);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ $file = isset($_GET['file']) ? basename($_GET['file']) : '';
|
|||||||
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
||||||
|
|
||||||
// Validate file name (allowing letters, numbers, underscores, dashes, dots, and parentheses)
|
// 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);
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Invalid file name."]);
|
echo json_encode(["error" => "Invalid file name."]);
|
||||||
exit;
|
exit;
|
||||||
@@ -80,10 +80,6 @@ if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
|||||||
}
|
}
|
||||||
header('Content-Length: ' . filesize($realFilePath));
|
header('Content-Length: ' . filesize($realFilePath));
|
||||||
|
|
||||||
// Disable caching.
|
|
||||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
|
||||||
header('Pragma: no-cache');
|
|
||||||
|
|
||||||
readfile($realFilePath);
|
readfile($realFilePath);
|
||||||
exit;
|
exit;
|
||||||
?>
|
?>
|
||||||
@@ -79,10 +79,6 @@ if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
|||||||
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
|
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable caching.
|
|
||||||
header("Cache-Control: no-store, no-cache, must-revalidate");
|
|
||||||
header("Pragma: no-cache");
|
|
||||||
|
|
||||||
// Read and output the file.
|
// Read and output the file.
|
||||||
readfile($realFilePath);
|
readfile($realFilePath);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ $files = $data['files'];
|
|||||||
if ($folder !== "root") {
|
if ($folder !== "root") {
|
||||||
$parts = explode('/', $folder);
|
$parts = explode('/', $folder);
|
||||||
foreach ($parts as $part) {
|
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);
|
http_response_code(400);
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode(["error" => "Invalid folder name."]);
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
@@ -76,7 +76,7 @@ if (empty($files)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($files as $fileName) {
|
foreach ($files as $fileName) {
|
||||||
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $fileName)) {
|
if (!preg_match(REGEX_FILE_NAME, $fileName)) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ if (empty($files)) {
|
|||||||
if ($folder !== "root") {
|
if ($folder !== "root") {
|
||||||
$parts = explode('/', $folder);
|
$parts = explode('/', $folder);
|
||||||
foreach ($parts as $part) {
|
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);
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Invalid folder name."]);
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
exit;
|
exit;
|
||||||
@@ -92,7 +92,7 @@ $destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($dest
|
|||||||
$errors = [];
|
$errors = [];
|
||||||
$allSuccess = true;
|
$allSuccess = true;
|
||||||
$extractedFiles = array(); // Array to collect names of extracted files
|
$extractedFiles = array(); // Array to collect names of extracted files
|
||||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||||
|
|
||||||
// ---------- Process Each File ----------
|
// ---------- Process Each File ----------
|
||||||
foreach ($files as $zipFileName) {
|
foreach ($files as $zipFileName) {
|
||||||
|
|||||||
@@ -11,14 +11,24 @@ if (file_exists($configFile)) {
|
|||||||
echo json_encode(['error' => 'Failed to decrypt configuration.']);
|
echo json_encode(['error' => 'Failed to decrypt configuration.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
// Decode the configuration and ensure globalOtpauthUrl is set
|
// Decode the configuration and ensure required fields are set
|
||||||
$config = json_decode($decryptedContent, true);
|
$config = json_decode($decryptedContent, true);
|
||||||
|
|
||||||
|
// Ensure globalOtpauthUrl is set
|
||||||
if (!isset($config['globalOtpauthUrl'])) {
|
if (!isset($config['globalOtpauthUrl'])) {
|
||||||
$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);
|
echo json_encode($config);
|
||||||
} else {
|
} else {
|
||||||
|
// If no config file exists, provide defaults
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
|
'header_title' => "FileRise",
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
'providerUrl' => 'https://your-oidc-provider.com',
|
'providerUrl' => 'https://your-oidc-provider.com',
|
||||||
'clientId' => 'YOUR_CLIENT_ID',
|
'clientId' => 'YOUR_CLIENT_ID',
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once 'config.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');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure user is authenticated
|
// Ensure user is authenticated
|
||||||
@@ -14,7 +11,7 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
|
|
||||||
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
||||||
// Allow only safe characters in the folder parameter (letters, numbers, underscores, dashes, spaces, and forward slashes).
|
// 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."]);
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -28,11 +25,6 @@ if ($folder !== 'root') {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper: Generate the metadata file path for a given folder.
|
* 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) {
|
function getMetadataFilePath($folder) {
|
||||||
if (strtolower($folder) === 'root' || $folder === '') {
|
if (strtolower($folder) === 'root' || $folder === '') {
|
||||||
@@ -53,7 +45,7 @@ $files = array_values(array_diff(scandir($directory), array('.', '..')));
|
|||||||
$fileList = [];
|
$fileList = [];
|
||||||
|
|
||||||
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
// 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) {
|
foreach ($files as $file) {
|
||||||
// Skip hidden files (those that begin with a dot)
|
// 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.
|
// Since metadata is stored per folder, the key is simply the file name.
|
||||||
$metaKey = $file;
|
$metaKey = $file;
|
||||||
|
|
||||||
$fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown";
|
$fileDateModified = filemtime($filePath) ? date(DATE_TIME_FORMAT, filemtime($filePath)) : "Unknown";
|
||||||
$fileUploadedDate = isset($metadata[$metaKey]["uploaded"]) ? $metadata[$metaKey]["uploaded"] : "Unknown";
|
$fileUploadedDate = isset($metadata[$metaKey]["uploaded"]) ? $metadata[$metaKey]["uploaded"] : "Unknown";
|
||||||
$fileUploader = isset($metadata[$metaKey]["uploader"]) ? $metadata[$metaKey]["uploader"] : "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));
|
$fileSizeFormatted = sprintf("%s bytes", number_format($fileSizeBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
$fileList[] = [
|
// Build the basic file entry.
|
||||||
|
$fileEntry = [
|
||||||
'name' => $file,
|
'name' => $file,
|
||||||
'modified' => $fileDateModified,
|
'modified' => $fileDateModified,
|
||||||
'uploaded' => $fileUploadedDate,
|
'uploaded' => $fileUploadedDate,
|
||||||
@@ -96,6 +88,14 @@ foreach ($files as $file) {
|
|||||||
'uploader' => $fileUploader,
|
'uploader' => $fileUploader,
|
||||||
'tags' => isset($metadata[$metaKey]['tags']) ? $metadata[$metaKey]['tags'] : []
|
'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.
|
// Load global tags from createdTags.json.
|
||||||
|
|||||||
40
getFileTag.php
Normal file
40
getFileTag.php
Normal 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;
|
||||||
@@ -20,7 +20,7 @@ function getSubfolders($dir, $relative = '') {
|
|||||||
$folders = [];
|
$folders = [];
|
||||||
$items = scandir($dir);
|
$items = scandir($dir);
|
||||||
// Allow letters, numbers, underscores, dashes, and spaces in folder names.
|
// Allow letters, numbers, underscores, dashes, and spaces in folder names.
|
||||||
$safeFolderNamePattern = '/^[A-Za-z0-9_\- ]+$/';
|
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if ($item === '.' || $item === '..') continue;
|
if ($item === '.' || $item === '..') continue;
|
||||||
if (!preg_match($safeFolderNamePattern, $item)) {
|
if (!preg_match($safeFolderNamePattern, $item)) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ if (file_exists($usersFile)) {
|
|||||||
$parts = explode(':', trim($line));
|
$parts = explode(':', trim($line));
|
||||||
if (count($parts) >= 3) {
|
if (count($parts) >= 3) {
|
||||||
// Validate username format:
|
// Validate username format:
|
||||||
if (preg_match('/^[A-Za-z0-9_\- ]+$/', $parts[0])) {
|
if (preg_match(REGEX_USER, $parts[0])) {
|
||||||
$users[] = [
|
$users[] = [
|
||||||
"username" => $parts[0],
|
"username" => $parts[0],
|
||||||
"role" => trim($parts[2])
|
"role" => trim($parts[2])
|
||||||
|
|||||||
177
index.html
177
index.html
@@ -41,75 +41,80 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
||||||
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
|
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
|
||||||
crossorigin="anonymous"></script>
|
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" />
|
<link rel="stylesheet" href="css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header class="header-container">
|
<header class="header-container">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<div class="header-logo">
|
<a href="index.html">
|
||||||
<svg version="1.1" id="filingCabinetLogo" xmlns="http://www.w3.org/2000/svg"
|
<div class="header-logo">
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" xml:space="preserve">
|
<svg version="1.1" id="filingCabinetLogo" xmlns="http://www.w3.org/2000/svg"
|
||||||
<defs>
|
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" xml:space="preserve">
|
||||||
<!-- Gradient for the cabinet body -->
|
<defs>
|
||||||
<linearGradient id="cabinetGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
<!-- Gradient for the cabinet body -->
|
||||||
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
<linearGradient id="cabinetGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||||
</linearGradient>
|
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
||||||
<!-- Drop shadow filter with animated attributes for a lifting effect -->
|
</linearGradient>
|
||||||
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
|
<!-- Drop shadow filter with animated attributes for a lifting effect -->
|
||||||
<feDropShadow id="dropShadow" dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2">
|
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
<!-- Animate the vertical offset: from 2 to 1 (as it rises), hold, then back to 2 -->
|
<feDropShadow id="dropShadow" dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2">
|
||||||
<animate attributeName="dy" values="2;1;1;2" keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
<!-- Animate the vertical offset: from 2 to 1 (as it rises), hold, then back to 2 -->
|
||||||
<!-- Animate the blur similarly: from 2 to 1.5 then back to 2 -->
|
<animate attributeName="dy" values="2;1;1;2" keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
||||||
<animate attributeName="stdDeviation" values="2;1.5;1.5;2" keyTimes="0;0.2;0.8;1" dur="5s"
|
<!-- Animate the blur similarly: from 2 to 1.5 then back to 2 -->
|
||||||
fill="freeze" />
|
<animate attributeName="stdDeviation" values="2;1.5;1.5;2" keyTimes="0;0.2;0.8;1" dur="5s"
|
||||||
</feDropShadow>
|
fill="freeze" />
|
||||||
</filter>
|
</feDropShadow>
|
||||||
</defs>
|
</filter>
|
||||||
<style type="text/css">
|
</defs>
|
||||||
/* Cabinet with gradient, white outline, and drop shadow */
|
<style type="text/css">
|
||||||
.cabinet {
|
/* Cabinet with gradient, white outline, and drop shadow */
|
||||||
fill: url(#cabinetGradient);
|
.cabinet {
|
||||||
stroke: white;
|
fill: url(#cabinetGradient);
|
||||||
stroke-width: 2;
|
stroke: white;
|
||||||
}
|
stroke-width: 2;
|
||||||
|
}
|
||||||
.divider {
|
|
||||||
stroke: #1565C0;
|
.divider {
|
||||||
stroke-width: 1.5;
|
stroke: #1565C0;
|
||||||
}
|
stroke-width: 1.5;
|
||||||
|
}
|
||||||
.drawer {
|
|
||||||
fill: #FFFFFF;
|
.drawer {
|
||||||
}
|
fill: #FFFFFF;
|
||||||
|
}
|
||||||
.handle {
|
|
||||||
fill: #1565C0;
|
.handle {
|
||||||
}
|
fill: #1565C0;
|
||||||
</style>
|
}
|
||||||
<!-- Group that will animate upward and then back down once -->
|
</style>
|
||||||
<g id="cabinetGroup">
|
<!-- Group that will animate upward and then back down once -->
|
||||||
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
|
<g id="cabinetGroup">
|
||||||
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
|
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
|
||||||
<!-- Divider lines for drawers -->
|
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
|
||||||
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
|
<!-- Divider lines for drawers -->
|
||||||
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
|
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
|
||||||
<!-- Drawers with Handles -->
|
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
|
||||||
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
|
<!-- Drawers with Handles -->
|
||||||
<circle cx="54" cy="27" r="1.5" class="handle" />
|
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||||
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
|
<circle cx="54" cy="27" r="1.5" class="handle" />
|
||||||
<circle cx="54" cy="39" r="1.5" class="handle" />
|
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||||
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
|
<circle cx="54" cy="39" r="1.5" class="handle" />
|
||||||
<circle cx="54" cy="51" r="1.5" class="handle" />
|
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||||
<!-- Additional detail: a small top handle on the cabinet door -->
|
<circle cx="54" cy="51" r="1.5" class="handle" />
|
||||||
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
|
<!-- Additional detail: a small top handle on the cabinet door -->
|
||||||
<!-- Animate transform: rises by 2 pixels over 1s, holds for 3s, then falls over 1s (total 5s) -->
|
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
|
||||||
<animateTransform attributeName="transform" type="translate" values="0 0; 0 -2; 0 -2; 0 0"
|
<!-- Animate transform: rises by 2 pixels over 1s, holds for 3s, then falls over 1s (total 5s) -->
|
||||||
keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
<animateTransform attributeName="transform" type="translate" values="0 0; 0 -2; 0 -2; 0 0"
|
||||||
</g>
|
keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
||||||
</svg>
|
</g>
|
||||||
</div>
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
<h1 data-i18n-key="header_title">FileRise</h1>
|
<h1 data-i18n-key="header_title">FileRise</h1>
|
||||||
@@ -245,7 +250,7 @@
|
|||||||
<div id="folderTreeContainer"></div>
|
<div id="folderTreeContainer"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="folder-actions mt-3">
|
<div class="folder-actions mt-3">
|
||||||
<button id="createFolderBtn" class="btn btn-primary">
|
<button id="createFolderBtn" class="btn btn-primary" data-i18n-title="create_folder">
|
||||||
<i class="material-icons">create_new_folder</i>
|
<i class="material-icons">create_new_folder</i>
|
||||||
</button>
|
</button>
|
||||||
<div id="createFolderModal" class="modal">
|
<div id="createFolderModal" class="modal">
|
||||||
@@ -262,7 +267,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="renameFolderBtn" class="btn btn-warning ml-2">
|
<button id="renameFolderBtn" class="btn btn-warning ml-2" data-i18n-title="rename_folder">
|
||||||
<i class="material-icons">drive_file_rename_outline</i>
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
</button>
|
</button>
|
||||||
<div id="renameFolderModal" class="modal">
|
<div id="renameFolderModal" class="modal">
|
||||||
@@ -386,34 +391,36 @@
|
|||||||
</div> <!-- end mainColumn -->
|
</div> <!-- end mainColumn -->
|
||||||
</div> <!-- end main-wrapper -->
|
</div> <!-- end main-wrapper -->
|
||||||
|
|
||||||
<!-- Download Progress Modal -->
|
<!-- Download Progress Modal -->
|
||||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||||
<!-- Material icon spinner with a dedicated class -->
|
<!-- Material icon spinner with a dedicated class -->
|
||||||
<span class="material-icons download-spinner">autorenew</span>
|
<span class="material-icons download-spinner">autorenew</span>
|
||||||
<p>Preparing your download...</p>
|
<p data-i18n-key="preparing_download">Preparing your download...</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Single File Download Modal -->
|
<!-- Single File Download Modal -->
|
||||||
<div id="downloadFileModal" class="modal" style="display: none;">
|
<div id="downloadFileModal" class="modal" style="display: none;">
|
||||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||||
<h4>Download File</h4>
|
<h4 data-i18n-key="download_file">Download File</h4>
|
||||||
<p>Confirm or change the download file name:</p>
|
<p data-i18n-key="confirm_or_change_filename">Confirm or change the download file name:</p>
|
||||||
<input type="text" id="downloadFileNameInput" class="form-control" placeholder="Filename" />
|
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename" placeholder="Filename" />
|
||||||
<div style="margin-top: 15px; text-align: right;">
|
<div style="margin-top: 15px; text-align: right;">
|
||||||
<button id="cancelDownloadFile" class="btn btn-secondary"
|
<button id="cancelDownloadFile" class="btn btn-secondary"
|
||||||
onclick="document.getElementById('downloadFileModal').style.display = 'none';">Cancel</button>
|
onclick="document.getElementById('downloadFileModal').style.display = 'none';"
|
||||||
<button id="confirmSingleDownloadButton" class="btn btn-primary"
|
data-i18n-key="cancel">Cancel</button>
|
||||||
onclick="confirmSingleDownload()">Download</button>
|
<button id="confirmSingleDownloadButton" class="btn btn-primary"
|
||||||
</div>
|
onclick="confirmSingleDownload()"
|
||||||
|
data-i18n-key="download">Download</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
||||||
<div id="changePasswordModal" class="modal" style="display:none;">
|
<div id="changePasswordModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content" style="max-width:400px; margin:auto;">
|
<div class="modal-content" style="max-width:400px; margin:auto;">
|
||||||
<span id="closeChangePasswordModal" style="cursor:pointer;">×</span>
|
<span id="closeChangePasswordModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
||||||
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
||||||
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
||||||
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
||||||
|
|||||||
75
js/auth.js
75
js/auth.js
@@ -1,5 +1,5 @@
|
|||||||
import { sendRequest } from './networkUtils.js';
|
import { sendRequest } from './networkUtils.js';
|
||||||
import { t } from './i18n.js';
|
import { t, applyTranslations } from './i18n.js';
|
||||||
import {
|
import {
|
||||||
toggleVisibility,
|
toggleVisibility,
|
||||||
showToast as originalShowToast,
|
showToast as originalShowToast,
|
||||||
@@ -97,18 +97,36 @@ function loadAdminConfigFunc() {
|
|||||||
return fetch("getConfig.php", { credentials: "include" })
|
return fetch("getConfig.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(config => {
|
.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("disableFormLogin", config.loginOptions.disableFormLogin);
|
||||||
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
|
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
|
||||||
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
|
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
|
||||||
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
|
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
|
||||||
|
|
||||||
|
// Update the UI for login options
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
|
|
||||||
|
const headerTitleElem = document.querySelector(".header-title h1");
|
||||||
|
if (headerTitleElem) {
|
||||||
|
headerTitleElem.textContent = config.header_title || "FileRise";
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
// Fallback defaults in case of error
|
||||||
|
localStorage.setItem("headerTitle", "FileRise");
|
||||||
localStorage.setItem("disableFormLogin", "false");
|
localStorage.setItem("disableFormLogin", "false");
|
||||||
localStorage.setItem("disableBasicAuth", "false");
|
localStorage.setItem("disableBasicAuth", "false");
|
||||||
localStorage.setItem("disableOIDCLogin", "false");
|
localStorage.setItem("disableOIDCLogin", "false");
|
||||||
localStorage.setItem("globalOtpauthUrl", "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
|
localStorage.setItem("globalOtpauthUrl", "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
|
|
||||||
|
const headerTitleElem = document.querySelector(".header-title h1");
|
||||||
|
if (headerTitleElem) {
|
||||||
|
headerTitleElem.textContent = "FileRise";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,10 +150,11 @@ function updateAuthenticatedUI(data) {
|
|||||||
if (data.username) {
|
if (data.username) {
|
||||||
localStorage.setItem("username", data.username);
|
localStorage.setItem("username", data.username);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
if (typeof data.folderOnly !== "undefined") {
|
if (typeof data.folderOnly !== "undefined") {
|
||||||
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
const headerButtons = document.querySelector(".header-buttons");
|
const headerButtons = document.querySelector(".header-buttons");
|
||||||
const firstButton = headerButtons.firstElementChild;
|
const firstButton = headerButtons.firstElementChild;
|
||||||
|
|
||||||
@@ -145,7 +164,8 @@ function updateAuthenticatedUI(data) {
|
|||||||
restoreBtn = document.createElement("button");
|
restoreBtn = document.createElement("button");
|
||||||
restoreBtn.id = "restoreFilesBtn";
|
restoreBtn.id = "restoreFilesBtn";
|
||||||
restoreBtn.classList.add("btn", "btn-warning");
|
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);
|
if (firstButton) insertAfter(restoreBtn, firstButton);
|
||||||
else headerButtons.appendChild(restoreBtn);
|
else headerButtons.appendChild(restoreBtn);
|
||||||
}
|
}
|
||||||
@@ -156,7 +176,8 @@ function updateAuthenticatedUI(data) {
|
|||||||
adminPanelBtn = document.createElement("button");
|
adminPanelBtn = document.createElement("button");
|
||||||
adminPanelBtn.id = "adminPanelBtn";
|
adminPanelBtn.id = "adminPanelBtn";
|
||||||
adminPanelBtn.classList.add("btn", "btn-info");
|
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);
|
insertAfter(adminPanelBtn, restoreBtn);
|
||||||
adminPanelBtn.addEventListener("click", openAdminPanel);
|
adminPanelBtn.addEventListener("click", openAdminPanel);
|
||||||
} else {
|
} else {
|
||||||
@@ -175,17 +196,19 @@ function updateAuthenticatedUI(data) {
|
|||||||
userPanelBtn = document.createElement("button");
|
userPanelBtn = document.createElement("button");
|
||||||
userPanelBtn.id = "userPanelBtn";
|
userPanelBtn.id = "userPanelBtn";
|
||||||
userPanelBtn.classList.add("btn", "btn-user");
|
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");
|
const adminBtn = document.getElementById("adminPanelBtn");
|
||||||
if (adminBtn) insertAfter(userPanelBtn, adminBtn);
|
if (adminBtn) insertAfter(userPanelBtn, adminBtn);
|
||||||
else if (firstButton) insertAfter(userPanelBtn, firstButton);
|
else if (firstButton) insertAfter(userPanelBtn, firstButton);
|
||||||
else headerButtons.appendChild(userPanelBtn);
|
else headerButtons.appendChild(userPanelBtn);
|
||||||
userPanelBtn.addEventListener("click", openUserPanel);
|
userPanelBtn.addEventListener("click", openUserPanel);
|
||||||
} else {
|
} else {
|
||||||
userPanelBtn.style.display = "block";
|
userPanelBtn.style.display = "block";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applyTranslations();
|
||||||
updateItemsPerPageSelect();
|
updateItemsPerPageSelect();
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
}
|
}
|
||||||
@@ -227,15 +250,29 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
function submitLogin(data) {
|
function submitLogin(data) {
|
||||||
setLastLoginData(data);
|
setLastLoginData(data);
|
||||||
window.__lastLoginData = 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 => {
|
.then(response => {
|
||||||
if (response.success || response.status === "ok") {
|
if (response.success || response.status === "ok") {
|
||||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
|
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
|
||||||
window.location.reload();
|
// Fetch and update permissions, then reload.
|
||||||
} else if (response.totp_required) {
|
sendRequest("getUserPermissions.php", "GET")
|
||||||
openTOTPLoginModal();
|
.then(permissionData => {
|
||||||
} else if (response.error && response.error.includes("Too many failed login attempts")) {
|
if (permissionData && typeof permissionData === "object") {
|
||||||
showToast(response.error);
|
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")) {
|
||||||
|
showToast(response.error);
|
||||||
const loginButton = document.getElementById("authForm").querySelector("button[type='submit']");
|
const loginButton = document.getElementById("authForm").querySelector("button[type='submit']");
|
||||||
if (loginButton) {
|
if (loginButton) {
|
||||||
loginButton.disabled = true;
|
loginButton.disabled = true;
|
||||||
@@ -293,7 +330,7 @@ function loadUserList() {
|
|||||||
closeRemoveUserModal();
|
closeRemoveUserModal();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
}
|
}
|
||||||
window.loadUserList = loadUserList;
|
window.loadUserList = loadUserList;
|
||||||
|
|
||||||
@@ -320,7 +357,7 @@ function initAuth() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "X-CSRF-Token": window.csrfToken }
|
headers: { "X-CSRF-Token": window.csrfToken }
|
||||||
}).then(() => window.location.reload(true)).catch(() => {});
|
}).then(() => window.location.reload(true)).catch(() => { });
|
||||||
});
|
});
|
||||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||||
resetUserForm();
|
resetUserForm();
|
||||||
@@ -386,7 +423,7 @@ function initAuth() {
|
|||||||
showToast("Error: " + (data.error || "Could not remove user"));
|
showToast("Error: " + (data.error || "Could not remove user"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
});
|
});
|
||||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
||||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||||
|
|||||||
342
js/authModals.js
342
js/authModals.js
@@ -2,8 +2,9 @@ import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.
|
|||||||
import { sendRequest } from './networkUtils.js';
|
import { sendRequest } from './networkUtils.js';
|
||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||||
|
|
||||||
const version = "v1.1.1";
|
const version = "v1.1.3";
|
||||||
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
|
// 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;
|
let lastLoginData = null;
|
||||||
export function setLastLoginData(data) {
|
export function setLastLoginData(data) {
|
||||||
@@ -44,7 +45,7 @@ export function openTOTPLoginModal() {
|
|||||||
<input type="text" id="recoveryInput"
|
<input type="text" id="recoveryInput"
|
||||||
style="font-size:24px; text-align:center; width:100%; padding:10px;"
|
style="font-size:24px; text-align:center; width:100%; padding:10px;"
|
||||||
placeholder="Recovery code" />
|
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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -66,12 +67,12 @@ export function openTOTPLoginModal() {
|
|||||||
// Switch to recovery
|
// Switch to recovery
|
||||||
totpSection.style.display = "none";
|
totpSection.style.display = "none";
|
||||||
recoverySection.style.display = "block";
|
recoverySection.style.display = "block";
|
||||||
toggleLink.textContent = "Use TOTP Code instead";
|
toggleLink.textContent = t("use_totp_code_instead");
|
||||||
} else {
|
} else {
|
||||||
// Switch back to TOTP
|
// Switch back to TOTP
|
||||||
recoverySection.style.display = "none";
|
recoverySection.style.display = "none";
|
||||||
totpSection.style.display = "block";
|
totpSection.style.display = "block";
|
||||||
toggleLink.textContent = "Use Recovery Code instead";
|
toggleLink.textContent = t("use_recovery_code_instead");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ export function openTOTPLoginModal() {
|
|||||||
document.getElementById("submitRecovery").addEventListener("click", () => {
|
document.getElementById("submitRecovery").addEventListener("click", () => {
|
||||||
const recoveryCode = document.getElementById("recoveryInput").value.trim();
|
const recoveryCode = document.getElementById("recoveryInput").value.trim();
|
||||||
if (!recoveryCode) {
|
if (!recoveryCode) {
|
||||||
showToast("Please enter your recovery code.");
|
showToast(t("please_enter_recovery_code"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("totp_recover.php", {
|
fetch("totp_recover.php", {
|
||||||
@@ -97,11 +98,11 @@ export function openTOTPLoginModal() {
|
|||||||
// recovery succeeded → finalize login
|
// recovery succeeded → finalize login
|
||||||
window.location.href = "index.html";
|
window.location.href = "index.html";
|
||||||
} else {
|
} else {
|
||||||
showToast(json.message || "Recovery code verification failed");
|
showToast(json.message || t("recovery_code_verification_failed"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
showToast("Error verifying recovery code.");
|
showToast(t("error_verifying_recovery_code"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,14 +126,14 @@ export function openTOTPLoginModal() {
|
|||||||
if (json.status === "ok") {
|
if (json.status === "ok") {
|
||||||
window.location.href = "index.html";
|
window.location.href = "index.html";
|
||||||
} else {
|
} else {
|
||||||
showToast(json.message || "TOTP verification failed");
|
showToast(json.message || t("totp_verification_failed"));
|
||||||
this.value = "";
|
this.value = "";
|
||||||
totpLoginModal.style.display = "flex";
|
totpLoginModal.style.display = "flex";
|
||||||
totpInput.focus();
|
totpInput.focus();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
showToast("TOTP verification failed");
|
showToast(t("totp_verification_failed"));
|
||||||
this.value = "";
|
this.value = "";
|
||||||
totpLoginModal.style.display = "flex";
|
totpLoginModal.style.display = "flex";
|
||||||
totpInput.focus();
|
totpInput.focus();
|
||||||
@@ -162,9 +163,9 @@ export function openUserPanel() {
|
|||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
position: relative;
|
position: fixed;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 90vh;
|
max-height: 350px !important;
|
||||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||||
transform: none;
|
transform: none;
|
||||||
transition: none;
|
transition: none;
|
||||||
@@ -187,26 +188,26 @@ export function openUserPanel() {
|
|||||||
z-index: 3000;
|
z-index: 3000;
|
||||||
`;
|
`;
|
||||||
userPanelModal.innerHTML = `
|
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;">×</span>
|
<span id="closeUserPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||||
<h3>User Panel (${username})</h3>
|
<h3>${t("user_panel")} (${username})</h3>
|
||||||
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">Change Password</button>
|
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">${t("change_password")}</button>
|
||||||
<fieldset style="margin-bottom: 15px;">
|
<fieldset style="margin-bottom: 15px;">
|
||||||
<legend>TOTP Settings</legend>
|
<legend>${t("totp_settings")}</legend>
|
||||||
<div class="form-group">
|
<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;" />
|
<input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset style="margin-bottom: 15px;">
|
<fieldset style="margin-bottom: 15px;">
|
||||||
<legend>Language</legend>
|
<legend>${t("language")}</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="languageSelector">Select Language:</label>
|
<label for="languageSelector">${t("select_language")}:</label>
|
||||||
<select id="languageSelector">
|
<select id="languageSelector">
|
||||||
<option value="en">English</option>
|
<option value="en">${t("english")}</option>
|
||||||
<option value="es">Español</option>
|
<option value="es">${t("spanish")}</option>
|
||||||
<option value="fr">Français</option>
|
<option value="fr">${t("french")}</option>
|
||||||
<option value="de">Deutsch</option>
|
<option value="de">${t("german")}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -239,12 +240,12 @@ export function openUserPanel() {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
showToast("Error updating TOTP setting: " + result.error);
|
showToast(t("error_updating_totp_setting") + ": " + result.error);
|
||||||
} else if (enabled) {
|
} else if (enabled) {
|
||||||
openTOTPModal();
|
openTOTPModal();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => { showToast("Error updating TOTP setting."); });
|
.catch(() => { showToast(t("error_updating_totp_setting")); });
|
||||||
});
|
});
|
||||||
// Language dropdown initialization
|
// Language dropdown initialization
|
||||||
const languageSelector = document.getElementById("languageSelector");
|
const languageSelector = document.getElementById("languageSelector");
|
||||||
@@ -283,10 +284,10 @@ function showRecoveryCodeModal(recoveryCode) {
|
|||||||
`;
|
`;
|
||||||
recoveryModal.innerHTML = `
|
recoveryModal.innerHTML = `
|
||||||
<div style="background: #fff; color: #000; padding: 20px; max-width: 400px; width: 90%; border-radius: 8px; text-align: center;">
|
<div style="background: #fff; color: #000; padding: 20px; max-width: 400px; width: 90%; border-radius: 8px; text-align: center;">
|
||||||
<h3>Your Recovery Code</h3>
|
<h3>${t("your_recovery_code")}</h3>
|
||||||
<p>Please save this code securely. It will not be shown again and can only be used once.</p>
|
<p>${t("please_save_recovery_code")}</p>
|
||||||
<code style="display: block; margin: 10px 0; font-size: 20px;">${recoveryCode}</code>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(recoveryModal);
|
document.body.appendChild(recoveryModal);
|
||||||
@@ -325,19 +326,21 @@ export function openTOTPModal() {
|
|||||||
z-index: 3100;
|
z-index: 3100;
|
||||||
`;
|
`;
|
||||||
totpModal.innerHTML = `
|
totpModal.innerHTML = `
|
||||||
<div class="modal-content" style="${modalContentStyles}">
|
<div class="modal-content" style="${modalContentStyles}">
|
||||||
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||||
<h3>TOTP Setup</h3>
|
<h3>${t("totp_setup")}</h3>
|
||||||
<p>Scan this QR code with your authenticator app:</p>
|
<p>${t("scan_qr_code")}</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;">
|
<!-- Create an image placeholder without the CSRF token in the src -->
|
||||||
<br/>
|
<img id="totpQRCodeImage" src="" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||||
<p>Enter the 6-digit code from your app to confirm setup:</p>
|
<br/>
|
||||||
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
|
<p>${t("enter_totp_confirmation")}</p>
|
||||||
<br/><br/>
|
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
|
||||||
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button>
|
<br/><br/>
|
||||||
</div>
|
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">${t("confirm")}</button>
|
||||||
`;
|
</div>
|
||||||
|
`;
|
||||||
document.body.appendChild(totpModal);
|
document.body.appendChild(totpModal);
|
||||||
|
loadTOTPQRCode();
|
||||||
|
|
||||||
document.getElementById("closeTOTPModal").addEventListener("click", () => {
|
document.getElementById("closeTOTPModal").addEventListener("click", () => {
|
||||||
closeTOTPModal(true);
|
closeTOTPModal(true);
|
||||||
@@ -346,7 +349,7 @@ export function openTOTPModal() {
|
|||||||
document.getElementById("confirmTOTPBtn").addEventListener("click", function () {
|
document.getElementById("confirmTOTPBtn").addEventListener("click", function () {
|
||||||
const code = document.getElementById("totpConfirmInput").value.trim();
|
const code = document.getElementById("totpConfirmInput").value.trim();
|
||||||
if (code.length !== 6) {
|
if (code.length !== 6) {
|
||||||
showToast("Please enter a valid 6-digit code.");
|
showToast(t("please_enter_valid_code"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("totp_verify.php", {
|
fetch("totp_verify.php", {
|
||||||
@@ -361,7 +364,7 @@ export function openTOTPModal() {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.status === 'ok') {
|
if (result.status === 'ok') {
|
||||||
showToast("TOTP successfully enabled.");
|
showToast(t("totp_enabled_successfully"));
|
||||||
// After successful TOTP verification, fetch the recovery code
|
// After successful TOTP verification, fetch the recovery code
|
||||||
fetch("totp_saveCode.php", {
|
fetch("totp_saveCode.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -377,16 +380,16 @@ export function openTOTPModal() {
|
|||||||
// Show the recovery code in a secure modal
|
// Show the recovery code in a secure modal
|
||||||
showRecoveryCodeModal(data.recoveryCode);
|
showRecoveryCodeModal(data.recoveryCode);
|
||||||
} else {
|
} 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);
|
closeTOTPModal(false);
|
||||||
} else {
|
} 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
|
// Focus the input and attach enter key listener
|
||||||
@@ -406,6 +409,13 @@ export function openTOTPModal() {
|
|||||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
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
|
// Focus the input and attach enter key listener
|
||||||
const totpConfirmInput = document.getElementById("totpConfirmInput");
|
const totpConfirmInput = document.getElementById("totpConfirmInput");
|
||||||
if (totpConfirmInput) {
|
if (totpConfirmInput) {
|
||||||
@@ -419,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
|
// Updated closeTOTPModal function with a disable parameter
|
||||||
export function closeTOTPModal(disable = true) {
|
export function closeTOTPModal(disable = true) {
|
||||||
const totpModal = document.getElementById("totpModal");
|
const totpModal = document.getElementById("totpModal");
|
||||||
@@ -443,17 +480,88 @@ export function closeTOTPModal(disable = true) {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (!result.success) {
|
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() {
|
export function openAdminPanel() {
|
||||||
fetch("getConfig.php", { credentials: "include" })
|
fetch("getConfig.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(config => {
|
.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.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||||
@@ -487,77 +595,84 @@ export function openAdminPanel() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 3000;
|
z-index: 3000;
|
||||||
`;
|
`;
|
||||||
// Added a version number next to "Admin Panel"
|
|
||||||
adminModal.innerHTML = `
|
adminModal.innerHTML = `
|
||||||
<div class="modal-content" style="${modalContentStyles}">
|
<div class="modal-content" style="${modalContentStyles}">
|
||||||
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||||
<h3>
|
<h3>${adminTitle}</h3>
|
||||||
<h3>${adminTitle}</h3>
|
|
||||||
</h3>
|
|
||||||
<form id="adminPanelForm">
|
<form id="adminPanelForm">
|
||||||
<fieldset style="margin-bottom: 15px;">
|
<fieldset style="margin-bottom: 15px;">
|
||||||
<legend>User Management</legend>
|
<legend>${t("user_management")}</legend>
|
||||||
<div style="display: flex; gap: 10px;">
|
<div style="display: flex; gap: 10px;">
|
||||||
<button type="button" id="adminOpenAddUser" class="btn btn-success">Add User</button>
|
<button type="button" id="adminOpenAddUser" class="btn btn-success">${t("add_user")}</button>
|
||||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">Remove User</button>
|
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">${t("remove_user")}</button>
|
||||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">User Permissions</button>
|
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset style="margin-bottom: 15px;">
|
<fieldset style="margin-bottom: 15px;">
|
||||||
<legend>OIDC Configuration</legend>
|
<legend>Header Settings</legend>
|
||||||
<div class="form-group">
|
<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}" />
|
<input type="text" id="oidcProviderUrl" class="form-control" value="${window.currentOIDCConfig.providerUrl}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<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}" />
|
<input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<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}" />
|
<input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<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}" />
|
<input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset style="margin-bottom: 15px;">
|
<fieldset style="margin-bottom: 15px;">
|
||||||
<legend>Global TOTP Settings</legend>
|
<legend>${t("global_totp_settings")}</legend>
|
||||||
<div class="form-group">
|
<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'}" />
|
<input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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;">
|
<div style="display: flex; justify-content: space-between;">
|
||||||
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">Cancel</button>
|
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">${t("cancel")}</button>
|
||||||
<button type="button" id="saveAdminSettings" class="btn btn-primary">Save Settings</button>
|
<button type="button" id="saveAdminSettings" class="btn btn-primary">${t("save_settings")}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(adminModal);
|
document.body.appendChild(adminModal);
|
||||||
|
|
||||||
|
// Bind closing events that will use our enhanced close function.
|
||||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||||
adminModal.addEventListener("click", (e) => {
|
adminModal.addEventListener("click", (e) => {
|
||||||
if (e.target === adminModal) closeAdminPanel();
|
if (e.target === adminModal) closeAdminPanel();
|
||||||
});
|
});
|
||||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||||
|
|
||||||
|
// Bind other buttons.
|
||||||
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById("newUsername").focus();
|
document.getElementById("newUsername").focus();
|
||||||
@@ -568,7 +683,6 @@ export function openAdminPanel() {
|
|||||||
}
|
}
|
||||||
toggleVisibility("removeUserModal", true);
|
toggleVisibility("removeUserModal", true);
|
||||||
});
|
});
|
||||||
// New event binding for the User Permissions button:
|
|
||||||
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
||||||
openUserPermissionsModal();
|
openUserPermissionsModal();
|
||||||
});
|
});
|
||||||
@@ -578,7 +692,7 @@ export function openAdminPanel() {
|
|||||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||||
if (totalDisabled === 3) {
|
if (totalDisabled === 3) {
|
||||||
showToast("At least one login method must remain enabled.");
|
showToast(t("at_least_one_login_method"));
|
||||||
disableOIDCLoginCheckbox.checked = false;
|
disableOIDCLoginCheckbox.checked = false;
|
||||||
localStorage.setItem("disableOIDCLogin", "false");
|
localStorage.setItem("disableOIDCLogin", "false");
|
||||||
if (typeof window.updateLoginOptionsUI === "function") {
|
if (typeof window.updateLoginOptionsUI === "function") {
|
||||||
@@ -590,6 +704,7 @@ export function openAdminPanel() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
|
||||||
const newOIDCConfig = {
|
const newOIDCConfig = {
|
||||||
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||||
clientId: document.getElementById("oidcClientId").value.trim(),
|
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||||
@@ -601,6 +716,7 @@ export function openAdminPanel() {
|
|||||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
||||||
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
||||||
sendRequest("updateConfig.php", "POST", {
|
sendRequest("updateConfig.php", "POST", {
|
||||||
|
header_title: newHeaderTitle,
|
||||||
oidc: newOIDCConfig,
|
oidc: newOIDCConfig,
|
||||||
disableFormLogin,
|
disableFormLogin,
|
||||||
disableBasicAuth,
|
disableBasicAuth,
|
||||||
@@ -609,27 +725,31 @@ export function openAdminPanel() {
|
|||||||
}, { "X-CSRF-Token": window.csrfToken })
|
}, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
showToast("Settings updated successfully.");
|
showToast(t("settings_updated_successfully"));
|
||||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||||
if (typeof window.updateLoginOptionsUI === "function") {
|
if (typeof window.updateLoginOptionsUI === "function") {
|
||||||
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||||
}
|
}
|
||||||
|
// Update the captured initial state since the changes have now been saved.
|
||||||
|
captureInitialAdminConfig();
|
||||||
closeAdminPanel();
|
closeAdminPanel();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
showToast("Error updating settings: " + (response.error || "Unknown error"));
|
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => { });
|
||||||
});
|
});
|
||||||
|
// Enforce login option constraints.
|
||||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||||
function enforceLoginOptionConstraint(changedCheckbox) {
|
function enforceLoginOptionConstraint(changedCheckbox) {
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||||
if (changedCheckbox.checked && totalDisabled === 3) {
|
if (changedCheckbox.checked && totalDisabled === 3) {
|
||||||
showToast("At least one login method must remain enabled.");
|
showToast(t("at_least_one_login_method"));
|
||||||
changedCheckbox.checked = false;
|
changedCheckbox.checked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -640,6 +760,9 @@ export function openAdminPanel() {
|
|||||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||||
|
|
||||||
|
// Capture initial state after the modal loads.
|
||||||
|
captureInitialAdminConfig();
|
||||||
} else {
|
} else {
|
||||||
adminModal.style.backgroundColor = overlayBackground;
|
adminModal.style.backgroundColor = overlayBackground;
|
||||||
const modalContent = adminModal.querySelector(".modal-content");
|
const modalContent = adminModal.querySelector(".modal-content");
|
||||||
@@ -657,6 +780,7 @@ export function openAdminPanel() {
|
|||||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||||
adminModal.style.display = "flex";
|
adminModal.style.display = "flex";
|
||||||
|
captureInitialAdminConfig();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -678,19 +802,25 @@ export function openAdminPanel() {
|
|||||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
||||||
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
||||||
adminModal.style.display = "flex";
|
adminModal.style.display = "flex";
|
||||||
|
captureInitialAdminConfig();
|
||||||
} else {
|
} else {
|
||||||
openAdminPanel();
|
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");
|
const adminModal = document.getElementById("adminPanelModal");
|
||||||
if (adminModal) adminModal.style.display = "none";
|
if (adminModal) adminModal.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- New: User Permissions Modal ---
|
// --- New: User Permissions Modal ---
|
||||||
|
|
||||||
export function openUserPermissionsModal() {
|
export function openUserPermissionsModal() {
|
||||||
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
||||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||||
@@ -723,13 +853,13 @@ export function openUserPermissionsModal() {
|
|||||||
userPermissionsModal.innerHTML = `
|
userPermissionsModal.innerHTML = `
|
||||||
<div class="modal-content" style="${modalContentStyles}">
|
<div class="modal-content" style="${modalContentStyles}">
|
||||||
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||||
<h3>User Permissions</h3>
|
<h3>${t("user_permissions")}</h3>
|
||||||
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
|
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
|
||||||
<!-- User rows will be loaded here -->
|
<!-- User rows will be loaded here -->
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
<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="cancelUserPermissionsBtn" class="btn btn-secondary">${t("cancel")}</button>
|
||||||
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">Save Permissions</button>
|
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">${t("save_permissions")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -760,14 +890,14 @@ export function openUserPermissionsModal() {
|
|||||||
sendRequest("updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
sendRequest("updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
showToast("User permissions updated successfully.");
|
showToast(t("user_permissions_updated_successfully"));
|
||||||
userPermissionsModal.style.display = "none";
|
userPermissionsModal.style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
showToast("Error updating permissions: " + (response.error || "Unknown error"));
|
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
showToast("Error updating permissions.");
|
showToast(t("error_updating_permissions"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -792,20 +922,26 @@ function loadUserPermissionsList() {
|
|||||||
.then(usersData => {
|
.then(usersData => {
|
||||||
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
listContainer.innerHTML = "<p>No users found.</p>";
|
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users.forEach(user => {
|
users.forEach(user => {
|
||||||
// Skip admin users.
|
// Skip admin users.
|
||||||
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
|
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 = {
|
const defaultPerm = {
|
||||||
folderOnly: localStorage.getItem("folderOnly") === "true",
|
folderOnly: false,
|
||||||
readOnly: localStorage.getItem("readOnly") === "true",
|
readOnly: false,
|
||||||
disableUpload: localStorage.getItem("disableUpload") === "true"
|
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.
|
// Create a row for the user.
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
@@ -817,15 +953,15 @@ function loadUserPermissionsList() {
|
|||||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||||
<label style="display: flex; align-items: center; gap: 5px;">
|
<label style="display: flex; align-items: center; gap: 5px;">
|
||||||
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
|
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
|
||||||
User Folder Only
|
${t("user_folder_only")}
|
||||||
</label>
|
</label>
|
||||||
<label style="display: flex; align-items: center; gap: 5px;">
|
<label style="display: flex; align-items: center; gap: 5px;">
|
||||||
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
|
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
|
||||||
Read Only
|
${t("read_only")}
|
||||||
</label>
|
</label>
|
||||||
<label style="display: flex; align-items: center; gap: 5px;">
|
<label style="display: flex; align-items: center; gap: 5px;">
|
||||||
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
|
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
|
||||||
Disable Upload
|
${t("disable_upload")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
||||||
@@ -835,6 +971,6 @@ function loadUserPermissionsList() {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
listContainer.innerHTML = "<p>Error loading users.</p>";
|
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,23 +90,36 @@ export function showToast(message, duration = 3000) {
|
|||||||
|
|
||||||
export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) {
|
export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) {
|
||||||
const safeSearchTerm = escapeHTML(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 `
|
return `
|
||||||
<div class="row align-items-center mb-3">
|
<div class="row align-items-center mb-3">
|
||||||
<div class="col-12 col-md-8 mb-2 mb-md-0">
|
<div class="col-12 col-md-8 mb-2 mb-md-0">
|
||||||
<div class="input-group">
|
<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">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text" id="searchIcon">
|
<span class="input-group-text" id="searchIcon">
|
||||||
<i class="material-icons">search</i>
|
<i class="material-icons">search</i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="searchInput" class="form-control" placeholder="${t("search_placeholder")}" 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>
|
</div>
|
||||||
<div class="col-12 col-md-4 text-left">
|
<div class="col-12 col-md-4 text-left">
|
||||||
<div class="d-flex justify-content-center justify-content-md-start align-items-center">
|
<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>
|
<button class="custom-prev-next-btn" ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">${t("prev")}</button>
|
||||||
<span class="page-indicator">Page ${currentPage} of ${totalPages || 1}</span>
|
<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})">Next</button>
|
<button class="custom-prev-next-btn" ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">${t("next")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,7 +137,7 @@ export function buildFileTableHeader(sortOrder) {
|
|||||||
<th data-column="uploaded" class="hide-small hide-medium sortable-col">${t("upload_date")} ${sortOrder.column === "uploaded" ? (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="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 data-column="uploader" class="hide-small hide-medium sortable-col">${t("uploader")} ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
|
||||||
<th>Actions</th>
|
<th>${t("actions")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
`;
|
`;
|
||||||
@@ -168,36 +181,38 @@ export function buildFileTableRow(file, folderPath) {
|
|||||||
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
|
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
|
||||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||||
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
|
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
|
||||||
title="Download">
|
title="${t('download')}">
|
||||||
<i class="material-icons">file_download</i>
|
<i class="material-icons">file_download</i>
|
||||||
</button>
|
</button>
|
||||||
${file.editable ? `
|
${file.editable ? `
|
||||||
<button class="btn btn-sm edit-btn"
|
<button class="btn btn-sm edit-btn"
|
||||||
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||||
title="Edit">
|
title="${t('edit')}">
|
||||||
<i class="material-icons">edit</i>
|
<i class="material-icons">edit</i>
|
||||||
</button>
|
</button>
|
||||||
` : ""}
|
` : ""}
|
||||||
${previewButton}
|
${previewButton}
|
||||||
<button class="btn btn-sm btn-warning rename-btn"
|
<button class="btn btn-sm btn-warning rename-btn"
|
||||||
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
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>
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildBottomControls(itemsPerPageSetting) {
|
export function buildBottomControls(itemsPerPageSetting) {
|
||||||
return `
|
return `
|
||||||
<div class="d-flex align-items-center mt-3 bottom-controls">
|
<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)">
|
<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>
|
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ export function loadSidebarOrder() {
|
|||||||
modal = document.createElement('div');
|
modal = document.createElement('div');
|
||||||
modal.className = 'header-card-modal';
|
modal.className = 'header-card-modal';
|
||||||
modal.style.position = 'fixed';
|
modal.style.position = 'fixed';
|
||||||
modal.style.top = '80px';
|
modal.style.top = '55px';
|
||||||
modal.style.right = '80px';
|
modal.style.right = '80px';
|
||||||
modal.style.zIndex = '11000';
|
modal.style.zIndex = '11000';
|
||||||
// Render the modal but initially keep it hidden.
|
// Render the modal but initially keep it hidden.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// dragDrop.js
|
// fileDragDrop.js
|
||||||
import { showToast } from './domUtils.js';
|
import { showToast } from './domUtils.js';
|
||||||
import { loadFileList } from './fileListView.js';
|
import { loadFileList } from './fileListView.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// editor.js
|
// fileEditor.js
|
||||||
import { escapeHTML, showToast } from './domUtils.js';
|
import { escapeHTML, showToast } from './domUtils.js';
|
||||||
import { loadFileList } from './fileListView.js';
|
import { loadFileList } from './fileListView.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ window.itemsPerPage = window.itemsPerPage || 10;
|
|||||||
window.currentPage = window.currentPage || 1;
|
window.currentPage = window.currentPage || 1;
|
||||||
window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery"
|
window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery"
|
||||||
|
|
||||||
|
// Global flag for advanced search mode.
|
||||||
|
window.advancedSearchEnabled = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --- Helper Functions ---
|
* --- Helper Functions ---
|
||||||
*/
|
*/
|
||||||
@@ -33,11 +36,8 @@ window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "ga
|
|||||||
*/
|
*/
|
||||||
function parseSizeToBytes(sizeStr) {
|
function parseSizeToBytes(sizeStr) {
|
||||||
if (!sizeStr) return 0;
|
if (!sizeStr) return 0;
|
||||||
// Remove any whitespace
|
|
||||||
let s = sizeStr.trim();
|
let s = sizeStr.trim();
|
||||||
// Extract the numerical part.
|
|
||||||
let value = parseFloat(s);
|
let value = parseFloat(s);
|
||||||
// Determine if there is a unit. Convert the unit to uppercase for easier matching.
|
|
||||||
let upper = s.toUpperCase();
|
let upper = s.toUpperCase();
|
||||||
if (upper.includes("KB")) {
|
if (upper.includes("KB")) {
|
||||||
value *= 1024;
|
value *= 1024;
|
||||||
@@ -50,7 +50,7 @@ function parseSizeToBytes(sizeStr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format the total bytes as a human-readable string, choosing an appropriate unit.
|
* Format the total bytes as a human-readable string.
|
||||||
*/
|
*/
|
||||||
function formatSize(totalBytes) {
|
function formatSize(totalBytes) {
|
||||||
if (totalBytes < 1024) {
|
if (totalBytes < 1024) {
|
||||||
@@ -66,41 +66,104 @@ function formatSize(totalBytes) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the folder summary HTML using the filtered file list.
|
* Build the folder summary HTML using the filtered file list.
|
||||||
* This function sums the file sizes in bytes correctly, then formats the total.
|
|
||||||
*/
|
*/
|
||||||
function buildFolderSummary(filteredFiles) {
|
function buildFolderSummary(filteredFiles) {
|
||||||
const totalFiles = filteredFiles.length;
|
const totalFiles = filteredFiles.length;
|
||||||
const totalBytes = filteredFiles.reduce((sum, file) => {
|
const totalBytes = filteredFiles.reduce((sum, file) => {
|
||||||
// file.size might be something like "456.9KB" or just "1024".
|
|
||||||
return sum + parseSizeToBytes(file.size);
|
return sum + parseSizeToBytes(file.size);
|
||||||
}, 0);
|
}, 0);
|
||||||
const sizeStr = formatSize(totalBytes);
|
const sizeStr = formatSize(totalBytes);
|
||||||
return `<strong>Total Files:</strong> ${totalFiles} | <strong>Total Size:</strong> ${sizeStr}`;
|
return `<strong>${t('total_files')}:</strong> ${totalFiles} | <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 ---
|
* --- VIEW MODE TOGGLE BUTTON & Helpers ---
|
||||||
*/
|
*/
|
||||||
export function createViewToggleButton() {
|
export function createViewToggleButton() {
|
||||||
let toggleBtn = document.getElementById("toggleViewBtn");
|
let toggleBtn = document.getElementById("toggleViewBtn");
|
||||||
if (!toggleBtn) {
|
if (!toggleBtn) {
|
||||||
toggleBtn = document.createElement("button");
|
toggleBtn = document.createElement("button");
|
||||||
toggleBtn.id = "toggleViewBtn";
|
toggleBtn.id = "toggleViewBtn";
|
||||||
toggleBtn.classList.add("btn", "btn-secondary");
|
toggleBtn.classList.add("btn", "btn-toggleview");
|
||||||
const titleElem = document.getElementById("fileListTitle");
|
|
||||||
if (titleElem) {
|
// Set initial icon and tooltip based on current view mode.
|
||||||
titleElem.parentNode.insertBefore(toggleBtn, titleElem.nextSibling);
|
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" ? t("switch_to_table_view") : t("switch_to_gallery_view");
|
|
||||||
toggleBtn.onclick = () => {
|
toggleBtn.onclick = () => {
|
||||||
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
|
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
|
||||||
localStorage.setItem("viewMode", window.viewMode);
|
localStorage.setItem("viewMode", window.viewMode);
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
toggleBtn.textContent = window.viewMode === "gallery" ? t("switch_to_table_view") : t("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;
|
return toggleBtn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatFolderName(folder) {
|
export function formatFolderName(folder) {
|
||||||
if (folder === "root") return "(Root)";
|
if (folder === "root") return "(Root)";
|
||||||
@@ -134,7 +197,15 @@ export function loadFileList(folderParam) {
|
|||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
fileListContainer.innerHTML = ""; // Clear loading message.
|
fileListContainer.innerHTML = ""; // Clear loading message.
|
||||||
if (data.files && data.files.length > 0) {
|
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 => {
|
data.files = data.files.map(file => {
|
||||||
file.fullName = (file.path || file.name).trim().toLowerCase();
|
file.fullName = (file.path || file.name).trim().toLowerCase();
|
||||||
file.editable = canEditFile(file.name);
|
file.editable = canEditFile(file.name);
|
||||||
@@ -142,11 +213,13 @@ export function loadFileList(folderParam) {
|
|||||||
if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||||
file.type = "image";
|
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;
|
return file;
|
||||||
});
|
});
|
||||||
fileData = data.files;
|
fileData = data.files;
|
||||||
|
|
||||||
// Update the file list actions area without removing existing buttons.
|
// Update file summary.
|
||||||
const actionsContainer = document.getElementById("fileListActions");
|
const actionsContainer = document.getElementById("fileListActions");
|
||||||
if (actionsContainer) {
|
if (actionsContainer) {
|
||||||
let summaryElem = document.getElementById("fileSummary");
|
let summaryElem = document.getElementById("fileSummary");
|
||||||
@@ -164,7 +237,7 @@ export function loadFileList(folderParam) {
|
|||||||
summaryElem.innerHTML = buildFolderSummary(fileData);
|
summaryElem.innerHTML = buildFolderSummary(fileData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the view normally.
|
// Render view based on the view mode.
|
||||||
if (window.viewMode === "gallery") {
|
if (window.viewMode === "gallery") {
|
||||||
renderGalleryView(folder);
|
renderGalleryView(folder);
|
||||||
} else {
|
} else {
|
||||||
@@ -193,8 +266,7 @@ export function loadFileList(folderParam) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update renderFileTable so that it writes its content into the provided container.
|
* Update renderFileTable so it writes its content into the provided container.
|
||||||
* If no container is provided, it defaults to the element with id "fileList".
|
|
||||||
*/
|
*/
|
||||||
export function renderFileTable(folder, container) {
|
export function renderFileTable(folder, container) {
|
||||||
const fileListContent = container || document.getElementById("fileList");
|
const fileListContent = container || document.getElementById("fileList");
|
||||||
@@ -202,11 +274,9 @@ export function renderFileTable(folder, container) {
|
|||||||
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
||||||
let currentPage = window.currentPage || 1;
|
let currentPage = window.currentPage || 1;
|
||||||
|
|
||||||
const filteredFiles = fileData.filter(file => {
|
// Use Fuse.js search via our helper function.
|
||||||
const nameMatch = file.name.toLowerCase().includes(searchTerm);
|
const filteredFiles = searchFiles(searchTerm);
|
||||||
const tagMatch = file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
|
|
||||||
return nameMatch || tagMatch;
|
|
||||||
});
|
|
||||||
const totalFiles = filteredFiles.length;
|
const totalFiles = filteredFiles.length;
|
||||||
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
|
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
|
||||||
if (currentPage > totalPages) {
|
if (currentPage > totalPages) {
|
||||||
@@ -217,11 +287,15 @@ export function renderFileTable(folder, container) {
|
|||||||
? "uploads/"
|
? "uploads/"
|
||||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||||
|
|
||||||
|
// Build the top controls and append the advanced search toggle button.
|
||||||
const topControlsHTML = buildSearchAndPaginationControls({
|
const topControlsHTML = buildSearchAndPaginationControls({
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
searchTerm: window.currentSearchTerm || ""
|
searchTerm: window.currentSearchTerm || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const combinedTopHTML = topControlsHTML;
|
||||||
|
|
||||||
let headerHTML = buildFileTableHeader(sortOrder);
|
let headerHTML = buildFileTableHeader(sortOrder);
|
||||||
const startIndex = (currentPage - 1) * itemsPerPageSetting;
|
const startIndex = (currentPage - 1) * itemsPerPageSetting;
|
||||||
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
|
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
|
||||||
@@ -242,7 +316,7 @@ export function renderFileTable(folder, container) {
|
|||||||
rowHTML = rowHTML.replace(/(<td class="file-name-cell">)(.*?)(<\/td>)/, (match, p1, p2, p3) => {
|
rowHTML = rowHTML.replace(/(<td class="file-name-cell">)(.*?)(<\/td>)/, (match, p1, p2, p3) => {
|
||||||
return p1 + p2 + tagBadgesHTML + 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>
|
<i class="material-icons">share</i>
|
||||||
</button>$1`);
|
</button>$1`);
|
||||||
rowsHTML += rowHTML;
|
rowsHTML += rowHTML;
|
||||||
@@ -253,11 +327,11 @@ export function renderFileTable(folder, container) {
|
|||||||
rowsHTML += "</tbody></table>";
|
rowsHTML += "</tbody></table>";
|
||||||
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
|
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
|
||||||
|
|
||||||
fileListContent.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||||
|
|
||||||
createViewToggleButton();
|
createViewToggleButton();
|
||||||
|
|
||||||
// Setup event listeners as before...
|
// Setup event listeners.
|
||||||
const newSearchInput = document.getElementById("searchInput");
|
const newSearchInput = document.getElementById("searchInput");
|
||||||
if (newSearchInput) {
|
if (newSearchInput) {
|
||||||
newSearchInput.addEventListener("input", debounce(function () {
|
newSearchInput.addEventListener("input", debounce(function () {
|
||||||
@@ -299,7 +373,7 @@ export function renderFileTable(folder, container) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
updateFileActionButtons();
|
updateFileActionButtons();
|
||||||
document.querySelectorAll("#fileListContent tbody tr").forEach(row => {
|
document.querySelectorAll("#fileList tbody tr").forEach(row => {
|
||||||
row.setAttribute("draggable", "true");
|
row.setAttribute("draggable", "true");
|
||||||
import('./fileDragDrop.js').then(module => {
|
import('./fileDragDrop.js').then(module => {
|
||||||
row.addEventListener("dragstart", module.fileDragStartHandler);
|
row.addEventListener("dragstart", module.fileDragStartHandler);
|
||||||
@@ -317,10 +391,8 @@ export function renderFileTable(folder, container) {
|
|||||||
export function renderGalleryView(folder, container) {
|
export function renderGalleryView(folder, container) {
|
||||||
const fileListContent = container || document.getElementById("fileList");
|
const fileListContent = container || document.getElementById("fileList");
|
||||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||||
const filteredFiles = fileData.filter(file => {
|
// Use Fuse.js search for gallery view as well.
|
||||||
return file.name.toLowerCase().includes(searchTerm) ||
|
const filteredFiles = searchFiles(searchTerm);
|
||||||
(file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm)));
|
|
||||||
});
|
|
||||||
const folderPath = folder === "root"
|
const folderPath = folder === "root"
|
||||||
? "uploads/"
|
? "uploads/"
|
||||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||||
@@ -348,23 +420,23 @@ export function renderGalleryView(folder, container) {
|
|||||||
${thumbnail}
|
${thumbnail}
|
||||||
</div>
|
</div>
|
||||||
<div class="gallery-info" style="margin-top: 5px;">
|
<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}
|
${tagBadgesHTML}
|
||||||
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
|
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
|
||||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||||
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
|
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
|
||||||
title="Download">
|
title="${t('download')}">
|
||||||
<i class="material-icons">file_download</i>
|
<i class="material-icons">file_download</i>
|
||||||
</button>
|
</button>
|
||||||
${file.editable ? `
|
${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>
|
<i class="material-icons">edit</i>
|
||||||
</button>
|
</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>
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
</button>
|
</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>
|
<i class="material-icons">share</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -477,4 +549,5 @@ window.changeItemsPerPage = function (newCount) {
|
|||||||
window.loadFileList = loadFileList;
|
window.loadFileList = loadFileList;
|
||||||
window.renderFileTable = renderFileTable;
|
window.renderFileTable = renderFileTable;
|
||||||
window.renderGalleryView = renderGalleryView;
|
window.renderGalleryView = renderGalleryView;
|
||||||
window.sortFiles = sortFiles;
|
window.sortFiles = sortFiles;
|
||||||
|
window.toggleAdvancedSearch = toggleAdvancedSearch;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// contextMenu.js
|
// fileMenu.js
|
||||||
import { updateRowHighlight, showToast } from './domUtils.js';
|
import { updateRowHighlight, showToast } from './domUtils.js';
|
||||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile } from './fileActions.js';
|
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile } from './fileActions.js';
|
||||||
import { previewFile } from './filePreview.js';
|
import { previewFile } from './filePreview.js';
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function openShareModal(file, folder) {
|
|||||||
<option value="240">240 minutes</option>
|
<option value="240">240 minutes</option>
|
||||||
<option value="1440">1 Day</option>
|
<option value="1440">1 Day</option>
|
||||||
</select>
|
</select>
|
||||||
<p>Password (optional):</p>
|
<p>${t("password_optional")}</p>
|
||||||
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
|
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
|
||||||
<br>
|
<br>
|
||||||
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
|
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ if (localStorage.getItem('globalTags')) {
|
|||||||
|
|
||||||
// New function to load global tags from the server's persistent JSON.
|
// New function to load global tags from the server's persistent JSON.
|
||||||
export function loadGlobalTags() {
|
export function loadGlobalTags() {
|
||||||
fetch("metadata/createdTags.json", { credentials: "include" })
|
fetch("getFileTag.php", { credentials: "include" })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// If the file doesn't exist, assume there are no global tags.
|
// If the file doesn't exist, assume there are no global tags.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function openFolderShareModal(folder) {
|
|||||||
<option value="1440">1 ${t("day")}</option>
|
<option value="1440">1 ${t("day")}</option>
|
||||||
</select>
|
</select>
|
||||||
<p>${t("password_optional")}</p>
|
<p>${t("password_optional")}</p>
|
||||||
<input type="text" id="folderSharePassword" placeholder="${t("password")}" style="width: 100%;"/>
|
<input type="text" id="folderSharePassword" placeholder="${t("enter_password")}" style="width: 100%;"/>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
|
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
|
||||||
|
|||||||
1559
js/i18n.js
1559
js/i18n.js
File diff suppressed because it is too large
Load Diff
27
js/main.js
27
js/main.js
@@ -12,7 +12,8 @@ import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload }
|
|||||||
import { editFile, saveFile } from './fileEditor.js';
|
import { editFile, saveFile } from './fileEditor.js';
|
||||||
import { t, applyTranslations, setLocale } from './i18n.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' })
|
return fetch('token.php', { credentials: 'include' })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -21,11 +22,9 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Set global variables.
|
|
||||||
window.csrfToken = data.csrf_token;
|
window.csrfToken = data.csrf_token;
|
||||||
window.SHARE_URL = data.share_url;
|
window.SHARE_URL = data.share_url;
|
||||||
|
|
||||||
// Update (or create) the CSRF meta tag.
|
|
||||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
||||||
if (!metaCSRF) {
|
if (!metaCSRF) {
|
||||||
metaCSRF = document.createElement('meta');
|
metaCSRF = document.createElement('meta');
|
||||||
@@ -34,7 +33,6 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
|||||||
}
|
}
|
||||||
metaCSRF.setAttribute('content', data.csrf_token);
|
metaCSRF.setAttribute('content', data.csrf_token);
|
||||||
|
|
||||||
// Update (or create) the share URL meta tag.
|
|
||||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
let metaShare = document.querySelector('meta[name="share-url"]');
|
||||||
if (!metaShare) {
|
if (!metaShare) {
|
||||||
metaShare = document.createElement('meta');
|
metaShare = document.createElement('meta');
|
||||||
@@ -44,15 +42,6 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
|||||||
metaShare.setAttribute('content', data.share_url);
|
metaShare.setAttribute('content', data.share_url);
|
||||||
|
|
||||||
return data;
|
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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +67,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
// Apply the translations to update the UI
|
// Apply the translations to update the UI
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
// First, load the CSRF token (with retry).
|
// First, load the CSRF token (with retry).
|
||||||
loadCsrfTokenWithRetry().then(() => {
|
loadCsrfToken().then(() => {
|
||||||
// Once CSRF token is loaded, initialize authentication.
|
// Once CSRF token is loaded, initialize authentication.
|
||||||
initAuth();
|
initAuth();
|
||||||
|
|
||||||
@@ -139,18 +128,18 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
|
|
||||||
if (darkModeToggle) {
|
if (darkModeToggle) {
|
||||||
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
|
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
|
||||||
? "Light Mode"
|
? t("light_mode")
|
||||||
: "Dark Mode";
|
: t("dark_mode");
|
||||||
|
|
||||||
darkModeToggle.addEventListener("click", function () {
|
darkModeToggle.addEventListener("click", function () {
|
||||||
if (document.body.classList.contains("dark-mode")) {
|
if (document.body.classList.contains("dark-mode")) {
|
||||||
document.body.classList.remove("dark-mode");
|
document.body.classList.remove("dark-mode");
|
||||||
localStorage.setItem("darkMode", "false");
|
localStorage.setItem("darkMode", "false");
|
||||||
darkModeToggle.textContent = "Dark Mode";
|
darkModeToggle.textContent = t("dark_mode");
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.add("dark-mode");
|
document.body.classList.add("dark-mode");
|
||||||
localStorage.setItem("darkMode", "true");
|
localStorage.setItem("darkMode", "true");
|
||||||
darkModeToggle.textContent = "Light Mode";
|
darkModeToggle.textContent = t("light_mode");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ $username = trim($_SERVER['PHP_AUTH_USER']);
|
|||||||
$password = trim($_SERVER['PHP_AUTH_PW']);
|
$password = trim($_SERVER['PHP_AUTH_PW']);
|
||||||
|
|
||||||
// Validate username format (optional)
|
// 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('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||||
header('HTTP/1.0 401 Unauthorized');
|
header('HTTP/1.0 401 Unauthorized');
|
||||||
echo 'Invalid username format';
|
echo 'Invalid username format';
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
|
||||||
header("Pragma: no-cache");
|
|
||||||
header("Expires: 0");
|
|
||||||
|
|
||||||
// --- CSRF Protection ---
|
// --- CSRF Protection ---
|
||||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
@@ -45,7 +42,7 @@ $sourceFolder = trim($data['source']) ?: 'root';
|
|||||||
$destinationFolder = trim($data['destination']) ?: 'root';
|
$destinationFolder = trim($data['destination']) ?: 'root';
|
||||||
|
|
||||||
// Allow only letters, numbers, underscores, dashes, spaces, and forward slashes in folder names.
|
// 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)) {
|
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
|
||||||
echo json_encode(["error" => "Invalid source folder name."]);
|
echo json_encode(["error" => "Invalid source folder name."]);
|
||||||
exit;
|
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) : [];
|
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||||
|
|
||||||
foreach ($data['files'] as $fileName) {
|
foreach ($data['files'] as $fileName) {
|
||||||
// Save the original name for metadata lookup.
|
// Save the original name for metadata lookup.
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ if (!isset($_POST['folder'])) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$folder = $_POST['folder'];
|
$folder = urldecode($_POST['folder']);
|
||||||
// Validate the folder name (only alphanumerics, dashes allowed)
|
$regex = "/^resumable_" . PATTERN_FOLDER_NAME . "$/u"; // full regex pattern
|
||||||
if (!preg_match('/^resumable_[A-Za-z0-9\-]+$/', $folder)) {
|
if (!preg_match($regex, $folder)) {
|
||||||
echo json_encode(["error" => "Invalid folder name"]);
|
echo json_encode(["error" => "Invalid folder name"]);
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ if (!$usernameToRemove) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Optional: Validate the username format (allow letters, numbers, underscores, dashes, and spaces)
|
// 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"]);
|
echo json_encode(["error" => "Invalid username format"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($dat
|
|||||||
|
|
||||||
$folder = trim($data['folder']) ?: 'root';
|
$folder = trim($data['folder']) ?: 'root';
|
||||||
// For subfolders, allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
// 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"]);
|
echo json_encode(["error" => "Invalid folder name"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ $oldName = basename(trim($data['oldName']));
|
|||||||
$newName = basename(trim($data['newName']));
|
$newName = basename(trim($data['newName']));
|
||||||
|
|
||||||
// Validate file names: allow letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
// 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."]);
|
echo json_encode(["error" => "Invalid file name."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
header('Content-Type: application/json');
|
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
|
// Ensure user is authenticated
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
@@ -48,7 +45,7 @@ $oldFolder = trim($input['oldFolder']);
|
|||||||
$newFolder = trim($input['newFolder']);
|
$newFolder = trim($input['newFolder']);
|
||||||
|
|
||||||
// Validate folder names
|
// 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).']);
|
echo json_encode(['success' => false, 'error' => 'Invalid folder name(s).']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ if (!isset($data['files']) || !is_array($data['files'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define a safe file name pattern.
|
// Define a safe file name pattern.
|
||||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||||
|
|
||||||
$restoredItems = [];
|
$restoredItems = [];
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ $folder = isset($data["folder"]) ? trim($data["folder"]) : "root";
|
|||||||
|
|
||||||
// If a subfolder is provided, validate it.
|
// If a subfolder is provided, validate it.
|
||||||
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
// 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"]);
|
echo json_encode(["error" => "Invalid folder name"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,21 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CSRF Protection: validate token from header.
|
// CSRF Protection: validate token from header.
|
||||||
$headers = getallheaders();
|
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
if (!isset($headers['X-CSRF-Token']) || $headers['X-CSRF-Token'] !== $_SESSION['csrf_token']) {
|
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||||
echo json_encode(["error" => "Invalid CSRF token."]);
|
|
||||||
http_response_code(403);
|
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||||
exit;
|
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.
|
// Retrieve and sanitize input.
|
||||||
@@ -77,7 +87,7 @@ if ($file === "global") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate folder name.
|
// 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."]);
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify CSRF token from request headers.
|
// 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']) {
|
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||||
http_response_code(403);
|
respond('error', 403, 'Invalid CSRF token');
|
||||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ——— 2) CSRF check ———
|
// ——— 2) CSRF check ———
|
||||||
if (empty($_SERVER['HTTP_X_CSRF_TOKEN'])
|
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
|| $_SERVER['HTTP_X_CSRF_TOKEN'] !== ($_SESSION['csrf_token'] ?? '')) {
|
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||||
http_response_code(403);
|
|
||||||
error_log("Invalid CSRF token on recovery for IP {$_SERVER['REMOTE_ADDR']}");
|
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||||
exit(json_encode(['status'=>'error','message'=>'Invalid CSRF token']));
|
respond('error', 403, 'Invalid CSRF token');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ——— 3) Identify user to recover ———
|
// ——— 3) Identify user to recover ———
|
||||||
@@ -32,7 +32,7 @@ if (!$userId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ——— Validate userId format ———
|
// ——— Validate userId format ———
|
||||||
if (!preg_match('/^[A-Za-z0-9_\-]+$/', $userId)) {
|
if (!preg_match(REGEX_USER, $userId)) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
error_log("Invalid userId format: {$userId}");
|
error_log("Invalid userId format: {$userId}");
|
||||||
exit(json_encode(['status'=>'error','message'=>'Invalid user identifier']));
|
exit(json_encode(['status'=>'error','message'=>'Invalid user identifier']));
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2) CSRF check
|
// 2) CSRF check
|
||||||
if (empty($_SERVER['HTTP_X_CSRF_TOKEN'])
|
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
|| $_SERVER['HTTP_X_CSRF_TOKEN'] !== ($_SESSION['csrf_token'] ?? '')) {
|
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||||
http_response_code(403);
|
|
||||||
error_log("totp_saveCode: invalid CSRF token from IP {$_SERVER['REMOTE_ADDR']}");
|
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||||
exit(json_encode(['status'=>'error','message'=>'Invalid CSRF token']));
|
respond('error', 403, 'Invalid CSRF token');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Must be logged in
|
// 3) Must be logged in
|
||||||
@@ -29,7 +29,7 @@ if (empty($_SESSION['username'])) {
|
|||||||
|
|
||||||
// 4) Validate username format
|
// 4) Validate username format
|
||||||
$userId = $_SESSION['username'];
|
$userId = $_SESSION['username'];
|
||||||
if (!preg_match('/^[A-Za-z0-9_\-]+$/', $userId)) {
|
if (!preg_match(REGEX_USER, $userId)) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
error_log("totp_saveCode: invalid username format: {$userId}");
|
error_log("totp_saveCode: invalid username format: {$userId}");
|
||||||
exit(json_encode(['status'=>'error','message'=>'Invalid user identifier']));
|
exit(json_encode(['status'=>'error','message'=>'Invalid user identifier']));
|
||||||
|
|||||||
@@ -6,19 +6,35 @@ require_once 'config.php';
|
|||||||
|
|
||||||
use Endroid\QrCode\Builder\Builder;
|
use Endroid\QrCode\Builder\Builder;
|
||||||
use Endroid\QrCode\Writer\PngWriter;
|
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:
|
// Define the respond() helper if not already defined.
|
||||||
// ini_set('display_errors', 1);
|
if (!function_exists('respond')) {
|
||||||
// error_reporting(E_ALL);
|
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);
|
http_response_code(403);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify CSRF token provided as a GET parameter.
|
// Retrieve CSRF token from GET parameter or request headers.
|
||||||
if (!isset($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
|
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
|
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||||
|
|
||||||
|
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||||
|
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -108,7 +124,13 @@ function getGlobalOtpauthUrl() {
|
|||||||
return "";
|
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.
|
// Retrieve the current TOTP secret for the user.
|
||||||
$totpSecret = getUserTOTPSecret($username);
|
$totpSecret = getUserTOTPSecret($username);
|
||||||
@@ -120,8 +142,6 @@ if (!$totpSecret) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine the otpauth URL to use.
|
// 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();
|
$globalOtpauthUrl = getGlobalOtpauthUrl();
|
||||||
if (!empty($globalOtpauthUrl)) {
|
if (!empty($globalOtpauthUrl)) {
|
||||||
$label = "FileRise:" . $username;
|
$label = "FileRise:" . $username;
|
||||||
@@ -140,7 +160,6 @@ if (!empty($globalOtpauthUrl)) {
|
|||||||
$result = Builder::create()
|
$result = Builder::create()
|
||||||
->writer(new PngWriter())
|
->writer(new PngWriter())
|
||||||
->data($otpauthUrl)
|
->data($otpauthUrl)
|
||||||
->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
|
|
||||||
->build();
|
->build();
|
||||||
|
|
||||||
header('Content-Type: ' . $result->getMimeType());
|
header('Content-Type: ' . $result->getMimeType());
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ require_once 'config.php';
|
|||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
||||||
|
|
||||||
|
use RobThree\Auth\Algorithm;
|
||||||
|
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// standardized error helper
|
// standardized error helper
|
||||||
function respond($status, $code, $message, $data = []) {
|
function respond($status, $code, $message, $data = []) {
|
||||||
@@ -54,8 +57,9 @@ try {
|
|||||||
respond('error', 403, 'Not authenticated');
|
respond('error', 403, 'Not authenticated');
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSRF check
|
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||||
|
|
||||||
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||||
respond('error', 403, 'Invalid CSRF token');
|
respond('error', 403, 'Invalid CSRF token');
|
||||||
}
|
}
|
||||||
@@ -71,7 +75,13 @@ try {
|
|||||||
if (isset($_SESSION['pending_login_user'])) {
|
if (isset($_SESSION['pending_login_user'])) {
|
||||||
$username = $_SESSION['pending_login_user'];
|
$username = $_SESSION['pending_login_user'];
|
||||||
$totpSecret = $_SESSION['pending_login_secret'];
|
$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)) {
|
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||||
$_SESSION['totp_failures']++;
|
$_SESSION['totp_failures']++;
|
||||||
@@ -117,7 +127,14 @@ try {
|
|||||||
respond('error', 500, 'TOTP secret not found. Please set up TOTP again.');
|
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)) {
|
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||||
$_SESSION['totp_failures']++;
|
$_SESSION['totp_failures']++;
|
||||||
respond('error', 400, 'Invalid TOTP code');
|
respond('error', 400, 'Invalid TOTP code');
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ if (!is_array($data)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retrieve new header title, sanitize if necessary.
|
||||||
|
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
||||||
|
|
||||||
// Validate and sanitize OIDC configuration.
|
// Validate and sanitize OIDC configuration.
|
||||||
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
||||||
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
$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.
|
// Retrieve the global OTPAuth URL (new field). If not provided, default to an empty string.
|
||||||
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
|
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
|
||||||
|
|
||||||
// Prepare configuration array.
|
// Prepare configuration array including the header title.
|
||||||
$configUpdate = [
|
$configUpdate = [
|
||||||
|
'header_title' => $headerTitle, // New field for the header title
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
'providerUrl' => $oidcProviderUrl,
|
'providerUrl' => $oidcProviderUrl,
|
||||||
'clientId' => $oidcClientId,
|
'clientId' => $oidcClientId,
|
||||||
@@ -79,15 +83,10 @@ $encryptedContent = encryptData($plainTextConfig, $encryptionKey);
|
|||||||
|
|
||||||
// Attempt to write the new configuration.
|
// Attempt to write the new configuration.
|
||||||
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
|
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.");
|
error_log("updateConfig.php: Initial write failed, attempting to delete the old configuration file.");
|
||||||
|
|
||||||
// Delete the old file.
|
|
||||||
if (file_exists($configFile)) {
|
if (file_exists($configFile)) {
|
||||||
unlink($configFile);
|
unlink($configFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try writing again.
|
|
||||||
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
|
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
|
||||||
error_log("updateConfig.php: Failed to write configuration even after deletion.");
|
error_log("updateConfig.php: Failed to write configuration even after deletion.");
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
|||||||
@@ -40,16 +40,39 @@ if (file_exists($permissionsFile)) {
|
|||||||
$existingPermissions = [];
|
$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.
|
// Loop through each permission update.
|
||||||
foreach ($permissions as $perm) {
|
foreach ($permissions as $perm) {
|
||||||
// Ensure username is provided.
|
// Ensure username is provided.
|
||||||
if (!isset($perm['username'])) continue;
|
if (!isset($perm['username'])) continue;
|
||||||
$username = $perm['username'];
|
$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.
|
// Skip updating permissions for admin users.
|
||||||
if (strtolower($username) === "admin") continue;
|
if ($role === "1") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Update permissions: default any missing value to false.
|
// Update permissions: default any missing value to false.
|
||||||
$existingPermissions[$username] = [
|
$existingPermissions[strtolower($username)] = [
|
||||||
'folderOnly' => isset($perm['folderOnly']) ? (bool)$perm['folderOnly'] : false,
|
'folderOnly' => isset($perm['folderOnly']) ? (bool)$perm['folderOnly'] : false,
|
||||||
'readOnly' => isset($perm['readOnly']) ? (bool)$perm['readOnly'] : false,
|
'readOnly' => isset($perm['readOnly']) ? (bool)$perm['readOnly'] : false,
|
||||||
'disableUpload' => isset($perm['disableUpload']) ? (bool)$perm['disableUpload'] : false
|
'disableUpload' => isset($perm['disableUpload']) ? (bool)$perm['disableUpload'] : false
|
||||||
|
|||||||
17
upload.php
17
upload.php
@@ -65,14 +65,16 @@ if (isset($_POST['resumableChunkNumber'])) {
|
|||||||
$resumableFilename = $_POST['resumableFilename'];
|
$resumableFilename = $_POST['resumableFilename'];
|
||||||
|
|
||||||
|
|
||||||
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
|
// First, strip directory components.
|
||||||
http_response_code(400); // Set an error HTTP status code
|
$resumableFilename = urldecode(basename($_POST['resumableFilename']));
|
||||||
|
if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) {
|
||||||
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Invalid file name: " . $resumableFilename]);
|
echo json_encode(["error" => "Invalid file name: " . $resumableFilename]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
|
$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"]);
|
echo json_encode(["error" => "Invalid folder name"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -173,7 +175,7 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
|
|||||||
// ------------- Full Upload (Non-chunked) -------------
|
// ------------- Full Upload (Non-chunked) -------------
|
||||||
// Validate folder name input.
|
// Validate folder name input.
|
||||||
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
|
$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"]);
|
echo json_encode(["error" => "Invalid folder name"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -195,10 +197,12 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
|
|||||||
$metadataCollection = []; // key: folder path, value: metadata array
|
$metadataCollection = []; // key: folder path, value: metadata array
|
||||||
$metadataChanged = []; // key: folder path, value: boolean
|
$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) {
|
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)) {
|
if (!preg_match($safeFileNamePattern, $safeFileName)) {
|
||||||
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
||||||
exit;
|
exit;
|
||||||
@@ -224,6 +228,7 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
|
|||||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
|
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
|
||||||
. str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR;
|
. str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR;
|
||||||
}
|
}
|
||||||
|
// Reapply basename to the relativePath to get the final safe file name.
|
||||||
$safeFileName = basename($relativePath);
|
$safeFileName = basename($relativePath);
|
||||||
}
|
}
|
||||||
// --- End Minimal Folder/Subfolder Logic ---
|
// --- End Minimal Folder/Subfolder Logic ---
|
||||||
|
|||||||
@@ -109,8 +109,6 @@ if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Metadata Update for Shared Upload ---
|
// --- Metadata Update for Shared Upload ---
|
||||||
// We want to update metadata similarly to your normal upload.
|
|
||||||
// Determine a key for metadata storage for the folder.
|
|
||||||
$metadataKey = ($folder === '' || $folder === 'root') ? "root" : $folder;
|
$metadataKey = ($folder === '' || $folder === 'root') ? "root" : $folder;
|
||||||
// Sanitize the metadata file name.
|
// Sanitize the metadata file name.
|
||||||
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
|
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
|
||||||
|
|||||||
Reference in New Issue
Block a user