Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a70d8fc2c7 | ||
|
|
d9f69d7917 | ||
|
|
28ac23c2f6 | ||
|
|
b06c49f213 | ||
|
|
8553efabc1 | ||
|
|
81a08ffd5b | ||
|
|
296dae96a5 | ||
|
|
337f529afd | ||
|
|
4360f2830a | ||
|
|
894cc938a5 | ||
|
|
01801ba950 | ||
|
|
5b592575a4 | ||
|
|
7cce03d092 | ||
|
|
ff92a6d26c | ||
|
|
4fa5faa2bf | ||
|
|
98850a7c65 |
65
CHANGELOG.md
65
CHANGELOG.md
@@ -1,6 +1,69 @@
|
||||
# Changelog
|
||||
|
||||
## Folder Sharing Feature - Changelog 4/9/2025
|
||||
## Changes 4/12/2025
|
||||
|
||||
- **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:**
|
||||
- Modified the `toggleRowSelection` function so that when the Shift key is held down, all rows between the last clicked (anchor) row (stored as `window.lastSelectedFileRow`) and the currently clicked row are selected.
|
||||
- **Modifier Handling:**
|
||||
- Regular clicks (or Ctrl/Cmd clicks) simply toggle the clicked row without clearing other selections.
|
||||
- **Prevented Default Browser Behavior:**
|
||||
- Added `event.preventDefault()` in the Shift‑click branch to avoid unwanted text selection.
|
||||
- **Maintaining the Anchor:**
|
||||
- The last clicked row is stored for future range selections.
|
||||
|
||||
## Total Files and File Size Summary
|
||||
|
||||
- **Size Calculation:**
|
||||
- Created `parseSizeToBytes(sizeStr)` to convert file size strings (e.g. `"456.9KB"`, `"1.2 MB"`) into a numerical byte value.
|
||||
- Created `formatSize(totalBytes)` to format a byte value into a human‑readable string (choosing between Bytes, KB, MB, or GB).
|
||||
- Created `buildFolderSummary(filteredFiles)` to:
|
||||
- Sum the sizes of all files (using `parseSizeToBytes`).
|
||||
- Count the total number of files.
|
||||
- **Dynamic Display in `loadFileList`:**
|
||||
- Updated `loadFileList` to update a summary element (with `id="fileSummary"`) inside the `#fileListActions` container when files are present.
|
||||
- When no files are found, the summary element is hidden (setting its `display` to `"none"` or clearing the container).
|
||||
- **Responsive Styling:**
|
||||
- Added CSS media queries to the `#fileSummary` element so that on small screens it is centered and any extra side margins are removed. Dark and light mode supported.
|
||||
|
||||
- **Other changes**
|
||||
|
||||
- `shareFolder.php` updated to display format size.
|
||||
- Fix to prevent the filename text from overflowing its container in the gallery view.
|
||||
- Reduced header height.
|
||||
- Create Folder changed to Material Icon `create_new_folder`
|
||||
|
||||
---
|
||||
|
||||
## Folder Sharing Feature - Changelog 4/9/2025 v1.1.0
|
||||
|
||||
### New Endpoints
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Thank you for your interest in contributing to FileRise! We appreciate your help
|
||||
```
|
||||
|
||||
3. **Set Up a Local Environment**
|
||||
FileRise runs on a standard LAMP stack. Ensure you have PHP, Apache, and the necessary dependencies installed. For frontend development, Node.js may be required for build tasks if applicable.
|
||||
FileRise runs on a standard LAMP stack. Ensure you have PHP, Apache, and the necessary dependencies installed.
|
||||
|
||||
4. **Configuration**
|
||||
Copy any example configuration files (if provided) and adjust them as needed for your local setup.
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 SeNS
|
||||
Copyright (c) 2025 FileRise
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
24
README.md
24
README.md
@@ -12,7 +12,7 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
||||
|
||||
---
|
||||
|
||||
## Features at a Glance or [Full Feature Wiki](https://github.com/error311/FileRise/wiki/Features)
|
||||
## Features at a Glance or [Full Features Wiki](https://github.com/error311/FileRise/wiki/Features)
|
||||
|
||||
- 🚀 **Easy File Uploads:** Upload multiple files and folders via drag & drop or file picker. Supports large files with pause/resumable chunked uploads and shows real-time progress for each file. No more failed transfers – FileRise will pick up where it left off if your connection drops.
|
||||
|
||||
@@ -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.
|
||||
|
||||
- 🏷️ **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 quickly locate them using our advanced, indexed real-time search. The built-in search now leverages Fuse.js to provide fuzzy matching across file names, tags, and uploader fields—helping you find that “important” document even if you make a typo.
|
||||
|
||||
- 🔒 **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
|
||||
|
||||
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).
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) {
|
||||
if (!preg_match(REGEX_USER, $newUsername)) {
|
||||
echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
14
auth.php
14
auth.php
@@ -2,7 +2,9 @@
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once 'config.php';
|
||||
|
||||
// Only send the Content-Type header; CORS and related headers are handled via .htaccess.
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Global exception handler: logs errors and returns a generic error message.
|
||||
@@ -177,7 +179,7 @@ if (!$username || !$password) {
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
||||
if (!preg_match(REGEX_USER, $username)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||
exit();
|
||||
@@ -197,7 +199,13 @@ if ($user !== false) {
|
||||
]);
|
||||
exit();
|
||||
} else {
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||||
new GoogleChartsQrCodeProvider(), // QR code provider
|
||||
'FileRise', // issuer
|
||||
6, // number of digits
|
||||
30, // period in seconds
|
||||
Algorithm::Sha1 // Correct enum case name from your enum
|
||||
);
|
||||
$providedCode = trim($data['totp_code']);
|
||||
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
|
||||
echo json_encode(["error" => "Invalid TOTP code"]);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"require": {
|
||||
"jumbojett/openid-connect-php": "^1.0.0",
|
||||
"phpseclib/phpseclib": "~3.0.7",
|
||||
"robthree/twofactorauth": "^1.7",
|
||||
"endroid/qr-code": "^4.0"
|
||||
"robthree/twofactorauth": "^3.0",
|
||||
"endroid/qr-code": "^5.0"
|
||||
}
|
||||
}
|
||||
74
composer.lock
generated
74
composer.lock
generated
@@ -4,32 +4,32 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c9857f23364f2280ef4b71cdc72d3f78",
|
||||
"content-hash": "6b70aec0c1830ebb2b8f9bb625b04a22",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"version": "2.0.8",
|
||||
"version": "v3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Bacon/BaconQrCode.git",
|
||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
|
||||
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
|
||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f",
|
||||
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dasprid/enum": "^1.0.3",
|
||||
"ext-iconv": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phly/keep-a-changelog": "^2.1",
|
||||
"phpunit/phpunit": "^7 | ^8 | ^9",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2.9",
|
||||
"squizlabs/php_codesniffer": "^3.4"
|
||||
"phly/keep-a-changelog": "^2.12",
|
||||
"phpunit/phpunit": "^10.5.11 || 11.0.4",
|
||||
"spatie/phpunit-snapshot-assertions": "^5.1.5",
|
||||
"squizlabs/php_codesniffer": "^3.9"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "to generate QR code images"
|
||||
@@ -56,9 +56,9 @@
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"support": {
|
||||
"issues": "https://github.com/Bacon/BaconQrCode/issues",
|
||||
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
|
||||
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1"
|
||||
},
|
||||
"time": "2022-12-07T17:46:57+00:00"
|
||||
"time": "2024-10-01T13:55:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dasprid/enum",
|
||||
@@ -112,29 +112,26 @@
|
||||
},
|
||||
{
|
||||
"name": "endroid/qr-code",
|
||||
"version": "4.8.5",
|
||||
"version": "5.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/endroid/qr-code.git",
|
||||
"reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77"
|
||||
"reference": "393fec6c4cbdc1bd65570ac9d245704428010122"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/endroid/qr-code/zipball/0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
|
||||
"reference": "0db25b506a8411a5e1644ebaa67123a6eb7b6a77",
|
||||
"url": "https://api.github.com/repos/endroid/qr-code/zipball/393fec6c4cbdc1bd65570ac9d245704428010122",
|
||||
"reference": "393fec6c4cbdc1bd65570ac9d245704428010122",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"bacon/bacon-qr-code": "^2.0.5",
|
||||
"bacon/bacon-qr-code": "^3.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"conflict": {
|
||||
"khanamiryan/qrcode-detector-decoder": "^1.0.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"endroid/quality": "dev-master",
|
||||
"endroid/quality": "dev-main",
|
||||
"ext-gd": "*",
|
||||
"khanamiryan/qrcode-detector-decoder": "^1.0.4||^2.0.2",
|
||||
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
|
||||
"setasign/fpdf": "^1.8.2"
|
||||
},
|
||||
"suggest": {
|
||||
@@ -146,7 +143,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.x-dev"
|
||||
"dev-main": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -175,7 +172,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/endroid/qr-code/issues",
|
||||
"source": "https://github.com/endroid/qr-code/tree/4.8.5"
|
||||
"source": "https://github.com/endroid/qr-code/tree/5.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -183,7 +180,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-09-29T14:03:20+00:00"
|
||||
"time": "2024-09-08T08:52:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jumbojett/openid-connect-php",
|
||||
@@ -456,24 +453,25 @@
|
||||
},
|
||||
{
|
||||
"name": "robthree/twofactorauth",
|
||||
"version": "1.8.2",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/RobThree/TwoFactorAuth.git",
|
||||
"reference": "65681de5a324eae05140ac58b08648a60212afc0"
|
||||
"reference": "6d70f9ca8e25568f163a7b3b3ff77bd8ea743978"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/65681de5a324eae05140ac58b08648a60212afc0",
|
||||
"reference": "65681de5a324eae05140ac58b08648a60212afc0",
|
||||
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/6d70f9ca8e25568f163a7b3b3ff77bd8ea743978",
|
||||
"reference": "6d70f9ca8e25568f163a7b3b3ff77bd8ea743978",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.6.0"
|
||||
"php": ">=8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpunit/phpunit": "@stable"
|
||||
"friendsofphp/php-cs-fixer": "^3.13",
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpunit/phpunit": "^9"
|
||||
},
|
||||
"suggest": {
|
||||
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
|
||||
@@ -494,6 +492,16 @@
|
||||
"name": "Rob Janssen",
|
||||
"homepage": "http://robiii.me",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas CARPi",
|
||||
"homepage": "https://github.com/NicolasCARPi",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Will Power",
|
||||
"homepage": "https://github.com/willpower232",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Two Factor Authentication",
|
||||
@@ -522,7 +530,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2022-03-22T16:11:07+00:00"
|
||||
"time": "2024-10-24T15:14:25+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
||||
28
config.php
28
config.php
@@ -1,5 +1,20 @@
|
||||
<?php
|
||||
// config.php
|
||||
header("Cache-Control: no-cache, must-revalidate");
|
||||
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
// Security headers
|
||||
header("X-Content-Type-Options: nosniff");
|
||||
header("X-Frame-Options: SAMEORIGIN");
|
||||
header("Referrer-Policy: no-referrer-when-downgrade");
|
||||
// Only include Strict-Transport-Security if you are using HTTPS
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
header("Strict-Transport-Security: max-age=31536000; includeSubDomains; preload");
|
||||
}
|
||||
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
|
||||
header("X-XSS-Protection: 1; mode=block");
|
||||
|
||||
// Define constants.
|
||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||
@@ -11,6 +26,10 @@ define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
||||
define('TIMEZONE', 'America/New_York');
|
||||
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
||||
define('TOTAL_UPLOAD_SIZE', '5G');
|
||||
define('REGEX_FOLDER_NAME', '/^[\p{L}\p{N}_\-\s\/\\\\]+$/u');
|
||||
define('PATTERN_FOLDER_NAME', '[\p{L}\p{N}_\-\s\/\\\\]+');
|
||||
define('REGEX_FILE_NAME', '/^[\p{L}\p{N}\p{M}%\-\.\(\) _]+$/u');
|
||||
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
||||
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
|
||||
@@ -48,9 +67,12 @@ function decryptData($encryptedData, $encryptionKey)
|
||||
}
|
||||
|
||||
// Load encryption key from environment (override in production).
|
||||
$encryptionKey = getenv('PERSISTENT_TOKENS_KEY') ?: 'default_please_change_this_key';
|
||||
if (!$encryptionKey) {
|
||||
die('Encryption key for persistent tokens is not set.');
|
||||
$envKey = getenv('PERSISTENT_TOKENS_KEY');
|
||||
if ($envKey === false || $envKey === '') {
|
||||
$encryptionKey = 'default_please_change_this_key';
|
||||
error_log('WARNING: Using default encryption key. Please set PERSISTENT_TOKENS_KEY in your environment.');
|
||||
} else {
|
||||
$encryptionKey = $envKey;
|
||||
}
|
||||
|
||||
function loadUserPermissions($username)
|
||||
|
||||
@@ -44,7 +44,7 @@ $destinationFolder = trim($data['destination']);
|
||||
$files = $data['files'];
|
||||
|
||||
// Validate folder names: allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
||||
$folderPattern = '/^[A-Za-z0-9_\- \/]+$/';
|
||||
$folderPattern = REGEX_FOLDER_NAME;
|
||||
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
|
||||
echo json_encode(["error" => "Invalid source folder name."]);
|
||||
exit;
|
||||
@@ -104,7 +104,7 @@ $destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($dest
|
||||
$errors = [];
|
||||
|
||||
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||
|
||||
foreach ($files as $fileName) {
|
||||
// Save the original name for metadata lookup.
|
||||
|
||||
@@ -45,13 +45,13 @@ $folderName = trim($input['folderName']);
|
||||
$parent = isset($input['parent']) ? trim($input['parent']) : "";
|
||||
|
||||
// Basic sanitation: allow only letters, numbers, underscores, dashes, and spaces in folderName
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $folderName)) {
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Optionally, sanitize the parent folder if needed.
|
||||
if ($parent && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $parent)) {
|
||||
if ($parent && !preg_match(REGEX_FOLDER_NAME, $parent)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid parent folder name.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,16 @@ if (!$input) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create shared folders."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$folder = isset($input['folder']) ? trim($input['folder']) : "";
|
||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||
$password = isset($input['password']) ? $input['password'] : "";
|
||||
@@ -17,7 +27,7 @@ $allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0;
|
||||
|
||||
// Validate folder name using regex.
|
||||
// Allow letters, numbers, underscores, hyphens, spaces and slashes.
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -9,13 +9,23 @@ if (!$input) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to create share files."]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$folder = isset($input['folder']) ? trim($input['folder']) : "";
|
||||
$file = isset($input['file']) ? basename($input['file']) : "";
|
||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
||||
$password = isset($input['password']) ? $input['password'] : "";
|
||||
|
||||
// Validate folder using regex.
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ body {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
height: 65px;
|
||||
padding: 10px 20px;
|
||||
background-color: #2196F3;
|
||||
transition: background-color 0.3s ease;
|
||||
@@ -82,13 +82,13 @@ body.dark-mode .header-container {
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
max-height: 70px;
|
||||
max-height: 60px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-logo svg {
|
||||
height: 70px;
|
||||
height: 60px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@@ -650,12 +650,15 @@ body.dark-mode .editor-header {
|
||||
}
|
||||
|
||||
#uploadBtn {
|
||||
margin-top: 20px;
|
||||
font-size: 20px;
|
||||
padding: 10px 22px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-body.d-flex.flex-column {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
#customChooseBtn {
|
||||
background-color: #9E9E9E;
|
||||
color: #fff;
|
||||
@@ -1945,12 +1948,12 @@ body.dark-mode #folderContextMenu {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
min-height: 353px;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
|
||||
#uploadFolderRow.highlight {
|
||||
min-height: 353px;
|
||||
min-height: 320px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@@ -2134,4 +2137,27 @@ body.dark-mode .header-drop-zone.drag-active {
|
||||
content: "Drop";
|
||||
font-size: 10px;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
/* Disable text selection on rows to prevent accidental copying when shift-clicking */
|
||||
#fileList tbody tr.clickable-row {
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE10+/Edge */
|
||||
user-select: none; /* Standard */
|
||||
}
|
||||
|
||||
#fileSummary {
|
||||
color: black;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
#fileSummary {
|
||||
float: none !important;
|
||||
margin: 0 auto !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
body.dark-mode #fileSummary {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ if (!isset($data['files']) || !is_array($data['files'])) {
|
||||
$folder = isset($data['folder']) ? trim($data['folder']) : 'root';
|
||||
|
||||
// Validate folder: allow letters, numbers, underscores, dashes, spaces, and forward slashes
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
@@ -96,7 +96,7 @@ $movedFiles = [];
|
||||
$errors = [];
|
||||
|
||||
// Define a safe file name pattern: allow letters, numbers, underscores, dashes, dots, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||
|
||||
foreach ($data['files'] as $fileName) {
|
||||
$basename = basename(trim($fileName));
|
||||
|
||||
@@ -50,7 +50,7 @@ if ($folderName === 'root') {
|
||||
}
|
||||
|
||||
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
||||
if (!preg_match('/^[A-Za-z0-9_\- \/]+$/', $folderName)) {
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid folder name.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ $deletedFiles = [];
|
||||
$errors = [];
|
||||
|
||||
// Define a safe file name pattern.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||
|
||||
foreach ($filesToDelete as $trashName) {
|
||||
$trashName = trim($trashName);
|
||||
|
||||
@@ -14,7 +14,7 @@ $file = isset($_GET['file']) ? basename($_GET['file']) : '';
|
||||
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
||||
|
||||
// Validate file name (allowing letters, numbers, underscores, dashes, dots, and parentheses)
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $file)) {
|
||||
if (!preg_match(REGEX_FILE_NAME, $file)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid file name."]);
|
||||
exit;
|
||||
@@ -80,10 +80,6 @@ if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
||||
}
|
||||
header('Content-Length: ' . filesize($realFilePath));
|
||||
|
||||
// Disable caching.
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
readfile($realFilePath);
|
||||
exit;
|
||||
?>
|
||||
@@ -79,10 +79,6 @@ if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
||||
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.
|
||||
readfile($realFilePath);
|
||||
exit;
|
||||
|
||||
@@ -38,7 +38,7 @@ $files = $data['files'];
|
||||
if ($folder !== "root") {
|
||||
$parts = explode('/', $folder);
|
||||
foreach ($parts as $part) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
@@ -76,7 +76,7 @@ if (empty($files)) {
|
||||
}
|
||||
|
||||
foreach ($files as $fileName) {
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $fileName)) {
|
||||
if (!preg_match(REGEX_FILE_NAME, $fileName)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
||||
|
||||
@@ -50,7 +50,7 @@ if (empty($files)) {
|
||||
if ($folder !== "root") {
|
||||
$parts = explode('/', $folder);
|
||||
foreach ($parts as $part) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
@@ -92,7 +92,7 @@ $destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($dest
|
||||
$errors = [];
|
||||
$allSuccess = true;
|
||||
$extractedFiles = array(); // Array to collect names of extracted files
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||
|
||||
// ---------- Process Each File ----------
|
||||
foreach ($files as $zipFileName) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Ensure user is authenticated
|
||||
@@ -14,7 +11,7 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
|
||||
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
|
||||
// Allow only safe characters in the folder parameter (letters, numbers, underscores, dashes, spaces, and forward slashes).
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
@@ -53,7 +50,7 @@ $files = array_values(array_diff(scandir($directory), array('.', '..')));
|
||||
$fileList = [];
|
||||
|
||||
// Define a safe file name pattern: letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||
|
||||
foreach ($files as $file) {
|
||||
// Skip hidden files (those that begin with a dot)
|
||||
|
||||
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 = [];
|
||||
$items = scandir($dir);
|
||||
// Allow letters, numbers, underscores, dashes, and spaces in folder names.
|
||||
$safeFolderNamePattern = '/^[A-Za-z0-9_\- ]+$/';
|
||||
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
if (!preg_match($safeFolderNamePattern, $item)) {
|
||||
|
||||
@@ -17,7 +17,7 @@ if (file_exists($usersFile)) {
|
||||
$parts = explode(':', trim($line));
|
||||
if (count($parts) >= 3) {
|
||||
// Validate username format:
|
||||
if (preg_match('/^[A-Za-z0-9_\- ]+$/', $parts[0])) {
|
||||
if (preg_match(REGEX_USER, $parts[0])) {
|
||||
$users[] = [
|
||||
"username" => $parts[0],
|
||||
"role" => trim($parts[2])
|
||||
|
||||
10
index.html
10
index.html
@@ -41,6 +41,9 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
||||
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"
|
||||
integrity="sha384-zPE55eyESN+FxCWGEnlNxGyAPJud6IZ6TtJmXb56OFRGhxZPN4akj9rjA3gw5Qqa"
|
||||
crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="css/styles.css" />
|
||||
</head>
|
||||
|
||||
@@ -245,8 +248,9 @@
|
||||
<div id="folderTreeContainer"></div>
|
||||
</div>
|
||||
<div class="folder-actions mt-3">
|
||||
<button id="createFolderBtn" class="btn btn-primary" data-i18n-key="create_folder">Create
|
||||
Folder</button>
|
||||
<button id="createFolderBtn" class="btn btn-primary" data-i18n-title="create_folder">
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
</button>
|
||||
<div id="createFolderModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4 data-i18n-key="create_folder_title">Create Folder</h4>
|
||||
@@ -412,7 +416,7 @@
|
||||
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
||||
<div id="changePasswordModal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="max-width:400px; margin:auto;">
|
||||
<span id="closeChangePasswordModal" style="cursor:pointer;">×</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>
|
||||
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
||||
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
||||
|
||||
41
js/auth.js
41
js/auth.js
@@ -132,10 +132,11 @@ function updateAuthenticatedUI(data) {
|
||||
if (data.username) {
|
||||
localStorage.setItem("username", data.username);
|
||||
}
|
||||
/*
|
||||
if (typeof data.folderOnly !== "undefined") {
|
||||
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
||||
}
|
||||
|
||||
*/
|
||||
const headerButtons = document.querySelector(".header-buttons");
|
||||
const firstButton = headerButtons.firstElementChild;
|
||||
|
||||
@@ -227,15 +228,29 @@ function checkAuthentication(showLoginToast = true) {
|
||||
function submitLogin(data) {
|
||||
setLastLoginData(data);
|
||||
window.__lastLoginData = data;
|
||||
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success || response.status === "ok") {
|
||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
|
||||
window.location.reload();
|
||||
} else if (response.totp_required) {
|
||||
openTOTPLoginModal();
|
||||
} else if (response.error && response.error.includes("Too many failed login attempts")) {
|
||||
showToast(response.error);
|
||||
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
if (response.success || response.status === "ok") {
|
||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
|
||||
// Fetch and update permissions, then reload.
|
||||
sendRequest("getUserPermissions.php", "GET")
|
||||
.then(permissionData => {
|
||||
if (permissionData && typeof permissionData === "object") {
|
||||
localStorage.setItem("folderOnly", permissionData.folderOnly ? "true" : "false");
|
||||
localStorage.setItem("readOnly", permissionData.readOnly ? "true" : "false");
|
||||
localStorage.setItem("disableUpload", permissionData.disableUpload ? "true" : "false");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// if fetching permissions fails.
|
||||
})
|
||||
.finally(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
} else if (response.totp_required) {
|
||||
openTOTPLoginModal();
|
||||
} else if (response.error && response.error.includes("Too many failed login attempts")) {
|
||||
showToast(response.error);
|
||||
const loginButton = document.getElementById("authForm").querySelector("button[type='submit']");
|
||||
if (loginButton) {
|
||||
loginButton.disabled = true;
|
||||
@@ -293,7 +308,7 @@ function loadUserList() {
|
||||
closeRemoveUserModal();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
}
|
||||
window.loadUserList = loadUserList;
|
||||
|
||||
@@ -320,7 +335,7 @@ function initAuth() {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
}).then(() => window.location.reload(true)).catch(() => {});
|
||||
}).then(() => window.location.reload(true)).catch(() => { });
|
||||
});
|
||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||
resetUserForm();
|
||||
@@ -386,7 +401,7 @@ function initAuth() {
|
||||
showToast("Error: " + (data.error || "Could not remove user"));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
});
|
||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
|
||||
const version = "v1.1.0";
|
||||
const version = "v1.1.2";
|
||||
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
|
||||
let lastLoginData = null;
|
||||
@@ -162,9 +162,9 @@ export function openUserPanel() {
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
position: fixed;
|
||||
overflow-y: auto;
|
||||
max-height: 90vh;
|
||||
max-height: 350px !important;
|
||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||
transform: none;
|
||||
transition: none;
|
||||
@@ -187,7 +187,7 @@ export function openUserPanel() {
|
||||
z-index: 3000;
|
||||
`;
|
||||
userPanelModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<div class="modal-content user-panel-content" style="${modalContentStyles}">
|
||||
<span id="closeUserPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>User Panel (${username})</h3>
|
||||
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">Change Password</button>
|
||||
@@ -325,19 +325,21 @@ export function openTOTPModal() {
|
||||
z-index: 3100;
|
||||
`;
|
||||
totpModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>TOTP Setup</h3>
|
||||
<p>Scan this QR code with your authenticator app:</p>
|
||||
<img src="totp_setup.php?csrf=${encodeURIComponent(window.csrfToken)}" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
<br/>
|
||||
<p>Enter the 6-digit code from your app to confirm setup:</p>
|
||||
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
|
||||
<br/><br/>
|
||||
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button>
|
||||
</div>
|
||||
`;
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||
<h3>TOTP Setup</h3>
|
||||
<p>Scan this QR code with your authenticator app:</p>
|
||||
<!-- Create an image placeholder without the CSRF token in the src -->
|
||||
<img id="totpQRCodeImage" src="" alt="TOTP QR Code" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
|
||||
<br/>
|
||||
<p>Enter the 6-digit code from your app to confirm setup:</p>
|
||||
<input type="text" id="totpConfirmInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
|
||||
<br/><br/>
|
||||
<button type="button" id="confirmTOTPBtn" class="btn btn-primary">Confirm</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(totpModal);
|
||||
loadTOTPQRCode();
|
||||
|
||||
document.getElementById("closeTOTPModal").addEventListener("click", () => {
|
||||
closeTOTPModal(true);
|
||||
@@ -406,6 +408,13 @@ export function openTOTPModal() {
|
||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||
|
||||
// Clear any previous QR code src if needed and then load it:
|
||||
const qrImg = document.getElementById("totpQRCodeImage");
|
||||
if (qrImg) {
|
||||
qrImg.src = "";
|
||||
}
|
||||
loadTOTPQRCode();
|
||||
|
||||
// Focus the input and attach enter key listener
|
||||
const totpConfirmInput = document.getElementById("totpConfirmInput");
|
||||
if (totpConfirmInput) {
|
||||
@@ -419,6 +428,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("Error loading QR code.");
|
||||
});
|
||||
}
|
||||
|
||||
// Updated closeTOTPModal function with a disable parameter
|
||||
export function closeTOTPModal(disable = true) {
|
||||
const totpModal = document.getElementById("totpModal");
|
||||
@@ -801,11 +837,17 @@ function loadUserPermissionsList() {
|
||||
|
||||
// Use stored permissions if available; otherwise fall back to localStorage defaults.
|
||||
const defaultPerm = {
|
||||
folderOnly: localStorage.getItem("folderOnly") === "true",
|
||||
readOnly: localStorage.getItem("readOnly") === "true",
|
||||
disableUpload: localStorage.getItem("disableUpload") === "true"
|
||||
folderOnly: false,
|
||||
readOnly: false,
|
||||
disableUpload: false,
|
||||
};
|
||||
const userPerm = (permissionsData && typeof permissionsData === "object" && permissionsData[user.username]) || defaultPerm;
|
||||
|
||||
// Normalize the username key to match server storage (e.g., lowercase)
|
||||
const usernameKey = user.username.toLowerCase();
|
||||
|
||||
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
|
||||
? permissionsData[usernameKey]
|
||||
: defaultPerm;
|
||||
|
||||
// Create a row for the user.
|
||||
const row = document.createElement("div");
|
||||
|
||||
@@ -223,15 +223,63 @@ export function updateRowHighlight(checkbox) {
|
||||
}
|
||||
|
||||
export function toggleRowSelection(event, fileName) {
|
||||
// Prevent default text selection when shift is held.
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Ignore clicks on interactive elements.
|
||||
const targetTag = event.target.tagName.toLowerCase();
|
||||
if (targetTag === 'a' || targetTag === 'button' || targetTag === 'input') {
|
||||
if (["a", "button", "input"].includes(targetTag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the clicked row and its checkbox.
|
||||
const row = event.currentTarget;
|
||||
const checkbox = row.querySelector('.file-checkbox');
|
||||
const checkbox = row.querySelector(".file-checkbox");
|
||||
if (!checkbox) return;
|
||||
checkbox.checked = !checkbox.checked;
|
||||
updateRowHighlight(checkbox);
|
||||
|
||||
// Get all rows in the current file list view.
|
||||
const allRows = Array.from(document.querySelectorAll("#fileList tbody tr"));
|
||||
|
||||
// Helper: clear all selections (not used in this updated version).
|
||||
const clearAllSelections = () => {
|
||||
allRows.forEach(r => {
|
||||
const cb = r.querySelector(".file-checkbox");
|
||||
if (cb) {
|
||||
cb.checked = false;
|
||||
updateRowHighlight(cb);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// If the user is holding the Shift key, perform range selection.
|
||||
if (event.shiftKey) {
|
||||
// Use the last clicked row as the anchor.
|
||||
const lastRow = window.lastSelectedFileRow || row;
|
||||
const currentIndex = allRows.indexOf(row);
|
||||
const lastIndex = allRows.indexOf(lastRow);
|
||||
const start = Math.min(currentIndex, lastIndex);
|
||||
const end = Math.max(currentIndex, lastIndex);
|
||||
|
||||
// If neither CTRL nor Meta is pressed, you might choose
|
||||
// to clear existing selections. For this example we leave existing selections intact.
|
||||
for (let i = start; i <= end; i++) {
|
||||
const cb = allRows[i].querySelector(".file-checkbox");
|
||||
if (cb) {
|
||||
cb.checked = true;
|
||||
updateRowHighlight(cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise, for all non-shift clicks simply toggle the selected state.
|
||||
else {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
updateRowHighlight(checkbox);
|
||||
}
|
||||
|
||||
// Update the anchor row to the row that was clicked.
|
||||
window.lastSelectedFileRow = row;
|
||||
updateFileActionButtons();
|
||||
}
|
||||
|
||||
@@ -241,7 +289,7 @@ export function attachEnterKeyListener(modalId, buttonId) {
|
||||
// Make the modal focusable
|
||||
modal.setAttribute("tabindex", "-1");
|
||||
modal.focus();
|
||||
modal.addEventListener("keydown", function(e) {
|
||||
modal.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById(buttonId);
|
||||
|
||||
@@ -299,7 +299,7 @@ export function loadSidebarOrder() {
|
||||
modal = document.createElement('div');
|
||||
modal.className = 'header-card-modal';
|
||||
modal.style.position = 'fixed';
|
||||
modal.style.top = '80px';
|
||||
modal.style.top = '55px';
|
||||
modal.style.right = '80px';
|
||||
modal.style.zIndex = '11000';
|
||||
// Render the modal but initially keep it hidden.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// dragDrop.js
|
||||
// fileDragDrop.js
|
||||
import { showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// editor.js
|
||||
// fileEditor.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { t } from './i18n.js';
|
||||
import { bindFileListContextMenu } from './fileMenu.js';
|
||||
import { openDownloadModal } from './fileActions.js';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||
|
||||
export let fileData = [];
|
||||
export let sortOrder = { column: "uploaded", ascending: true };
|
||||
@@ -23,9 +24,75 @@ window.itemsPerPage = window.itemsPerPage || 10;
|
||||
window.currentPage = window.currentPage || 1;
|
||||
window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery"
|
||||
|
||||
// -----------------------------
|
||||
// VIEW MODE TOGGLE BUTTON & Helpers
|
||||
// -----------------------------
|
||||
/**
|
||||
* --- Helper Functions ---
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a file size string (e.g. "456.9KB", "1.2 MB", "1024") into bytes.
|
||||
*/
|
||||
function parseSizeToBytes(sizeStr) {
|
||||
if (!sizeStr) return 0;
|
||||
let s = sizeStr.trim();
|
||||
let value = parseFloat(s);
|
||||
let upper = s.toUpperCase();
|
||||
if (upper.includes("KB")) {
|
||||
value *= 1024;
|
||||
} else if (upper.includes("MB")) {
|
||||
value *= 1024 * 1024;
|
||||
} else if (upper.includes("GB")) {
|
||||
value *= 1024 * 1024 * 1024;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the total bytes as a human-readable string.
|
||||
*/
|
||||
function formatSize(totalBytes) {
|
||||
if (totalBytes < 1024) {
|
||||
return totalBytes + " Bytes";
|
||||
} else if (totalBytes < 1024 * 1024) {
|
||||
return (totalBytes / 1024).toFixed(2) + " KB";
|
||||
} else if (totalBytes < 1024 * 1024 * 1024) {
|
||||
return (totalBytes / (1024 * 1024)).toFixed(2) + " MB";
|
||||
} else {
|
||||
return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the folder summary HTML using the filtered file list.
|
||||
*/
|
||||
function buildFolderSummary(filteredFiles) {
|
||||
const totalFiles = filteredFiles.length;
|
||||
const totalBytes = filteredFiles.reduce((sum, file) => {
|
||||
return sum + parseSizeToBytes(file.size);
|
||||
}, 0);
|
||||
const sizeStr = formatSize(totalBytes);
|
||||
return `<strong>Total Files:</strong> ${totalFiles} | <strong>Total Size:</strong> ${sizeStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* --- Fuse.js Search Helper ---
|
||||
* Uses Fuse.js to perform a fuzzy search on fileData.
|
||||
* Searches over file name, uploader, and tag names.
|
||||
*/
|
||||
function searchFiles(searchTerm) {
|
||||
if (!searchTerm) return fileData;
|
||||
// Define search options – adjust threshold as needed.
|
||||
const options = {
|
||||
keys: ['name', 'uploader', 'tags.name'],
|
||||
threshold: 0.3
|
||||
};
|
||||
const fuse = new Fuse(fileData, options);
|
||||
// Fuse returns an array of results where each result has an "item" property.
|
||||
return fuse.search(searchTerm).map(result => result.item);
|
||||
}
|
||||
|
||||
/**
|
||||
* --- VIEW MODE TOGGLE BUTTON & Helpers ---
|
||||
*/
|
||||
export function createViewToggleButton() {
|
||||
let toggleBtn = document.getElementById("toggleViewBtn");
|
||||
if (!toggleBtn) {
|
||||
@@ -58,11 +125,9 @@ export function formatFolderName(folder) {
|
||||
window.toggleRowSelection = toggleRowSelection;
|
||||
window.updateRowHighlight = updateRowHighlight;
|
||||
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||
|
||||
// -----------------------------
|
||||
// FILE LIST & VIEW RENDERING
|
||||
// -----------------------------
|
||||
/**
|
||||
* --- FILE LIST & VIEW RENDERING ---
|
||||
*/
|
||||
export function loadFileList(folderParam) {
|
||||
const folder = folderParam || "root";
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
@@ -80,8 +145,16 @@ export function loadFileList(folderParam) {
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
fileListContainer.innerHTML = "";
|
||||
if (data.files && data.files.length > 0) {
|
||||
fileListContainer.innerHTML = ""; // Clear loading message.
|
||||
if (data.files && Object.keys(data.files).length > 0) {
|
||||
// In case the returned "files" is an object instead of an array, transform it.
|
||||
if (!Array.isArray(data.files)) {
|
||||
data.files = Object.entries(data.files).map(([name, meta]) => {
|
||||
meta.name = name;
|
||||
return meta;
|
||||
});
|
||||
}
|
||||
// Process each file – add computed properties.
|
||||
data.files = data.files.map(file => {
|
||||
file.fullName = (file.path || file.name).trim().toLowerCase();
|
||||
file.editable = canEditFile(file.name);
|
||||
@@ -92,6 +165,26 @@ export function loadFileList(folderParam) {
|
||||
return file;
|
||||
});
|
||||
fileData = data.files;
|
||||
|
||||
// Update file summary.
|
||||
const actionsContainer = document.getElementById("fileListActions");
|
||||
if (actionsContainer) {
|
||||
let summaryElem = document.getElementById("fileSummary");
|
||||
if (!summaryElem) {
|
||||
summaryElem = document.createElement("div");
|
||||
summaryElem.id = "fileSummary";
|
||||
summaryElem.style.float = "right";
|
||||
summaryElem.style.marginLeft = "auto";
|
||||
summaryElem.style.marginRight = "60px";
|
||||
summaryElem.style.fontSize = "0.9em";
|
||||
actionsContainer.appendChild(summaryElem);
|
||||
} else {
|
||||
summaryElem.style.display = "block";
|
||||
}
|
||||
summaryElem.innerHTML = buildFolderSummary(fileData);
|
||||
}
|
||||
|
||||
// Render view based on the view mode.
|
||||
if (window.viewMode === "gallery") {
|
||||
renderGalleryView(folder);
|
||||
} else {
|
||||
@@ -99,6 +192,10 @@ export function loadFileList(folderParam) {
|
||||
}
|
||||
} else {
|
||||
fileListContainer.textContent = t("no_files_found");
|
||||
const summaryElem = document.getElementById("fileSummary");
|
||||
if (summaryElem) {
|
||||
summaryElem.style.display = "none";
|
||||
}
|
||||
updateFileActionButtons();
|
||||
}
|
||||
return data.files || [];
|
||||
@@ -115,25 +212,24 @@ export function loadFileList(folderParam) {
|
||||
});
|
||||
}
|
||||
|
||||
export function renderFileTable(folder) {
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
/**
|
||||
* Update renderFileTable so it writes its content into the provided container.
|
||||
*/
|
||||
export function renderFileTable(folder, container) {
|
||||
const fileListContent = container || document.getElementById("fileList");
|
||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
||||
let currentPage = window.currentPage || 1;
|
||||
|
||||
const filteredFiles = fileData.filter(file => {
|
||||
const nameMatch = file.name.toLowerCase().includes(searchTerm);
|
||||
const tagMatch = file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm));
|
||||
return nameMatch || tagMatch;
|
||||
});
|
||||
|
||||
// Use Fuse.js search via our helper function.
|
||||
const filteredFiles = searchFiles(searchTerm);
|
||||
|
||||
const totalFiles = filteredFiles.length;
|
||||
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
|
||||
if (currentPage > totalPages) {
|
||||
currentPage = totalPages > 0 ? totalPages : 1;
|
||||
window.currentPage = currentPage;
|
||||
}
|
||||
|
||||
const folderPath = folder === "root"
|
||||
? "uploads/"
|
||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||
@@ -147,7 +243,6 @@ export function renderFileTable(folder) {
|
||||
const startIndex = (currentPage - 1) * itemsPerPageSetting;
|
||||
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
|
||||
let rowsHTML = "<tbody>";
|
||||
|
||||
if (totalFiles > 0) {
|
||||
filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => {
|
||||
let rowHTML = buildFileTableRow(file, folderPath);
|
||||
@@ -161,15 +256,12 @@ export function renderFileTable(folder) {
|
||||
});
|
||||
tagBadgesHTML += "</div>";
|
||||
}
|
||||
|
||||
rowHTML = rowHTML.replace(/(<td class="file-name-cell">)(.*?)(<\/td>)/, (match, p1, p2, p3) => {
|
||||
return p1 + p2 + tagBadgesHTML + p3;
|
||||
});
|
||||
|
||||
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="Share">
|
||||
<i class="material-icons">share</i>
|
||||
</button>$1`);
|
||||
|
||||
<i class="material-icons">share</i>
|
||||
</button>$1`);
|
||||
rowsHTML += rowHTML;
|
||||
});
|
||||
} else {
|
||||
@@ -177,16 +269,18 @@ export function renderFileTable(folder) {
|
||||
}
|
||||
rowsHTML += "</tbody></table>";
|
||||
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
|
||||
fileListContainer.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||
|
||||
fileListContent.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||
|
||||
createViewToggleButton();
|
||||
|
||||
// Setup event listeners.
|
||||
const newSearchInput = document.getElementById("searchInput");
|
||||
if (newSearchInput) {
|
||||
newSearchInput.addEventListener("input", debounce(function () {
|
||||
window.currentSearchTerm = newSearchInput.value;
|
||||
window.currentPage = 1;
|
||||
renderFileTable(folder);
|
||||
renderFileTable(folder, container);
|
||||
setTimeout(() => {
|
||||
const freshInput = document.getElementById("searchInput");
|
||||
if (freshInput) {
|
||||
@@ -197,21 +291,18 @@ export function renderFileTable(folder) {
|
||||
}, 0);
|
||||
}, 300));
|
||||
}
|
||||
|
||||
document.querySelectorAll("table.table thead th[data-column]").forEach(cell => {
|
||||
cell.addEventListener("click", function () {
|
||||
const column = this.getAttribute("data-column");
|
||||
sortFiles(column, folder);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => {
|
||||
checkbox.addEventListener("change", function (e) {
|
||||
updateRowHighlight(e.target);
|
||||
updateFileActionButtons();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".share-btn").forEach(btn => {
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -224,40 +315,32 @@ export function renderFileTable(folder) {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
updateFileActionButtons();
|
||||
|
||||
// Add drag-and-drop support for each table row.
|
||||
document.querySelectorAll("#fileList tbody tr").forEach(row => {
|
||||
row.setAttribute("draggable", "true");
|
||||
import('./fileDragDrop.js').then(module => {
|
||||
row.addEventListener("dragstart", module.fileDragStartHandler);
|
||||
});
|
||||
});
|
||||
|
||||
// Prevent clicks on these buttons from selecting the row
|
||||
document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => e.stopPropagation());
|
||||
});
|
||||
|
||||
// re‑bind context menu
|
||||
bindFileListContextMenu();
|
||||
}
|
||||
|
||||
export function renderGalleryView(folder) {
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
/**
|
||||
* Similarly, update renderGalleryView to accept an optional container.
|
||||
*/
|
||||
export function renderGalleryView(folder, container) {
|
||||
const fileListContent = container || document.getElementById("fileList");
|
||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||
const filteredFiles = fileData.filter(file => {
|
||||
return file.name.toLowerCase().includes(searchTerm) ||
|
||||
(file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm)));
|
||||
});
|
||||
|
||||
// Use Fuse.js search for gallery view as well.
|
||||
const filteredFiles = searchFiles(searchTerm);
|
||||
const folderPath = folder === "root"
|
||||
? "uploads/"
|
||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||
const gridStyle = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; padding: 10px;";
|
||||
let galleryHTML = `<div class="gallery-container" style="${gridStyle}">`;
|
||||
|
||||
filteredFiles.forEach((file) => {
|
||||
let thumbnail;
|
||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||
@@ -267,7 +350,6 @@ export function renderGalleryView(folder) {
|
||||
} else {
|
||||
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
|
||||
}
|
||||
|
||||
let tagBadgesHTML = "";
|
||||
if (file.tags && file.tags.length > 0) {
|
||||
tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`;
|
||||
@@ -276,13 +358,12 @@ export function renderGalleryView(folder) {
|
||||
});
|
||||
tagBadgesHTML += `</div>`;
|
||||
}
|
||||
|
||||
galleryHTML += `<div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;">
|
||||
<div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')">
|
||||
${thumbnail}
|
||||
</div>
|
||||
<div class="gallery-info" style="margin-top: 5px;">
|
||||
<span class="gallery-file-name" style="display: block;">${escapeHTML(file.name)}</span>
|
||||
<span class="gallery-file-name" style="display: block; white-space: normal; overflow-wrap: break-word; word-wrap: break-word;">${escapeHTML(file.name)}</span>
|
||||
${tagBadgesHTML}
|
||||
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
|
||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||
@@ -305,15 +386,10 @@ export function renderGalleryView(folder) {
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
galleryHTML += "</div>";
|
||||
|
||||
fileListContainer.innerHTML = galleryHTML;
|
||||
|
||||
fileListContent.innerHTML = galleryHTML;
|
||||
createViewToggleButton();
|
||||
updateFileActionButtons();
|
||||
|
||||
// Bind share button clicks
|
||||
document.querySelectorAll(".share-btn").forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
@@ -413,7 +489,6 @@ window.changeItemsPerPage = function (newCount) {
|
||||
};
|
||||
|
||||
// fileListView.js (bottom)
|
||||
|
||||
window.loadFileList = loadFileList;
|
||||
window.renderFileTable = renderFileTable;
|
||||
window.renderGalleryView = renderGalleryView;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// contextMenu.js
|
||||
// fileMenu.js
|
||||
import { updateRowHighlight, showToast } from './domUtils.js';
|
||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile } from './fileActions.js';
|
||||
import { previewFile } from './filePreview.js';
|
||||
|
||||
@@ -305,7 +305,7 @@ if (localStorage.getItem('globalTags')) {
|
||||
|
||||
// New function to load global tags from the server's persistent JSON.
|
||||
export function loadGlobalTags() {
|
||||
fetch("metadata/createdTags.json", { credentials: "include" })
|
||||
fetch("getFileTag.php", { credentials: "include" })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
// If the file doesn't exist, assume there are no global tags.
|
||||
|
||||
@@ -5,7 +5,7 @@ const translations = {
|
||||
"no_files_selected": "No files selected.",
|
||||
"confirm_delete_files": "Are you sure you want to delete {count} selected file(s)?",
|
||||
"element_not_found": "Element with id \"{id}\" not found.",
|
||||
"search_placeholder": "Search files or tag...",
|
||||
"search_placeholder": "Search files, tags, or uploader...",
|
||||
"file_name": "File Name",
|
||||
"date_modified": "Date Modified",
|
||||
"upload_date": "Upload Date",
|
||||
|
||||
19
js/main.js
19
js/main.js
@@ -12,7 +12,8 @@ import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload }
|
||||
import { editFile, saveFile } from './fileEditor.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
|
||||
function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
||||
// Remove the retry logic version and just use loadCsrfToken directly:
|
||||
function loadCsrfToken() {
|
||||
return fetch('token.php', { credentials: 'include' })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
@@ -21,11 +22,9 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Set global variables.
|
||||
window.csrfToken = data.csrf_token;
|
||||
window.SHARE_URL = data.share_url;
|
||||
|
||||
// Update (or create) the CSRF meta tag.
|
||||
|
||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!metaCSRF) {
|
||||
metaCSRF = document.createElement('meta');
|
||||
@@ -34,7 +33,6 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
||||
}
|
||||
metaCSRF.setAttribute('content', data.csrf_token);
|
||||
|
||||
// Update (or create) the share URL meta tag.
|
||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
||||
if (!metaShare) {
|
||||
metaShare = document.createElement('meta');
|
||||
@@ -44,15 +42,6 @@ function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
||||
metaShare.setAttribute('content', data.share_url);
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(error => {
|
||||
if (retries > 0) {
|
||||
console.warn(`CSRF token load failed. Retrying in ${delay}ms... (${retries} retries left)`, error);
|
||||
return new Promise(resolve => setTimeout(resolve, delay))
|
||||
.then(() => loadCsrfTokenWithRetry(retries - 1, delay * 2));
|
||||
}
|
||||
console.error("Failed to load CSRF token after retries.", error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,7 +67,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// Apply the translations to update the UI
|
||||
applyTranslations();
|
||||
// First, load the CSRF token (with retry).
|
||||
loadCsrfTokenWithRetry().then(() => {
|
||||
loadCsrfToken().then(() => {
|
||||
// Once CSRF token is loaded, initialize authentication.
|
||||
initAuth();
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ $username = trim($_SERVER['PHP_AUTH_USER']);
|
||||
$password = trim($_SERVER['PHP_AUTH_PW']);
|
||||
|
||||
// Validate username format (optional)
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
||||
if (!preg_match(REGEX_USER, $username)) {
|
||||
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo 'Invalid username format';
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
@@ -45,7 +42,7 @@ $sourceFolder = trim($data['source']) ?: 'root';
|
||||
$destinationFolder = trim($data['destination']) ?: 'root';
|
||||
|
||||
// Allow only letters, numbers, underscores, dashes, spaces, and forward slashes in folder names.
|
||||
$folderPattern = '/^[A-Za-z0-9_\- \/]+$/';
|
||||
$folderPattern = REGEX_FOLDER_NAME;
|
||||
if ($sourceFolder !== 'root' && !preg_match($folderPattern, $sourceFolder)) {
|
||||
echo json_encode(["error" => "Invalid source folder name."]);
|
||||
exit;
|
||||
@@ -111,7 +108,7 @@ $srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMet
|
||||
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
|
||||
|
||||
$errors = [];
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||
|
||||
foreach ($data['files'] as $fileName) {
|
||||
// Save the original name for metadata lookup.
|
||||
|
||||
@@ -17,9 +17,9 @@ if (!isset($_POST['folder'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = $_POST['folder'];
|
||||
// Validate the folder name (only alphanumerics, dashes allowed)
|
||||
if (!preg_match('/^resumable_[A-Za-z0-9\-]+$/', $folder)) {
|
||||
$folder = urldecode($_POST['folder']);
|
||||
$regex = "/^resumable_" . PATTERN_FOLDER_NAME . "$/u"; // full regex pattern
|
||||
if (!preg_match($regex, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name"]);
|
||||
http_response_code(400);
|
||||
exit;
|
||||
|
||||
@@ -30,7 +30,7 @@ if (!$usernameToRemove) {
|
||||
}
|
||||
|
||||
// Optional: Validate the username format (allow letters, numbers, underscores, dashes, and spaces)
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $usernameToRemove)) {
|
||||
if (!preg_match(REGEX_USER, $usernameToRemove)) {
|
||||
echo json_encode(["error" => "Invalid username format"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($dat
|
||||
|
||||
$folder = trim($data['folder']) ?: 'root';
|
||||
// For subfolders, allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name"]);
|
||||
exit;
|
||||
}
|
||||
@@ -49,7 +49,7 @@ $oldName = basename(trim($data['oldName']));
|
||||
$newName = basename(trim($data['newName']));
|
||||
|
||||
// Validate file names: allow letters, numbers, underscores, dashes, dots, parentheses, and spaces.
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\. \(\)]+$/', $oldName) || !preg_match('/^[A-Za-z0-9_\-\. \(\)]+$/', $newName)) {
|
||||
if (!preg_match(REGEX_FILE_NAME, $oldName) || !preg_match(REGEX_FILE_NAME, $newName)) {
|
||||
echo json_encode(["error" => "Invalid file name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
@@ -48,7 +45,7 @@ $oldFolder = trim($input['oldFolder']);
|
||||
$newFolder = trim($input['newFolder']);
|
||||
|
||||
// Validate folder names
|
||||
if (!preg_match('/^[A-Za-z0-9_\- \/]+$/', $oldFolder) || !preg_match('/^[A-Za-z0-9_\- \/]+$/', $newFolder)) {
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid folder name(s).']);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ if (!isset($data['files']) || !is_array($data['files'])) {
|
||||
}
|
||||
|
||||
// Define a safe file name pattern.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||
|
||||
$restoredItems = [];
|
||||
$errors = [];
|
||||
|
||||
@@ -48,7 +48,7 @@ $folder = isset($data["folder"]) ? trim($data["folder"]) : "root";
|
||||
|
||||
// If a subfolder is provided, validate it.
|
||||
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
||||
if ($folder !== "root" && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
if ($folder !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,21 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
}
|
||||
|
||||
// CSRF Protection: validate token from header.
|
||||
$headers = getallheaders();
|
||||
if (!isset($headers['X-CSRF-Token']) || $headers['X-CSRF-Token'] !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token."]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||
respond('error', 403, 'Invalid CSRF token');
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username) {
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||
echo json_encode(["error" => "Read-only users are not allowed to file tags"]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve and sanitize input.
|
||||
@@ -77,7 +87,7 @@ if ($file === "global") {
|
||||
}
|
||||
|
||||
// Validate folder name.
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -146,6 +146,24 @@ $totalPages = max(1, ceil($totalFiles / $itemsPerPage));
|
||||
$currentPage = min($page, $totalPages);
|
||||
$startIndex = ($currentPage - 1) * $itemsPerPage;
|
||||
$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
|
||||
|
||||
/**
|
||||
* Convert file size in bytes into a human-readable string.
|
||||
*
|
||||
* @param int $bytes The file size in bytes.
|
||||
* @return string The formatted size string.
|
||||
*/
|
||||
function formatBytes($bytes) {
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . " B";
|
||||
} elseif ($bytes < 1024 * 1024) {
|
||||
return round($bytes / 1024, 2) . " KB";
|
||||
} elseif ($bytes < 1024 * 1024 * 1024) {
|
||||
return round($bytes / (1024 * 1024), 2) . " MB";
|
||||
} else {
|
||||
return round($bytes / (1024 * 1024 * 1024), 2) . " GB";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -268,13 +286,13 @@ $filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Size (MB)</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($filesOnPage as $file):
|
||||
$filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file;
|
||||
$sizeMB = round(filesize($filePath) / (1024 * 1024), 2);
|
||||
$fileSize = formatBytes(filesize($filePath));
|
||||
// Build download link using share token and file name.
|
||||
$downloadLink = "downloadSharedFile.php?token=" . urlencode($token) . "&file=" . urlencode($file);
|
||||
?>
|
||||
@@ -285,7 +303,7 @@ $filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
|
||||
<span class="download-icon">⇩</span>
|
||||
</a>
|
||||
</td>
|
||||
<td><?php echo $sizeMB; ?></td>
|
||||
<td><?php echo $fileSize; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
@@ -11,11 +11,11 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
}
|
||||
|
||||
// Verify CSRF token from request headers.
|
||||
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
respond('error', 403, 'Invalid CSRF token');
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
@@ -13,11 +13,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
}
|
||||
|
||||
// ——— 2) CSRF check ———
|
||||
if (empty($_SERVER['HTTP_X_CSRF_TOKEN'])
|
||||
|| $_SERVER['HTTP_X_CSRF_TOKEN'] !== ($_SESSION['csrf_token'] ?? '')) {
|
||||
http_response_code(403);
|
||||
error_log("Invalid CSRF token on recovery for IP {$_SERVER['REMOTE_ADDR']}");
|
||||
exit(json_encode(['status'=>'error','message'=>'Invalid CSRF token']));
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||
respond('error', 403, 'Invalid CSRF token');
|
||||
}
|
||||
|
||||
// ——— 3) Identify user to recover ———
|
||||
@@ -32,7 +32,7 @@ if (!$userId) {
|
||||
}
|
||||
|
||||
// ——— Validate userId format ———
|
||||
if (!preg_match('/^[A-Za-z0-9_\-]+$/', $userId)) {
|
||||
if (!preg_match(REGEX_USER, $userId)) {
|
||||
http_response_code(400);
|
||||
error_log("Invalid userId format: {$userId}");
|
||||
exit(json_encode(['status'=>'error','message'=>'Invalid user identifier']));
|
||||
|
||||
@@ -13,11 +13,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
}
|
||||
|
||||
// 2) CSRF check
|
||||
if (empty($_SERVER['HTTP_X_CSRF_TOKEN'])
|
||||
|| $_SERVER['HTTP_X_CSRF_TOKEN'] !== ($_SESSION['csrf_token'] ?? '')) {
|
||||
http_response_code(403);
|
||||
error_log("totp_saveCode: invalid CSRF token from IP {$_SERVER['REMOTE_ADDR']}");
|
||||
exit(json_encode(['status'=>'error','message'=>'Invalid CSRF token']));
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||
respond('error', 403, 'Invalid CSRF token');
|
||||
}
|
||||
|
||||
// 3) Must be logged in
|
||||
@@ -29,7 +29,7 @@ if (empty($_SESSION['username'])) {
|
||||
|
||||
// 4) Validate username format
|
||||
$userId = $_SESSION['username'];
|
||||
if (!preg_match('/^[A-Za-z0-9_\-]+$/', $userId)) {
|
||||
if (!preg_match(REGEX_USER, $userId)) {
|
||||
http_response_code(400);
|
||||
error_log("totp_saveCode: invalid username format: {$userId}");
|
||||
exit(json_encode(['status'=>'error','message'=>'Invalid user identifier']));
|
||||
|
||||
@@ -6,19 +6,35 @@ require_once 'config.php';
|
||||
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
|
||||
|
||||
// For debugging purposes, you might enable error reporting temporarily:
|
||||
// ini_set('display_errors', 1);
|
||||
// error_reporting(E_ALL);
|
||||
// Define the respond() helper if not already defined.
|
||||
if (!function_exists('respond')) {
|
||||
function respond($status, $code, $message, $data = []) {
|
||||
http_response_code($code);
|
||||
echo json_encode([
|
||||
'status' => $status,
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
// Allow access if the user is authenticated or pending TOTP.
|
||||
if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verify CSRF token provided as a GET parameter.
|
||||
if (!isset($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
|
||||
// Retrieve CSRF token from GET parameter or request headers.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
@@ -108,7 +124,13 @@ function getGlobalOtpauthUrl() {
|
||||
return "";
|
||||
}
|
||||
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||||
new GoogleChartsQrCodeProvider(), // QR code provider
|
||||
'FileRise', // issuer
|
||||
6, // number of digits
|
||||
30, // period in seconds
|
||||
Algorithm::Sha1 // enum case from your Algorithm enum
|
||||
);
|
||||
|
||||
// Retrieve the current TOTP secret for the user.
|
||||
$totpSecret = getUserTOTPSecret($username);
|
||||
@@ -120,8 +142,6 @@ if (!$totpSecret) {
|
||||
}
|
||||
|
||||
// Determine the otpauth URL to use.
|
||||
// If a global OTPAuth URL template is defined, replace placeholders {label} and {secret}.
|
||||
// Otherwise, use the default method.
|
||||
$globalOtpauthUrl = getGlobalOtpauthUrl();
|
||||
if (!empty($globalOtpauthUrl)) {
|
||||
$label = "FileRise:" . $username;
|
||||
@@ -140,7 +160,6 @@ if (!empty($globalOtpauthUrl)) {
|
||||
$result = Builder::create()
|
||||
->writer(new PngWriter())
|
||||
->data($otpauthUrl)
|
||||
->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
|
||||
->build();
|
||||
|
||||
header('Content-Type: ' . $result->getMimeType());
|
||||
|
||||
@@ -8,6 +8,9 @@ require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
||||
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
|
||||
|
||||
try {
|
||||
// standardized error helper
|
||||
function respond($status, $code, $message, $data = []) {
|
||||
@@ -54,8 +57,9 @@ try {
|
||||
respond('error', 403, 'Not authenticated');
|
||||
}
|
||||
|
||||
// CSRF check
|
||||
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$csrfHeader = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||
respond('error', 403, 'Invalid CSRF token');
|
||||
}
|
||||
@@ -71,7 +75,13 @@ try {
|
||||
if (isset($_SESSION['pending_login_user'])) {
|
||||
$username = $_SESSION['pending_login_user'];
|
||||
$totpSecret = $_SESSION['pending_login_secret'];
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||||
new GoogleChartsQrCodeProvider(), // QR code provider
|
||||
'FileRise', // issuer
|
||||
6, // number of digits
|
||||
30, // period in seconds
|
||||
Algorithm::Sha1 // Correct enum case name from your enum
|
||||
);
|
||||
|
||||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||
$_SESSION['totp_failures']++;
|
||||
@@ -117,7 +127,14 @@ try {
|
||||
respond('error', 500, 'TOTP secret not found. Please set up TOTP again.');
|
||||
}
|
||||
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||||
new GoogleChartsQrCodeProvider(), // QR code provider
|
||||
'FileRise', // issuer
|
||||
6, // number of digits
|
||||
30, // period in seconds
|
||||
Algorithm::Sha1 // Correct enum case name from your enum
|
||||
);
|
||||
|
||||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||
$_SESSION['totp_failures']++;
|
||||
respond('error', 400, 'Invalid TOTP code');
|
||||
|
||||
@@ -40,16 +40,39 @@ if (file_exists($permissionsFile)) {
|
||||
$existingPermissions = [];
|
||||
}
|
||||
|
||||
// Load user roles from the users file (similar to getUsers.php)
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$userRoles = [];
|
||||
if (file_exists($usersFile)) {
|
||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(':', trim($line));
|
||||
if (count($parts) >= 3) {
|
||||
// Validate username format:
|
||||
if (preg_match(REGEX_USER, $parts[0])) {
|
||||
// Use a lowercase key for consistency.
|
||||
$userRoles[strtolower($parts[0])] = trim($parts[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through each permission update.
|
||||
foreach ($permissions as $perm) {
|
||||
// Ensure username is provided.
|
||||
if (!isset($perm['username'])) continue;
|
||||
$username = $perm['username'];
|
||||
|
||||
// Look up the user's role from the users file.
|
||||
$role = isset($userRoles[strtolower($username)]) ? $userRoles[strtolower($username)] : null;
|
||||
|
||||
// Skip updating permissions for admin users.
|
||||
if (strtolower($username) === "admin") continue;
|
||||
if ($role === "1") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update permissions: default any missing value to false.
|
||||
$existingPermissions[$username] = [
|
||||
$existingPermissions[strtolower($username)] = [
|
||||
'folderOnly' => isset($perm['folderOnly']) ? (bool)$perm['folderOnly'] : false,
|
||||
'readOnly' => isset($perm['readOnly']) ? (bool)$perm['readOnly'] : false,
|
||||
'disableUpload' => isset($perm['disableUpload']) ? (bool)$perm['disableUpload'] : false
|
||||
|
||||
17
upload.php
17
upload.php
@@ -65,14 +65,16 @@ if (isset($_POST['resumableChunkNumber'])) {
|
||||
$resumableFilename = $_POST['resumableFilename'];
|
||||
|
||||
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
|
||||
http_response_code(400); // Set an error HTTP status code
|
||||
// First, strip directory components.
|
||||
$resumableFilename = urldecode(basename($_POST['resumableFilename']));
|
||||
if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid file name: " . $resumableFilename]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name"]);
|
||||
exit;
|
||||
}
|
||||
@@ -173,7 +175,7 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
|
||||
// ------------- Full Upload (Non-chunked) -------------
|
||||
// Validate folder name input.
|
||||
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name"]);
|
||||
exit;
|
||||
}
|
||||
@@ -195,10 +197,12 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
|
||||
$metadataCollection = []; // key: folder path, value: metadata array
|
||||
$metadataChanged = []; // key: folder path, value: boolean
|
||||
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
// Use a Unicode-enabled pattern to allow special characters.
|
||||
$safeFileNamePattern = REGEX_FILE_NAME;
|
||||
|
||||
foreach ($_FILES["file"]["name"] as $index => $fileName) {
|
||||
$safeFileName = basename($fileName);
|
||||
// First, ensure we only work with the base filename to avoid traversal issues.
|
||||
$safeFileName = trim(urldecode(basename($fileName)));
|
||||
if (!preg_match($safeFileNamePattern, $safeFileName)) {
|
||||
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
||||
exit;
|
||||
@@ -224,6 +228,7 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $resumableFilename)) {
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
|
||||
. str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
// Reapply basename to the relativePath to get the final safe file name.
|
||||
$safeFileName = basename($relativePath);
|
||||
}
|
||||
// --- End Minimal Folder/Subfolder Logic ---
|
||||
|
||||
@@ -109,8 +109,6 @@ if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) {
|
||||
}
|
||||
|
||||
// --- 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;
|
||||
// Sanitize the metadata file name.
|
||||
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
|
||||
|
||||
Reference in New Issue
Block a user