Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d23d5b7f3f | ||
|
|
a48ba09f02 | ||
|
|
61357af203 | ||
|
|
e390a35e8a | ||
|
|
7e50ba1f70 | ||
|
|
cc41f8cc95 | ||
|
|
7c31b9689f | ||
|
|
461921b7bc | ||
|
|
3b58123584 | ||
|
|
cd9d7eb0ba | ||
|
|
c0c8d68dc4 | ||
|
|
2dfcb4062f |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
public/api.html linguist-documentation
|
||||||
|
public/openapi.json linguist-documentation
|
||||||
40
.github/workflows/sync-changelog.yml
vendored
Normal file
40
.github/workflows/sync-changelog.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Sync Changelog to Docker Repo
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'CHANGELOG.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout FileRise
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: file-rise
|
||||||
|
|
||||||
|
- name: Checkout filerise-docker
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: error311/filerise-docker
|
||||||
|
token: ${{ secrets.PAT_TOKEN }}
|
||||||
|
path: docker-repo
|
||||||
|
|
||||||
|
- name: Copy CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
cp file-rise/CHANGELOG.md docker-repo/CHANGELOG.md
|
||||||
|
|
||||||
|
- name: Commit & push
|
||||||
|
working-directory: docker-repo
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add CHANGELOG.md
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
else
|
||||||
|
git commit -m "chore: sync CHANGELOG.md from FileRise"
|
||||||
|
git push origin main
|
||||||
|
fi
|
||||||
88
CHANGELOG.md
88
CHANGELOG.md
@@ -1,5 +1,93 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changes 4/21/2025 v1.2.2
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`src/webdav/CurrentUser.php`**
|
||||||
|
– Introduces a `CurrentUser` singleton to capture and expose the authenticated WebDAV username for use in other components.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **`src/webdav/FileRiseDirectory.php`**
|
||||||
|
– Constructor now takes three parameters (`$path`, `$user`, `$folderOnly`).
|
||||||
|
– Implements “folder‑only” mode: non‑admin users only see their own subfolder under the uploads root.
|
||||||
|
– Passes the current user through to `FileRiseFile` so that uploads/deletions are attributed correctly.
|
||||||
|
|
||||||
|
- **`src/webdav/FileRiseFile.php`**
|
||||||
|
– Uses `CurrentUser::get()` when writing metadata to populate the `uploader` field.
|
||||||
|
– Metadata helper (`updateMetadata`) now records both upload and modified timestamps along with the actual username.
|
||||||
|
|
||||||
|
- **`public/webdav.php`**
|
||||||
|
– Adds a header‐shim at the top to pull Basic‑Auth credentials out of `Authorization` for all HTTP methods.
|
||||||
|
– In the auth callback, sets the `CurrentUser` for the rest of the request.
|
||||||
|
- Admins & unrestricted users see the full `/uploads` directory.
|
||||||
|
- “Folder‑only” users are scoped to `/uploads/{username}`.
|
||||||
|
– Configures SabreDAV with the new `FileRiseDirectory($rootPath, $user, $folderOnly)` signature and sets the base URI to `/webdav.php/`.
|
||||||
|
|
||||||
|
## Changes 4/19/2025 v1.2.1
|
||||||
|
|
||||||
|
- **Extended “Remember Me” cookie behavior**
|
||||||
|
In `AuthController::finalizeLogin()`, after setting `remember_me_token` re‑issued the PHP session cookie with the same 30‑day expiry and called `session_regenerate_id(true)`.
|
||||||
|
|
||||||
|
- **Fetch URL fixes**
|
||||||
|
Changed all front‑end `fetch("api/…")` calls to absolute paths `fetch("/api/…")` to avoid relative‑path 404/403 issues.
|
||||||
|
|
||||||
|
- **CSRF token refresh**
|
||||||
|
Updated `submitLogin()` and both TOTP submission handlers to `async/await` a fresh CSRF token from `/api/auth/token.php` (with `credentials: "include"`) immediately before any POST.
|
||||||
|
|
||||||
|
- **submitLogin() overhaul**
|
||||||
|
Refactored to:
|
||||||
|
1. Fetch CSRF
|
||||||
|
2. POST credentials to `/api/auth/auth.php`
|
||||||
|
3. On `totp_required`, re‑fetch CSRF *again* before calling `openTOTPLoginModal()`
|
||||||
|
4. Handle full logins vs. TOTP flows cleanly.
|
||||||
|
|
||||||
|
- **TOTP handlers update**
|
||||||
|
In both the “Confirm TOTP” button flow and the auto‑submit on 6‑digit input:
|
||||||
|
- Refreshed CSRF token before every `/api/totp_verify.php` call
|
||||||
|
- Checked `response.ok` before parsing JSON
|
||||||
|
- Improved `.catch` error handling
|
||||||
|
|
||||||
|
- **verifyTOTP() endpoint enhancement**
|
||||||
|
Inside the **pending‑login** branch of `verifyTOTP()`:
|
||||||
|
- Pulled `$_SESSION['pending_login_remember_me']`
|
||||||
|
- If true, wrote the persistent token store, set `remember_me_token`, re‑issued the session cookie, and regenerated the session ID
|
||||||
|
- Cleaned up pending session variables
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 4/18/2025
|
||||||
|
|
||||||
|
### fileListView.js
|
||||||
|
|
||||||
|
- Seed and persist `itemsPerPage` from `localStorage`
|
||||||
|
- Use `window.itemsPerPage` for pagination in gallery
|
||||||
|
- Enable search input filtering in gallery mode
|
||||||
|
- Always re‑render the view‑toggle button on gallery load
|
||||||
|
- Restore per‑card action buttons (download, edit, rename, share)
|
||||||
|
- Assign real `value` to checkboxes and call `updateFileActionButtons()` on change
|
||||||
|
- Update `changePage` and `changeItemsPerPage` to respect `viewMode`
|
||||||
|
|
||||||
|
### fileTags.js
|
||||||
|
|
||||||
|
- Import `renderFileTable` and `renderGalleryView`
|
||||||
|
- Re‑render the list after saving a single‑file tag
|
||||||
|
- Re‑render the list after saving multi‑file tags
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 4/17/2025
|
||||||
|
|
||||||
|
- Generate OpenAPI spec and API HTML docs
|
||||||
|
- Fully auto‑generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
||||||
|
- .gitattributes added to mark (`openapi.json`) & (`api.html`) as documentation.
|
||||||
|
- User Panel added API Docs link.
|
||||||
|
- Adjusted remember_me_token.
|
||||||
|
- Test pipeline
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 4/16 Refactor API endpoints and modularize controllers and models
|
## Changes 4/16 Refactor API endpoints and modularize controllers and models
|
||||||
|
|
||||||
- Reorganized project structure to separate API logic into dedicated controllers and models:
|
- Reorganized project structure to separate API logic into dedicated controllers and models:
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@@ -50,8 +50,18 @@ ARG PGID=100
|
|||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get upgrade -y && \
|
apt-get upgrade -y && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
apache2 php php-json php-curl php-zip php-mbstring php-gd \
|
apache2 \
|
||||||
ca-certificates curl git openssl && \
|
php \
|
||||||
|
php-json \
|
||||||
|
php-curl \
|
||||||
|
php-zip \
|
||||||
|
php-mbstring \
|
||||||
|
php-gd \
|
||||||
|
php-xml \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
openssl && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|||||||
56
README.md
56
README.md
@@ -20,6 +20,10 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
|||||||
|
|
||||||
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) – convenient for sending individual files without exposing the whole app.
|
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) – convenient for sending individual files without exposing the whole app.
|
||||||
|
|
||||||
|
- 🔌 **WebDAV Support:** Mount FileRise as a network drive or connect via any WebDAV client. Supports standard file operations (upload/download/rename/delete) and direct `curl`/CLI access for scripting and automation. FolderOnly users are restricted to their personal folder, while admins and unrestricted users have full access. Compatible with Cyberduck, WinSCP, native OS drive mounts, and more.
|
||||||
|
|
||||||
|
- 📚 **API Documentation:** Fully auto‑generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
||||||
|
|
||||||
- 📝 **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 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.
|
- 🏷️ **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.
|
||||||
@@ -28,11 +32,11 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
|||||||
|
|
||||||
- 🎨 **Responsive UI (Dark/Light Mode):** FileRise is mobile-friendly out of the box – manage files from your phone or tablet with a responsive layout. Choose between Dark mode or Light theme, or let it follow your system preference. The interface remembers your preferences (layout, items per page, last visited folder, etc.) for a personalized experience each time.
|
- 🎨 **Responsive UI (Dark/Light Mode):** FileRise is mobile-friendly out of the box – manage files from your phone or tablet with a responsive layout. Choose between Dark mode or Light theme, or let it follow your system preference. The interface remembers your preferences (layout, items per page, last visited folder, etc.) for a personalized experience each time.
|
||||||
|
|
||||||
- 🌐 **Internationalization & Localization:** FileRise supports multiple languages via an integrated i18n system. Users can switch languages through a user panel dropdown, and their choice is saved in local storage for a consistent experience across sessions. Currently available in English, Spanish, and French—please report any translation issues you encounter.
|
- 🌐 **Internationalization & Localization:** FileRise supports multiple languages via an integrated i18n system. Users can switch languages through a user panel dropdown, and their choice is saved in local storage for a consistent experience across sessions. Currently available in English, Spanish, French & German—please report any translation issues you encounter.
|
||||||
|
|
||||||
- 🗑️ **Trash & File Recovery:** Mistakenly deleted files? No worries – deleted items go to the Trash instead of immediate removal. Admins can restore files from Trash or empty it to free space. FileRise auto-purges old trash entries (default 3 days) to keep your storage tidy.
|
- 🗑️ **Trash & File Recovery:** Mistakenly deleted files? No worries – deleted items go to the Trash instead of immediate removal. Admins can restore files from Trash or empty it to free space. FileRise auto-purges old trash entries (default 3 days) to keep your storage tidy.
|
||||||
|
|
||||||
- ⚙️ **Lightweight & Self-Contained:** FileRise runs on PHP 8.1+ with no external database required – data is stored in files (users, metadata) for simplicity. It’s a single-folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre-built image for a hassle-free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features.
|
- ⚙️ **Lightweight & Self‑Contained:** FileRise runs on PHP 8.1+ with no external database required – data is stored in files (users, metadata) for simplicity. It’s a single‑folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre‑built image for a hassle‑free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features.
|
||||||
|
|
||||||
(For a full list of features and detailed changelogs, see the [Wiki](https://github.com/error311/FileRise/wiki), [changelog](https://github.com/error311/FileRise/blob/master/CHANGELOG.md) or the [releases](https://github.com/error311/FileRise/releases) pages.)
|
(For a full list of features and detailed changelogs, see the [Wiki](https://github.com/error311/FileRise/wiki), [changelog](https://github.com/error311/FileRise/blob/master/CHANGELOG.md) or the [releases](https://github.com/error311/FileRise/releases) pages.)
|
||||||
|
|
||||||
@@ -58,8 +62,6 @@ If you have Docker installed, you can get FileRise up and running in minutes:
|
|||||||
docker pull error311/filerise-docker:latest
|
docker pull error311/filerise-docker:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
*(For Apple Silicon (M1/M2) users, use --platform linux/amd64 tag until multi-arch support is added.)*
|
|
||||||
|
|
||||||
- **Run a container:**
|
- **Run a container:**
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
@@ -145,6 +147,51 @@ Now navigate to the FileRise URL in your browser. On first load, you’ll be pro
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Quick‑start: Mount via WebDAV
|
||||||
|
|
||||||
|
Once FileRise is running, you can mount it like any other network drive:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux (GVFS/GIO)
|
||||||
|
gio mount dav://demo@your-host/webdav.php/
|
||||||
|
|
||||||
|
# macOS (Finder → Go → Connect to Server…)
|
||||||
|
dav://demo@your-host/webdav.php/
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (File Explorer)
|
||||||
|
|
||||||
|
- Open **File Explorer** → Right-click **This PC** → **Map network drive…**
|
||||||
|
- Choose a drive letter (e.g., `Z:`).
|
||||||
|
- In **Folder**, enter:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://your-host/webdav.php/
|
||||||
|
```
|
||||||
|
|
||||||
|
- Check **Connect using different credentials**, and enter your FileRise username and password.
|
||||||
|
- Click **Finish**. The drive will now appear under **This PC**.
|
||||||
|
|
||||||
|
> **Important:**
|
||||||
|
> Windows requires HTTPS (SSL) for WebDAV connections by default.
|
||||||
|
> If your server uses plain HTTP, you must adjust a registry setting:
|
||||||
|
>
|
||||||
|
> 1. Open **Registry Editor** (`regedit.exe`).
|
||||||
|
> 2. Navigate to:
|
||||||
|
>
|
||||||
|
> ```text
|
||||||
|
> HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> 3. Find or create a `DWORD` value named **BasicAuthLevel**.
|
||||||
|
> 4. Set its value to `2`.
|
||||||
|
> 5. Restart the **WebClient** service or reboot your computer.
|
||||||
|
|
||||||
|
📖 For a full guide (including SSL setup, HTTP workaround, and troubleshooting), see the [WebDAV Usage Wiki](https://github.com/error311/FileRise/wiki/WebDAV).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## FAQ / Troubleshooting
|
## FAQ / Troubleshooting
|
||||||
|
|
||||||
- **“Upload failed” or large files not uploading:** Make sure `TOTAL_UPLOAD_SIZE` in config and PHP’s `post_max_size` / `upload_max_filesize` are all set high enough. For extremely large files, you might also need to increase max_execution_time in PHP or rely on the resumable upload feature in smaller chunks.
|
- **“Upload failed” or large files not uploading:** Make sure `TOTAL_UPLOAD_SIZE` in config and PHP’s `post_max_size` / `upload_max_filesize` are all set high enough. For extremely large files, you might also need to increase max_execution_time in PHP or rely on the resumable upload feature in smaller chunks.
|
||||||
@@ -185,6 +232,7 @@ Areas where you can help: translations, bug fixes, UI improvements, or building
|
|||||||
- **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7)
|
- **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7)
|
||||||
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
|
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
|
||||||
- **[endroid/qr-code](https://github.com/endroid/qr-code)** (v^5.0)
|
- **[endroid/qr-code](https://github.com/endroid/qr-code)** (v^5.0)
|
||||||
|
- **[sabre/dav"](https://github.com/sabre-io/dav)** (^4.4)
|
||||||
|
|
||||||
### Client-Side Libraries
|
### Client-Side Libraries
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"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": "^3.0",
|
"robthree/twofactorauth": "^3.0",
|
||||||
"endroid/qr-code": "^5.0"
|
"endroid/qr-code": "^5.0",
|
||||||
|
"sabre/dav": "^4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
497
composer.lock
generated
497
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"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": "6b70aec0c1830ebb2b8f9bb625b04a22",
|
"content-hash": "3a9b8d9fcfdaaa865ba03eab392e88fd",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -451,6 +451,56 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-12-14T21:12:59+00:00"
|
"time": "2024-12-14T21:12:59+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/log",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/log.git",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Log\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for logging libraries",
|
||||||
|
"homepage": "https://github.com/php-fig/log",
|
||||||
|
"keywords": [
|
||||||
|
"log",
|
||||||
|
"psr",
|
||||||
|
"psr-3"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||||
|
},
|
||||||
|
"time": "2024-09-11T13:17:53+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "robthree/twofactorauth",
|
"name": "robthree/twofactorauth",
|
||||||
"version": "v3.0.2",
|
"version": "v3.0.2",
|
||||||
@@ -531,6 +581,451 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-10-24T15:14:25+00:00"
|
"time": "2024-10-24T15:14:25+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/dav",
|
||||||
|
"version": "4.7.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/dav.git",
|
||||||
|
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
|
||||||
|
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-date": "*",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-pcre": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"ext-spl": "*",
|
||||||
|
"lib-libxml": ">=2.7.0",
|
||||||
|
"php": "^7.1.0 || ^8.0",
|
||||||
|
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"sabre/event": "^5.0",
|
||||||
|
"sabre/http": "^5.0.5",
|
||||||
|
"sabre/uri": "^2.0",
|
||||||
|
"sabre/vobject": "^4.2.1",
|
||||||
|
"sabre/xml": "^2.0.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^2.19",
|
||||||
|
"monolog/monolog": "^1.27 || ^2.0",
|
||||||
|
"phpstan/phpstan": "^0.12 || ^1.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.0",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-imap": "*",
|
||||||
|
"ext-pdo": "*"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/sabredav",
|
||||||
|
"bin/naturalselection"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "WebDAV Framework for PHP",
|
||||||
|
"homepage": "http://sabre.io/",
|
||||||
|
"keywords": [
|
||||||
|
"CalDAV",
|
||||||
|
"CardDAV",
|
||||||
|
"WebDAV",
|
||||||
|
"framework",
|
||||||
|
"iCalendar"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/dav/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-dav"
|
||||||
|
},
|
||||||
|
"time": "2024-10-29T11:46:02+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/event",
|
||||||
|
"version": "5.1.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/event.git",
|
||||||
|
"reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2",
|
||||||
|
"reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "~2.17.1||^3.63",
|
||||||
|
"phpstan/phpstan": "^0.12",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/coroutine.php",
|
||||||
|
"lib/Loop/functions.php",
|
||||||
|
"lib/Promise/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\Event\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "sabre/event is a library for lightweight event-based programming",
|
||||||
|
"homepage": "http://sabre.io/event/",
|
||||||
|
"keywords": [
|
||||||
|
"EventEmitter",
|
||||||
|
"async",
|
||||||
|
"coroutine",
|
||||||
|
"eventloop",
|
||||||
|
"events",
|
||||||
|
"hooks",
|
||||||
|
"plugin",
|
||||||
|
"promise",
|
||||||
|
"reactor",
|
||||||
|
"signal"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/event/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-event"
|
||||||
|
},
|
||||||
|
"time": "2024-08-27T11:23:05+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/http",
|
||||||
|
"version": "5.1.12",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/http.git",
|
||||||
|
"reference": "dedff73f3995578bc942fa4c8484190cac14f139"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/http/zipball/dedff73f3995578bc942fa4c8484190cac14f139",
|
||||||
|
"reference": "dedff73f3995578bc942fa4c8484190cac14f139",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"sabre/event": ">=4.0 <6.0",
|
||||||
|
"sabre/uri": "^2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "~2.17.1||^3.63",
|
||||||
|
"phpstan/phpstan": "^0.12",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-curl": " to make http requests with the Client class"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\HTTP\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
|
||||||
|
"homepage": "https://github.com/fruux/sabre-http",
|
||||||
|
"keywords": [
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/http/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-http"
|
||||||
|
},
|
||||||
|
"time": "2024-08-27T16:07:41+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/uri",
|
||||||
|
"version": "2.3.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/uri.git",
|
||||||
|
"reference": "b76524c22de90d80ca73143680a8e77b1266c291"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291",
|
||||||
|
"reference": "b76524c22de90d80ca73143680a8e77b1266c291",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.63",
|
||||||
|
"phpstan/extension-installer": "^1.4",
|
||||||
|
"phpstan/phpstan": "^1.12",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.4",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.6",
|
||||||
|
"phpunit/phpunit": "^9.6"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\Uri\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Functions for making sense out of URIs.",
|
||||||
|
"homepage": "http://sabre.io/uri/",
|
||||||
|
"keywords": [
|
||||||
|
"rfc3986",
|
||||||
|
"uri",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/uri/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-uri"
|
||||||
|
},
|
||||||
|
"time": "2024-08-27T12:18:16+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/vobject",
|
||||||
|
"version": "4.5.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/vobject.git",
|
||||||
|
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
|
||||||
|
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"sabre/xml": "^2.1 || ^3.0 || ^4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "~2.17.1",
|
||||||
|
"phpstan/phpstan": "^0.12 || ^1.12 || ^2.0",
|
||||||
|
"phpunit/php-invoker": "^2.0 || ^3.1",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"hoa/bench": "If you would like to run the benchmark scripts"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/vobject",
|
||||||
|
"bin/generate_vcards"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\VObject\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dominik Tobschall",
|
||||||
|
"email": "dominik@fruux.com",
|
||||||
|
"homepage": "http://tobschall.de/",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ivan Enderlin",
|
||||||
|
"email": "ivan.enderlin@hoa-project.net",
|
||||||
|
"homepage": "http://mnt.io/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
|
||||||
|
"homepage": "http://sabre.io/vobject/",
|
||||||
|
"keywords": [
|
||||||
|
"availability",
|
||||||
|
"freebusy",
|
||||||
|
"iCalendar",
|
||||||
|
"ical",
|
||||||
|
"ics",
|
||||||
|
"jCal",
|
||||||
|
"jCard",
|
||||||
|
"recurrence",
|
||||||
|
"rfc2425",
|
||||||
|
"rfc2426",
|
||||||
|
"rfc2739",
|
||||||
|
"rfc4770",
|
||||||
|
"rfc5545",
|
||||||
|
"rfc5546",
|
||||||
|
"rfc6321",
|
||||||
|
"rfc6350",
|
||||||
|
"rfc6351",
|
||||||
|
"rfc6474",
|
||||||
|
"rfc6638",
|
||||||
|
"rfc6715",
|
||||||
|
"rfc6868",
|
||||||
|
"vCalendar",
|
||||||
|
"vCard",
|
||||||
|
"vcf",
|
||||||
|
"xCal",
|
||||||
|
"xCard"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/vobject/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-vobject"
|
||||||
|
},
|
||||||
|
"time": "2025-04-17T09:22:48+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/xml",
|
||||||
|
"version": "2.2.11",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/xml.git",
|
||||||
|
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
|
||||||
|
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-xmlreader": "*",
|
||||||
|
"ext-xmlwriter": "*",
|
||||||
|
"lib-libxml": ">=2.6.20",
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"sabre/uri": ">=1.0,<3.0.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "~2.17.1||3.63.2",
|
||||||
|
"phpstan/phpstan": "^0.12",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/Deserializer/functions.php",
|
||||||
|
"lib/Serializer/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\Xml\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Markus Staab",
|
||||||
|
"email": "markus.staab@redaxo.de",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "sabre/xml is an XML library that you may not hate.",
|
||||||
|
"homepage": "https://sabre.io/xml/",
|
||||||
|
"keywords": [
|
||||||
|
"XMLReader",
|
||||||
|
"XMLWriter",
|
||||||
|
"dom",
|
||||||
|
"xml"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/xml/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-xml"
|
||||||
|
},
|
||||||
|
"time": "2024-09-06T07:37:46+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
|
|||||||
@@ -1,73 +1,61 @@
|
|||||||
<?php
|
<?php
|
||||||
// config.php
|
// config.php
|
||||||
|
|
||||||
|
// Prevent caching
|
||||||
header("Cache-Control: no-cache, must-revalidate");
|
header("Cache-Control: no-cache, must-revalidate");
|
||||||
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
|
|
||||||
header("Pragma: no-cache");
|
header("Pragma: no-cache");
|
||||||
|
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
|
||||||
header("Expires: 0");
|
header("Expires: 0");
|
||||||
header('X-Content-Type-Options: nosniff');
|
|
||||||
// Security headers
|
// Security headers
|
||||||
header("X-Content-Type-Options: nosniff");
|
header('X-Content-Type-Options: nosniff');
|
||||||
header("X-Frame-Options: SAMEORIGIN");
|
header("X-Frame-Options: SAMEORIGIN");
|
||||||
header("Referrer-Policy: no-referrer-when-downgrade");
|
header("Referrer-Policy: no-referrer-when-downgrade");
|
||||||
// Only include Strict-Transport-Security if you are using HTTPS
|
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
|
||||||
|
header("X-XSS-Protection: 1; mode=block");
|
||||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||||
header("Strict-Transport-Security: max-age=31536000; includeSubDomains; preload");
|
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('PROJECT_ROOT', dirname(__DIR__));
|
define('PROJECT_ROOT', dirname(__DIR__));
|
||||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||||
define('USERS_DIR', '/var/www/users/');
|
define('USERS_DIR', '/var/www/users/');
|
||||||
define('USERS_FILE', 'users.txt');
|
define('USERS_FILE', 'users.txt');
|
||||||
define('META_DIR', '/var/www/metadata/');
|
define('META_DIR', '/var/www/metadata/');
|
||||||
define('META_FILE', 'file_metadata.json');
|
define('META_FILE', 'file_metadata.json');
|
||||||
define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
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('REGEX_FOLDER_NAME', '/^[\p{L}\p{N}_\-\s\/\\\\]+$/u');
|
||||||
define('PATTERN_FOLDER_NAME', '[\p{L}\p{N}_\-\s\/\\\\]+');
|
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
|
||||||
define('REGEX_FILE_NAME', '/^[\p{L}\p{N}\p{M}%\-\.\(\) _]+$/u');
|
define('REGEX_FILE_NAME', '/^[\p{L}\p{N}\p{M}%\-\.\(\) _]+$/u');
|
||||||
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
||||||
|
|
||||||
date_default_timezone_set(TIMEZONE);
|
date_default_timezone_set(TIMEZONE);
|
||||||
|
|
||||||
/**
|
// Encryption helpers
|
||||||
* Encrypts data using AES-256-CBC.
|
|
||||||
*
|
|
||||||
* @param string $data The plaintext.
|
|
||||||
* @param string $encryptionKey The encryption key.
|
|
||||||
* @return string Base64-encoded string containing IV and ciphertext.
|
|
||||||
*/
|
|
||||||
function encryptData($data, $encryptionKey)
|
function encryptData($data, $encryptionKey)
|
||||||
{
|
{
|
||||||
$cipher = 'AES-256-CBC';
|
$cipher = 'AES-256-CBC';
|
||||||
$ivlen = openssl_cipher_iv_length($cipher);
|
$ivlen = openssl_cipher_iv_length($cipher);
|
||||||
$iv = openssl_random_pseudo_bytes($ivlen);
|
$iv = openssl_random_pseudo_bytes($ivlen);
|
||||||
$ciphertext = openssl_encrypt($data, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
$ct = openssl_encrypt($data, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||||
return base64_encode($iv . $ciphertext);
|
return base64_encode($iv . $ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypts data encrypted with AES-256-CBC.
|
|
||||||
*
|
|
||||||
* @param string $encryptedData Base64-encoded data containing IV and ciphertext.
|
|
||||||
* @param string $encryptionKey The encryption key.
|
|
||||||
* @return string|false The decrypted plaintext or false on failure.
|
|
||||||
*/
|
|
||||||
function decryptData($encryptedData, $encryptionKey)
|
function decryptData($encryptedData, $encryptionKey)
|
||||||
{
|
{
|
||||||
$cipher = 'AES-256-CBC';
|
$cipher = 'AES-256-CBC';
|
||||||
$data = base64_decode($encryptedData);
|
$data = base64_decode($encryptedData);
|
||||||
$ivlen = openssl_cipher_iv_length($cipher);
|
$ivlen = openssl_cipher_iv_length($cipher);
|
||||||
$iv = substr($data, 0, $ivlen);
|
$iv = substr($data, 0, $ivlen);
|
||||||
$ciphertext = substr($data, $ivlen);
|
$ct = substr($data, $ivlen);
|
||||||
return openssl_decrypt($ciphertext, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
return openssl_decrypt($ct, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load encryption key from environment (override in production).
|
// Load encryption key
|
||||||
$envKey = getenv('PERSISTENT_TOKENS_KEY');
|
$envKey = getenv('PERSISTENT_TOKENS_KEY');
|
||||||
if ($envKey === false || $envKey === '') {
|
if ($envKey === false || $envKey === '') {
|
||||||
$encryptionKey = 'default_please_change_this_key';
|
$encryptionKey = 'default_please_change_this_key';
|
||||||
@@ -76,97 +64,89 @@ if ($envKey === false || $envKey === '') {
|
|||||||
$encryptionKey = $envKey;
|
$encryptionKey = $envKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to load JSON permissions (with optional decryption)
|
||||||
function loadUserPermissions($username)
|
function loadUserPermissions($username)
|
||||||
{
|
{
|
||||||
global $encryptionKey;
|
global $encryptionKey;
|
||||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||||
|
|
||||||
if (file_exists($permissionsFile)) {
|
if (file_exists($permissionsFile)) {
|
||||||
$content = file_get_contents($permissionsFile);
|
$content = file_get_contents($permissionsFile);
|
||||||
|
$decrypted = decryptData($content, $encryptionKey);
|
||||||
// Try to decrypt the content.
|
$json = ($decrypted !== false) ? $decrypted : $content;
|
||||||
$decryptedContent = decryptData($content, $encryptionKey);
|
$perms = json_decode($json, true);
|
||||||
if ($decryptedContent !== false) {
|
if (is_array($perms) && isset($perms[$username])) {
|
||||||
$permissions = json_decode($decryptedContent, true);
|
return !empty($perms[$username]) ? $perms[$username] : false;
|
||||||
} else {
|
|
||||||
$permissions = json_decode($content, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($permissions) && array_key_exists($username, $permissions)) {
|
|
||||||
$result = $permissions[$username];
|
|
||||||
return !empty($result) ? $result : false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Removed error_log() to prevent flooding logs when file is not found.
|
return false;
|
||||||
return false; // Return false if no permissions found.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine whether HTTPS is used.
|
// Determine HTTPS usage
|
||||||
$envSecure = getenv('SECURE');
|
$envSecure = getenv('SECURE');
|
||||||
if ($envSecure !== false) {
|
$secure = ($envSecure !== false)
|
||||||
$secure = filter_var($envSecure, FILTER_VALIDATE_BOOLEAN);
|
? filter_var($envSecure, FILTER_VALIDATE_BOOLEAN)
|
||||||
} else {
|
: (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
|
||||||
}
|
|
||||||
|
|
||||||
$cookieParams = [
|
// Choose session lifetime based on "remember me" cookie
|
||||||
'lifetime' => 7200,
|
$defaultSession = 7200; // 2 hours
|
||||||
|
$persistentDays = 30 * 24 * 60 * 60; // 30 days
|
||||||
|
$sessionLifetime = isset($_COOKIE['remember_me_token'])
|
||||||
|
? $persistentDays
|
||||||
|
: $defaultSession;
|
||||||
|
|
||||||
|
// Configure PHP session cookie and GC
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => $sessionLifetime,
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'domain' => '', // Set your domain as needed.
|
'domain' => '', // adjust if you need a specific domain
|
||||||
'secure' => $secure,
|
'secure' => $secure,
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Lax'
|
||||||
];
|
]);
|
||||||
// At the very beginning of config.php
|
ini_set('session.gc_maxlifetime', (string)$sessionLifetime);
|
||||||
/*ini_set('session.save_path', __DIR__ . '/../sessions');
|
|
||||||
if (!is_dir(__DIR__ . '/../sessions')) {
|
|
||||||
mkdir(__DIR__ . '/../sessions', 0777, true);
|
|
||||||
}*/
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_set_cookie_params($cookieParams);
|
|
||||||
ini_set('session.gc_maxlifetime', 7200);
|
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSRF token
|
||||||
if (empty($_SESSION['csrf_token'])) {
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-login via persistent token.
|
// Auto‑login via persistent token
|
||||||
if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token'])) {
|
if (empty($_SESSION["authenticated"]) && !empty($_COOKIE['remember_me_token'])) {
|
||||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
$persistentTokens = [];
|
$tokens = [];
|
||||||
if (file_exists($persistentTokensFile)) {
|
if (file_exists($tokFile)) {
|
||||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
$enc = file_get_contents($tokFile);
|
||||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
$dec = decryptData($enc, $encryptionKey);
|
||||||
$persistentTokens = json_decode($decryptedContent, true);
|
$tokens = json_decode($dec, true) ?: [];
|
||||||
if (!is_array($persistentTokens)) {
|
|
||||||
$persistentTokens = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (isset($persistentTokens[$_COOKIE['remember_me_token']])) {
|
$token = $_COOKIE['remember_me_token'];
|
||||||
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
|
if (!empty($tokens[$token])) {
|
||||||
if ($tokenData['expiry'] >= time()) {
|
$data = $tokens[$token];
|
||||||
|
if ($data['expiry'] >= time()) {
|
||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $tokenData["username"];
|
$_SESSION["username"] = $data["username"];
|
||||||
// IMPORTANT: Set the folderOnly flag here for auto-login.
|
$_SESSION["folderOnly"] = loadUserPermissions($data["username"]);
|
||||||
$_SESSION["folderOnly"] = loadUserPermissions($tokenData["username"]);
|
$_SESSION["isAdmin"] = !empty($data["isAdmin"]);
|
||||||
} else {
|
} else {
|
||||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
// expired — clean up
|
||||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
unset($tokens[$token]);
|
||||||
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
|
file_put_contents($tokFile, encryptData(json_encode($tokens, JSON_PRETTY_PRINT), $encryptionKey), LOCK_EX);
|
||||||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Share URL fallback
|
||||||
define('BASE_URL', 'http://yourwebsite/uploads/');
|
define('BASE_URL', 'http://yourwebsite/uploads/');
|
||||||
|
|
||||||
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||||
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
|
$defaultShare = isset($_SERVER['HTTP_HOST'])
|
||||||
? "http://" . $_SERVER['HTTP_HOST'] . "/api/file/share.php"
|
? "http://{$_SERVER['HTTP_HOST']}/api/file/share.php"
|
||||||
: "http://localhost/api/file/share.php";
|
: "http://localhost/api/file/share.php";
|
||||||
} else {
|
} else {
|
||||||
$defaultShareUrl = rtrim(BASE_URL, '/') . "/api/file/share.php";
|
$defaultShare = rtrim(BASE_URL, '/') . "/api/file/share.php";
|
||||||
}
|
}
|
||||||
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
define('SHARE_URL', getenv('SHARE_URL') ?: $defaultShare);
|
||||||
20
public/api.html
Normal file
20
public/api.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!-- public/api.html -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>FileRise API Docs</title>
|
||||||
|
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js" integrity="sha384-4vOjrBu7SuDWXcAw1qFznVLA/sKL+0l4nn+J1HY8w7cpa6twQEYuh4b0Cwuo7CyX" crossorigin="anonymous"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<redoc spec-url="openapi.json"></redoc>
|
||||||
|
<div id="redoc-container"></div>
|
||||||
|
<script>
|
||||||
|
// If the <redoc> tag didn’t render, fall back to init()
|
||||||
|
if (!customElements.get('redoc')) {
|
||||||
|
Redoc.init('openapi.json', {}, document.getElementById('redoc-container'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -95,7 +95,7 @@ function updateLoginOptionsUIFromStorage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function loadAdminConfigFunc() {
|
export function loadAdminConfigFunc() {
|
||||||
return fetch("api/admin/getConfig.php", { credentials: "include" })
|
return fetch("/api/admin/getConfig.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(config => {
|
.then(config => {
|
||||||
localStorage.setItem("headerTitle", config.header_title || "FileRise");
|
localStorage.setItem("headerTitle", config.header_title || "FileRise");
|
||||||
@@ -105,7 +105,7 @@ export function loadAdminConfigFunc() {
|
|||||||
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");
|
||||||
|
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
|
|
||||||
const headerTitleElem = document.querySelector(".header-title h1");
|
const headerTitleElem = document.querySelector(".header-title h1");
|
||||||
@@ -149,9 +149,9 @@ 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");
|
||||||
localStorage.setItem("readOnly", data.readOnly ? "true" : "false");
|
localStorage.setItem("readOnly", data.readOnly ? "true" : "false");
|
||||||
localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false");
|
localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,11 +198,11 @@ function updateAuthenticatedUI(data) {
|
|||||||
userPanelBtn.classList.add("btn", "btn-user");
|
userPanelBtn.classList.add("btn", "btn-user");
|
||||||
userPanelBtn.setAttribute("data-i18n-title", "user_panel");
|
userPanelBtn.setAttribute("data-i18n-title", "user_panel");
|
||||||
userPanelBtn.innerHTML = '<i class="material-icons">account_circle</i>';
|
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";
|
||||||
@@ -214,7 +214,7 @@ function updateAuthenticatedUI(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkAuthentication(showLoginToast = true) {
|
function checkAuthentication(showLoginToast = true) {
|
||||||
return sendRequest("api/auth/checkAuth.php")
|
return sendRequest("/api/auth/checkAuth.php")
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.setup) {
|
if (data.setup) {
|
||||||
window.setupMode = true;
|
window.setupMode = true;
|
||||||
@@ -228,9 +228,9 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
}
|
}
|
||||||
window.setupMode = false;
|
window.setupMode = false;
|
||||||
if (data.authenticated) {
|
if (data.authenticated) {
|
||||||
localStorage.setItem("folderOnly", data.folderOnly );
|
localStorage.setItem("folderOnly", data.folderOnly);
|
||||||
localStorage.setItem("readOnly", data.readOnly );
|
localStorage.setItem("readOnly", data.readOnly);
|
||||||
localStorage.setItem("disableUpload",data.disableUpload);
|
localStorage.setItem("disableUpload", data.disableUpload);
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
if (typeof data.totp_enabled !== "undefined") {
|
if (typeof data.totp_enabled !== "undefined") {
|
||||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||||
@@ -251,55 +251,71 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------- Authentication Submission ----------------- */
|
/* ----------------- Authentication Submission ----------------- */
|
||||||
function submitLogin(data) {
|
async function submitLogin(data) {
|
||||||
setLastLoginData(data);
|
setLastLoginData(data);
|
||||||
window.__lastLoginData = data;
|
window.__lastLoginData = data;
|
||||||
|
|
||||||
sendRequest("api/auth/auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
try {
|
||||||
.then(response => {
|
// ─── 1) Get CSRF for the initial auth call ───
|
||||||
if (response.success || response.status === "ok") {
|
let res = await fetch("/api/auth/token.php", { credentials: "include" });
|
||||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
|
if (!res.ok) throw new Error("Could not fetch CSRF token");
|
||||||
// Fetch and update permissions, then reload.
|
window.csrfToken = (await res.json()).csrf_token;
|
||||||
sendRequest("api/getUserPermissions.php", "GET")
|
|
||||||
.then(permissionData => {
|
// ─── 2) Send credentials ───
|
||||||
if (permissionData && typeof permissionData === "object") {
|
const response = await sendRequest(
|
||||||
localStorage.setItem("folderOnly", permissionData.folderOnly ? "true" : "false");
|
"/api/auth/auth.php",
|
||||||
localStorage.setItem("readOnly", permissionData.readOnly ? "true" : "false");
|
"POST",
|
||||||
localStorage.setItem("disableUpload", permissionData.disableUpload ? "true" : "false");
|
data,
|
||||||
}
|
{ "X-CSRF-Token": window.csrfToken }
|
||||||
})
|
);
|
||||||
.catch(() => {
|
|
||||||
// ignore permission‐fetch errors
|
// ─── 3a) Full login (no TOTP) ───
|
||||||
})
|
if (response.success || response.status === "ok") {
|
||||||
.finally(() => {
|
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
|
||||||
window.location.reload();
|
// … fetch permissions & reload …
|
||||||
});
|
try {
|
||||||
} else if (response.totp_required) {
|
const perm = await sendRequest("/api/getUserPermissions.php", "GET");
|
||||||
openTOTPLoginModal();
|
if (perm && typeof perm === "object") {
|
||||||
} else if (response.error && response.error.includes("Too many failed login attempts")) {
|
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
||||||
showToast(response.error);
|
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
||||||
const loginButton = document.querySelector("#authForm button[type='submit']");
|
localStorage.setItem("disableUpload",perm.disableUpload? "true" : "false");
|
||||||
if (loginButton) {
|
|
||||||
loginButton.disabled = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
loginButton.disabled = false;
|
|
||||||
showToast("You can now try logging in again.");
|
|
||||||
}, 30 * 60 * 1000);
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch {}
|
||||||
showToast("Login failed: " + (response.error || "Unknown error"));
|
return window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 3b) TOTP required ───
|
||||||
|
if (response.totp_required) {
|
||||||
|
// **Refresh** CSRF before the TOTP verify call
|
||||||
|
res = await fetch("/api/auth/token.php", { credentials: "include" });
|
||||||
|
if (res.ok) {
|
||||||
|
window.csrfToken = (await res.json()).csrf_token;
|
||||||
}
|
}
|
||||||
})
|
// now open the modal—any totp_verify fetch from here on will use the new token
|
||||||
.catch(err => {
|
return openTOTPLoginModal();
|
||||||
// err may be an Error object or a string
|
}
|
||||||
let msg = "Unknown error";
|
|
||||||
if (err && typeof err === "object") {
|
// ─── 3c) Too many attempts ───
|
||||||
msg = err.error || err.message || msg;
|
if (response.error && response.error.includes("Too many failed login attempts")) {
|
||||||
} else if (typeof err === "string") {
|
showToast(response.error);
|
||||||
msg = err;
|
const btn = document.querySelector("#authForm button[type='submit']");
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
showToast("You can now try logging in again.");
|
||||||
|
}, 30 * 60 * 1000);
|
||||||
}
|
}
|
||||||
showToast(`Login failed: ${msg}`);
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// ─── 3d) Other failures ───
|
||||||
|
showToast("Login failed: " + (response.error || "Unknown error"));
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err.message || err.error || "Unknown error";
|
||||||
|
showToast(`Login failed: ${msg}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.submitLogin = submitLogin;
|
window.submitLogin = submitLogin;
|
||||||
@@ -327,7 +343,7 @@ function closeRemoveUserModal() {
|
|||||||
|
|
||||||
function loadUserList() {
|
function loadUserList() {
|
||||||
// Updated path: from "getUsers.php" to "api/getUsers.php"
|
// Updated path: from "getUsers.php" to "api/getUsers.php"
|
||||||
fetch("api/getUsers.php", { credentials: "include" })
|
fetch("/api/getUsers.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Assuming the endpoint returns an array of users.
|
// Assuming the endpoint returns an array of users.
|
||||||
@@ -368,7 +384,7 @@ function initAuth() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||||
fetch("api/auth/logout.php", {
|
fetch("/api/auth/logout.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "X-CSRF-Token": window.csrfToken }
|
headers: { "X-CSRF-Token": window.csrfToken }
|
||||||
@@ -387,7 +403,7 @@ function initAuth() {
|
|||||||
showToast("Username and password are required!");
|
showToast("Username and password are required!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let url = "api/addUser.php";
|
let url = "/api/addUser.php";
|
||||||
if (window.setupMode) url += "?setup=1";
|
if (window.setupMode) url += "?setup=1";
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -422,7 +438,7 @@ function initAuth() {
|
|||||||
}
|
}
|
||||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
fetch("api/removeUser.php", {
|
fetch("/api/removeUser.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||||
@@ -461,7 +477,7 @@ function initAuth() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = { oldPassword, newPassword, confirmPassword };
|
const data = { oldPassword, newPassword, confirmPassword };
|
||||||
fetch("api/changePassword.php", {
|
fetch("/api/changePassword.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { sendRequest } from './networkUtils.js';
|
|||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||||
import { loadAdminConfigFunc } from './auth.js';
|
import { loadAdminConfigFunc } from './auth.js';
|
||||||
|
|
||||||
const version = "v1.2.0";
|
const version = "v1.2.2"; // Update this version string as needed
|
||||||
// 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>`;
|
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||||
|
|
||||||
let lastLoginData = null;
|
let lastLoginData = null;
|
||||||
@@ -84,7 +83,7 @@ export function openTOTPLoginModal() {
|
|||||||
showToast(t("please_enter_recovery_code"));
|
showToast(t("please_enter_recovery_code"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("api/totp_recover.php", {
|
fetch("/api/totp_recover.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -110,36 +109,47 @@ export function openTOTPLoginModal() {
|
|||||||
// TOTP submission
|
// TOTP submission
|
||||||
const totpInput = document.getElementById("totpLoginInput");
|
const totpInput = document.getElementById("totpLoginInput");
|
||||||
totpInput.focus();
|
totpInput.focus();
|
||||||
totpInput.addEventListener("input", function () {
|
|
||||||
|
totpInput.addEventListener("input", async function () {
|
||||||
const code = this.value.trim();
|
const code = this.value.trim();
|
||||||
if (code.length === 6) {
|
if (code.length !== 6) {
|
||||||
fetch("api/totp_verify.php", {
|
|
||||||
method: "POST",
|
return;
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-CSRF-Token": window.csrfToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ totp_code: code })
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(json => {
|
|
||||||
if (json.status === "ok") {
|
|
||||||
window.location.href = "/index.html";
|
|
||||||
} else {
|
|
||||||
showToast(json.message || t("totp_verification_failed"));
|
|
||||||
this.value = "";
|
|
||||||
totpLoginModal.style.display = "flex";
|
|
||||||
totpInput.focus();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
showToast(t("totp_verification_failed"));
|
|
||||||
this.value = "";
|
|
||||||
totpLoginModal.style.display = "flex";
|
|
||||||
totpInput.focus();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tokenRes = await fetch("/api/auth/token.php", {
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
if (!tokenRes.ok) {
|
||||||
|
showToast(t("totp_verification_failed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.csrfToken = (await tokenRes.json()).csrf_token;
|
||||||
|
|
||||||
|
const res = await fetch("/api/totp_verify.php", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRF-Token": window.csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ totp_code: code })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.status === "ok") {
|
||||||
|
window.location.href = "/index.html";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast(json.message || t("totp_verification_failed"));
|
||||||
|
} else {
|
||||||
|
showToast(t("totp_verification_failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value = "";
|
||||||
|
totpLoginModal.style.display = "flex";
|
||||||
|
this.focus();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Re-open existing modal
|
// Re-open existing modal
|
||||||
@@ -166,105 +176,112 @@ export function openUserPanel() {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 350px !important;
|
max-height: 400px !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;
|
||||||
`;
|
`;
|
||||||
// Retrieve the language setting from local storage, default to English ("en")
|
|
||||||
const savedLanguage = localStorage.getItem("language") || "en";
|
const savedLanguage = localStorage.getItem("language") || "en";
|
||||||
|
|
||||||
if (!userPanelModal) {
|
if (!userPanelModal) {
|
||||||
userPanelModal = document.createElement("div");
|
userPanelModal = document.createElement("div");
|
||||||
userPanelModal.id = "userPanelModal";
|
userPanelModal.id = "userPanelModal";
|
||||||
userPanelModal.style.cssText = `
|
userPanelModal.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: ${overlayBackground};
|
background-color: ${overlayBackground};
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 3000;
|
z-index: 3000;
|
||||||
`;
|
`;
|
||||||
userPanelModal.innerHTML = `
|
userPanelModal.innerHTML = `
|
||||||
<div class="modal-content user-panel-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>${t("user_panel")} (${username})</h3>
|
<h3>${t("user_panel")} (${username})</h3>
|
||||||
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">${t("change_password")}</button>
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">
|
||||||
<legend>${t("totp_settings")}</legend>
|
${t("change_password")}
|
||||||
<div class="form-group">
|
</button>
|
||||||
<label for="userTOTPEnabled">${t("enable_totp")}:</label>
|
|
||||||
<input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" />
|
<fieldset style="margin-bottom: 15px;">
|
||||||
</div>
|
<legend>${t("totp_settings")}</legend>
|
||||||
</fieldset>
|
<div class="form-group">
|
||||||
<fieldset style="margin-bottom: 15px;">
|
<label for="userTOTPEnabled">${t("enable_totp")}:</label>
|
||||||
<legend>${t("language")}</legend>
|
<input type="checkbox" id="userTOTPEnabled" style="vertical-align: middle;" />
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="languageSelector">${t("select_language")}:</label>
|
</fieldset>
|
||||||
<select id="languageSelector">
|
|
||||||
<option value="en">${t("english")}</option>
|
<fieldset style="margin-bottom: 15px;">
|
||||||
<option value="es">${t("spanish")}</option>
|
<legend>${t("language")}</legend>
|
||||||
<option value="fr">${t("french")}</option>
|
<div class="form-group">
|
||||||
<option value="de">${t("german")}</option>
|
<label for="languageSelector">${t("select_language")}:</label>
|
||||||
</select>
|
<select id="languageSelector">
|
||||||
</div>
|
<option value="en">${t("english")}</option>
|
||||||
</fieldset>
|
<option value="es">${t("spanish")}</option>
|
||||||
|
<option value="fr">${t("french")}</option>
|
||||||
|
<option value="de">${t("german")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- New API Docs link -->
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<a href="api.html" target="_blank" class="btn btn-secondary">
|
||||||
|
${t("api_docs") || "API Docs"}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
`;
|
</div>
|
||||||
|
`;
|
||||||
document.body.appendChild(userPanelModal);
|
document.body.appendChild(userPanelModal);
|
||||||
// Close button handler
|
|
||||||
|
// Handlers…
|
||||||
document.getElementById("closeUserPanel").addEventListener("click", () => {
|
document.getElementById("closeUserPanel").addEventListener("click", () => {
|
||||||
userPanelModal.style.display = "none";
|
userPanelModal.style.display = "none";
|
||||||
});
|
});
|
||||||
// Change Password button
|
|
||||||
document.getElementById("openChangePasswordModalBtn").addEventListener("click", () => {
|
document.getElementById("openChangePasswordModalBtn").addEventListener("click", () => {
|
||||||
document.getElementById("changePasswordModal").style.display = "block";
|
document.getElementById("changePasswordModal").style.display = "block";
|
||||||
});
|
});
|
||||||
// TOTP checkbox behavior
|
|
||||||
|
// TOTP checkbox
|
||||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||||
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
||||||
totpCheckbox.addEventListener("change", function () {
|
totpCheckbox.addEventListener("change", function () {
|
||||||
localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false");
|
localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false");
|
||||||
const enabled = this.checked;
|
fetch("/api/updateUserPanel.php", {
|
||||||
fetch("api/updateUserPanel.php", {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify({ totp_enabled: this.checked })
|
||||||
"X-CSRF-Token": window.csrfToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ totp_enabled: enabled })
|
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (!result.success) {
|
if (!result.success) showToast(t("error_updating_totp_setting") + ": " + result.error);
|
||||||
showToast(t("error_updating_totp_setting") + ": " + result.error);
|
else if (this.checked) openTOTPModal();
|
||||||
} else if (enabled) {
|
|
||||||
openTOTPModal();
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => { showToast(t("error_updating_totp_setting")); });
|
.catch(() => showToast(t("error_updating_totp_setting")));
|
||||||
});
|
});
|
||||||
// Language dropdown initialization
|
|
||||||
|
// Language selector
|
||||||
const languageSelector = document.getElementById("languageSelector");
|
const languageSelector = document.getElementById("languageSelector");
|
||||||
languageSelector.value = savedLanguage;
|
languageSelector.value = savedLanguage;
|
||||||
languageSelector.addEventListener("change", function () {
|
languageSelector.addEventListener("change", function () {
|
||||||
const selectedLanguage = this.value;
|
localStorage.setItem("language", this.value);
|
||||||
localStorage.setItem("language", selectedLanguage);
|
setLocale(this.value);
|
||||||
setLocale(selectedLanguage);
|
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If the modal already exists, update its colors
|
// Update colors if already exists
|
||||||
userPanelModal.style.backgroundColor = overlayBackground;
|
userPanelModal.style.backgroundColor = overlayBackground;
|
||||||
const modalContent = userPanelModal.querySelector(".modal-content");
|
const modalContent = userPanelModal.querySelector(".modal-content");
|
||||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
||||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
||||||
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
|
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
|
||||||
}
|
}
|
||||||
|
|
||||||
userPanelModal.style.display = "flex";
|
userPanelModal.style.display = "flex";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,13 +364,24 @@ export function openTOTPModal() {
|
|||||||
closeTOTPModal(true);
|
closeTOTPModal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("confirmTOTPBtn").addEventListener("click", function () {
|
document.getElementById("confirmTOTPBtn").addEventListener("click", async function () {
|
||||||
const code = document.getElementById("totpConfirmInput").value.trim();
|
const code = document.getElementById("totpConfirmInput").value.trim();
|
||||||
if (code.length !== 6) {
|
if (code.length !== 6) {
|
||||||
showToast(t("please_enter_valid_code"));
|
showToast(t("please_enter_valid_code"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("api/totp_verify.php", {
|
|
||||||
|
const tokenRes = await fetch("/api/auth/token.php", {
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
if (!tokenRes.ok) {
|
||||||
|
showToast(t("error_verifying_totp_code"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { csrf_token } = await tokenRes.json();
|
||||||
|
window.csrfToken = csrf_token;
|
||||||
|
|
||||||
|
const verifyRes = await fetch("/api/totp_verify.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -361,36 +389,40 @@ export function openTOTPModal() {
|
|||||||
"X-CSRF-Token": window.csrfToken
|
"X-CSRF-Token": window.csrfToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ totp_code: code })
|
body: JSON.stringify({ totp_code: code })
|
||||||
})
|
});
|
||||||
.then(r => r.json())
|
|
||||||
.then(result => {
|
if (!verifyRes.ok) {
|
||||||
if (result.status === 'ok') {
|
showToast(t("totp_verification_failed"));
|
||||||
showToast(t("totp_enabled_successfully"));
|
return;
|
||||||
// After successful TOTP verification, fetch the recovery code
|
}
|
||||||
fetch("api/totp_saveCode.php", {
|
const result = await verifyRes.json();
|
||||||
method: "POST",
|
if (result.status !== "ok") {
|
||||||
credentials: "include",
|
showToast(result.message || t("totp_verification_failed"));
|
||||||
headers: {
|
return;
|
||||||
"Content-Type": "application/json",
|
}
|
||||||
"X-CSRF-Token": window.csrfToken
|
|
||||||
}
|
showToast(t("totp_enabled_successfully"));
|
||||||
})
|
|
||||||
.then(r => r.json())
|
const saveRes = await fetch("/api/totp_saveCode.php", {
|
||||||
.then(data => {
|
method: "POST",
|
||||||
if (data.status === 'ok' && data.recoveryCode) {
|
credentials: "include",
|
||||||
// Show the recovery code in a secure modal
|
headers: {
|
||||||
showRecoveryCodeModal(data.recoveryCode);
|
"X-CSRF-Token": window.csrfToken
|
||||||
} else {
|
}
|
||||||
showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error")));
|
});
|
||||||
}
|
if (!saveRes.ok) {
|
||||||
})
|
showToast(t("error_generating_recovery_code"));
|
||||||
.catch(() => { showToast(t("error_generating_recovery_code")); });
|
closeTOTPModal(false);
|
||||||
closeTOTPModal(false);
|
return;
|
||||||
} else {
|
}
|
||||||
showToast(t("totp_verification_failed") + ": " + (result.message || t("invalid_code")));
|
const data = await saveRes.json();
|
||||||
}
|
if (data.status === "ok" && data.recoveryCode) {
|
||||||
})
|
showRecoveryCodeModal(data.recoveryCode);
|
||||||
.catch(() => { showToast(t("error_verifying_totp_code")); });
|
} else {
|
||||||
|
showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error")));
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTOTPModal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus the input and attach enter key listener
|
// Focus the input and attach enter key listener
|
||||||
@@ -431,7 +463,7 @@ export function openTOTPModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadTOTPQRCode() {
|
function loadTOTPQRCode() {
|
||||||
fetch("api/totp_setup.php", {
|
fetch("/api/totp_setup.php", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -470,7 +502,7 @@ export function closeTOTPModal(disable = true) {
|
|||||||
localStorage.setItem("userTOTPEnabled", "false");
|
localStorage.setItem("userTOTPEnabled", "false");
|
||||||
}
|
}
|
||||||
// Call endpoint to remove the TOTP secret from the user's record
|
// Call endpoint to remove the TOTP secret from the user's record
|
||||||
fetch("api/totp_disable.php", {
|
fetch("/api/totp_disable.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -549,14 +581,14 @@ function showCustomConfirmModal(message) {
|
|||||||
noBtn.removeEventListener("click", onNo);
|
noBtn.removeEventListener("click", onNo);
|
||||||
modal.style.display = "none";
|
modal.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
yesBtn.addEventListener("click", onYes);
|
yesBtn.addEventListener("click", onYes);
|
||||||
noBtn.addEventListener("click", onNo);
|
noBtn.addEventListener("click", onNo);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openAdminPanel() {
|
export function openAdminPanel() {
|
||||||
fetch("api/admin/getConfig.php", { credentials: "include" })
|
fetch("/api/admin/getConfig.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(config => {
|
.then(config => {
|
||||||
if (config.header_title) {
|
if (config.header_title) {
|
||||||
@@ -688,7 +720,7 @@ export function openAdminPanel() {
|
|||||||
openUserPermissionsModal();
|
openUserPermissionsModal();
|
||||||
});
|
});
|
||||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
||||||
|
|
||||||
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");
|
||||||
@@ -707,7 +739,7 @@ export function openAdminPanel() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
|
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(),
|
||||||
@@ -718,7 +750,7 @@ export function openAdminPanel() {
|
|||||||
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
||||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
||||||
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
||||||
sendRequest("api/admin/updateConfig.php", "POST", {
|
sendRequest("/api/admin/updateConfig.php", "POST", {
|
||||||
header_title: newHeaderTitle,
|
header_title: newHeaderTitle,
|
||||||
oidc: newOIDCConfig,
|
oidc: newOIDCConfig,
|
||||||
disableFormLogin,
|
disableFormLogin,
|
||||||
@@ -739,7 +771,7 @@ export function openAdminPanel() {
|
|||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
closeAdminPanel();
|
closeAdminPanel();
|
||||||
loadAdminConfigFunc();
|
loadAdminConfigFunc();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
|
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
|
||||||
}
|
}
|
||||||
@@ -764,7 +796,7 @@ 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.
|
// Capture initial state after the modal loads.
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
} else {
|
} else {
|
||||||
@@ -891,7 +923,7 @@ export function openUserPermissionsModal() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Send the permissionsData to the server.
|
// Send the permissionsData to the server.
|
||||||
sendRequest("api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
showToast(t("user_permissions_updated_successfully"));
|
showToast(t("user_permissions_updated_successfully"));
|
||||||
@@ -917,11 +949,11 @@ function loadUserPermissionsList() {
|
|||||||
listContainer.innerHTML = "";
|
listContainer.innerHTML = "";
|
||||||
|
|
||||||
// First, fetch the current permissions from the server.
|
// First, fetch the current permissions from the server.
|
||||||
fetch("api/getUserPermissions.php", { credentials: "include" })
|
fetch("/api/getUserPermissions.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(permissionsData => {
|
.then(permissionsData => {
|
||||||
// Then, fetch the list of users.
|
// Then, fetch the list of users.
|
||||||
return fetch("api/getUsers.php", { credentials: "include" })
|
return fetch("/api/getUsers.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(usersData => {
|
.then(usersData => {
|
||||||
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
const confirmDelete = document.getElementById("confirmDeleteFiles");
|
const confirmDelete = document.getElementById("confirmDeleteFiles");
|
||||||
if (confirmDelete) {
|
if (confirmDelete) {
|
||||||
confirmDelete.addEventListener("click", function () {
|
confirmDelete.addEventListener("click", function () {
|
||||||
fetch("api/file/deleteFiles.php", {
|
fetch("/api/file/deleteFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -178,7 +178,7 @@ export function handleExtractZipSelected(e) {
|
|||||||
// Show the progress modal.
|
// Show the progress modal.
|
||||||
document.getElementById("downloadProgressModal").style.display = "block";
|
document.getElementById("downloadProgressModal").style.display = "block";
|
||||||
|
|
||||||
fetch("api/file/extractZip.php", {
|
fetch("/api/file/extractZip.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -245,7 +245,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
console.log("Download confirmed. Showing progress modal.");
|
console.log("Download confirmed. Showing progress modal.");
|
||||||
document.getElementById("downloadProgressModal").style.display = "block";
|
document.getElementById("downloadProgressModal").style.display = "block";
|
||||||
const folder = window.currentFolder || "root";
|
const folder = window.currentFolder || "root";
|
||||||
fetch("api/file/downloadZip.php", {
|
fetch("/api/file/downloadZip.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -309,7 +309,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) {
|
|||||||
if (window.userFolderOnly) {
|
if (window.userFolderOnly) {
|
||||||
const username = localStorage.getItem("username") || "root";
|
const username = localStorage.getItem("username") || "root";
|
||||||
try {
|
try {
|
||||||
const response = await fetch("api/folder/getFolderList.php?restricted=1");
|
const response = await fetch("/api/folder/getFolderList.php?restricted=1");
|
||||||
let folders = await response.json();
|
let folders = await response.json();
|
||||||
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
|
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
|
||||||
folders = folders.map(item => item.folder);
|
folders = folders.map(item => item.folder);
|
||||||
@@ -339,7 +339,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("api/folder/getFolderList.php");
|
const response = await fetch("/api/folder/getFolderList.php");
|
||||||
let folders = await response.json();
|
let folders = await response.json();
|
||||||
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
|
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
|
||||||
folders = folders.map(item => item.folder);
|
folders = folders.map(item => item.folder);
|
||||||
@@ -397,7 +397,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
showToast("Error: Cannot copy files to the same folder.");
|
showToast("Error: Cannot copy files to the same folder.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("api/file/copyFiles.php", {
|
fetch("/api/file/copyFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -448,7 +448,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
showToast("Error: Cannot move files to the same folder.");
|
showToast("Error: Cannot move files to the same folder.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("api/file/moveFiles.php", {
|
fetch("/api/file/moveFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -514,7 +514,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const folderUsed = window.fileFolder;
|
const folderUsed = window.fileFolder;
|
||||||
fetch("api/file/renameFile.php", {
|
fetch("/api/file/renameFile.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export function folderDropHandler(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!dragData || !dragData.fileName) return;
|
if (!dragData || !dragData.fileName) return;
|
||||||
fetch("api/file/moveFiles.php", {
|
fetch("/api/file/moveFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export function saveFile(fileName, folder) {
|
|||||||
content: editor.getValue(),
|
content: editor.getValue(),
|
||||||
folder: folderUsed
|
folder: folderUsed
|
||||||
};
|
};
|
||||||
fetch("api/file/saveFile.php", {
|
fetch("/api/file/saveFile.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -20,9 +20,12 @@ import { openTagModal, openMultiTagModal } from './fileTags.js';
|
|||||||
export let fileData = [];
|
export let fileData = [];
|
||||||
export let sortOrder = { column: "uploaded", ascending: true };
|
export let sortOrder = { column: "uploaded", ascending: true };
|
||||||
|
|
||||||
window.itemsPerPage = window.itemsPerPage || 10;
|
window.itemsPerPage = parseInt(
|
||||||
|
localStorage.getItem('itemsPerPage') || window.itemsPerPage || '10',
|
||||||
|
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";
|
||||||
|
|
||||||
// Global flag for advanced search mode.
|
// Global flag for advanced search mode.
|
||||||
window.advancedSearchEnabled = false;
|
window.advancedSearchEnabled = false;
|
||||||
@@ -193,7 +196,7 @@ export function loadFileList(folderParam) {
|
|||||||
fileListContainer.style.visibility = "hidden";
|
fileListContainer.style.visibility = "hidden";
|
||||||
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
||||||
|
|
||||||
return fetch("api/file/getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime())
|
return fetch("/api/file/getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime())
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
showToast("Session expired. Please log in again.");
|
showToast("Session expired. Please log in again.");
|
||||||
@@ -407,33 +410,89 @@ export function renderGalleryView(folder, container) {
|
|||||||
? "uploads/"
|
? "uploads/"
|
||||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||||
|
|
||||||
// Use the current global column value (default to 3).
|
// pagination settings
|
||||||
const numColumns = window.galleryColumns || 3;
|
const itemsPerPage = window.itemsPerPage;
|
||||||
|
let currentPage = window.currentPage || 1;
|
||||||
|
const totalFiles = filteredFiles.length;
|
||||||
|
const totalPages = Math.ceil(totalFiles / itemsPerPage);
|
||||||
|
if (currentPage > totalPages) {
|
||||||
|
currentPage = totalPages || 1;
|
||||||
|
window.currentPage = currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Insert slider controls ---
|
// --- Top controls: search + pagination + items-per-page ---
|
||||||
const sliderHTML = `
|
let galleryHTML = buildSearchAndPaginationControls({
|
||||||
<div class="gallery-slider" style="margin: 10px; text-align: center;">
|
currentPage,
|
||||||
<label for="galleryColumnsSlider" style="margin-right: 5px;">${t('columns')}:</label>
|
totalPages,
|
||||||
<input type="range" id="galleryColumnsSlider" min="1" max="6" value="${numColumns}" style="vertical-align: middle;">
|
searchTerm: window.currentSearchTerm || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// wire up search input just like table view
|
||||||
|
setTimeout(() => {
|
||||||
|
const searchInput = document.getElementById("searchInput");
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener("input", debounce(() => {
|
||||||
|
window.currentSearchTerm = searchInput.value;
|
||||||
|
window.currentPage = 1;
|
||||||
|
renderGalleryView(folder);
|
||||||
|
// keep caret at end
|
||||||
|
setTimeout(() => {
|
||||||
|
const f = document.getElementById("searchInput");
|
||||||
|
if (f) {
|
||||||
|
f.focus();
|
||||||
|
const len = f.value.length;
|
||||||
|
f.setSelectionRange(len, len);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}, 300));
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// --- Column slider ---
|
||||||
|
const numColumns = window.galleryColumns || 3;
|
||||||
|
galleryHTML += `
|
||||||
|
<div class="gallery-slider" style="margin:10px; text-align:center;">
|
||||||
|
<label for="galleryColumnsSlider" style="margin-right:5px;">
|
||||||
|
${t('columns')}:
|
||||||
|
</label>
|
||||||
|
<input type="range" id="galleryColumnsSlider" min="1" max="6"
|
||||||
|
value="${numColumns}" style="vertical-align:middle;">
|
||||||
<span id="galleryColumnsValue">${numColumns}</span>
|
<span id="galleryColumnsValue">${numColumns}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Set up the grid container using the slider's current value.
|
// --- Start gallery grid ---
|
||||||
const gridStyle = `display: grid; grid-template-columns: repeat(${numColumns}, 1fr); gap: 10px; padding: 10px;`;
|
galleryHTML += `
|
||||||
|
<div class="gallery-container"
|
||||||
|
style="display:grid;
|
||||||
|
grid-template-columns:repeat(${numColumns},1fr);
|
||||||
|
gap:10px;
|
||||||
|
padding:10px;">
|
||||||
|
`;
|
||||||
|
|
||||||
// Build the gallery container HTML including the slider.
|
// slice current page
|
||||||
let galleryHTML = sliderHTML;
|
const startIdx = (currentPage - 1) * itemsPerPage;
|
||||||
galleryHTML += `<div class="gallery-container" style="${gridStyle}">`;
|
const pageFiles = filteredFiles.slice(startIdx, startIdx + itemsPerPage);
|
||||||
filteredFiles.forEach((file) => {
|
|
||||||
|
pageFiles.forEach((file, idx) => {
|
||||||
|
const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);
|
||||||
|
|
||||||
|
// thumbnail
|
||||||
let thumbnail;
|
let thumbnail;
|
||||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
if (/\.(jpe?g|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||||
const cacheKey = folderPath + encodeURIComponent(file.name);
|
const cacheKey = folderPath + encodeURIComponent(file.name);
|
||||||
if (window.imageCache && window.imageCache[cacheKey]) {
|
if (window.imageCache && window.imageCache[cacheKey]) {
|
||||||
thumbnail = `<img src="${window.imageCache[cacheKey]}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: ${getMaxImageHeight()}px; display: block; margin: 0 auto;">`;
|
thumbnail = `<img src="${window.imageCache[cacheKey]}"
|
||||||
|
class="gallery-thumbnail"
|
||||||
|
alt="${escapeHTML(file.name)}"
|
||||||
|
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||||
} else {
|
} else {
|
||||||
const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime();
|
const imageUrl = folderPath + encodeURIComponent(file.name) + "?t=" + Date.now();
|
||||||
thumbnail = `<img src="${imageUrl}" onload="cacheImage(this, '${cacheKey}')" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: ${getMaxImageHeight()}px; display: block; margin: 0 auto;">`;
|
thumbnail = `<img src="${imageUrl}"
|
||||||
|
onload="cacheImage(this,'${cacheKey}')"
|
||||||
|
class="gallery-thumbnail"
|
||||||
|
alt="${escapeHTML(file.name)}"
|
||||||
|
style="max-width:100%; max-height:${getMaxImageHeight()}px; display:block; margin:0 auto;">`;
|
||||||
}
|
}
|
||||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||||
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
|
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
|
||||||
@@ -441,82 +500,127 @@ export function renderGalleryView(folder, container) {
|
|||||||
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
|
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tag badges
|
||||||
let tagBadgesHTML = "";
|
let tagBadgesHTML = "";
|
||||||
if (file.tags && file.tags.length > 0) {
|
if (file.tags && file.tags.length) {
|
||||||
tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`;
|
tagBadgesHTML = `<div class="tag-badges" style="margin-top:4px;">`;
|
||||||
file.tags.forEach(tag => {
|
file.tags.forEach(tag => {
|
||||||
tagBadgesHTML += `<span style="background-color: ${tag.color}; color: #fff; padding: 2px 4px; border-radius: 3px; margin-right: 2px; font-size: 0.8em;">${escapeHTML(tag.name)}</span>`;
|
tagBadgesHTML += `<span style="background-color:${tag.color};
|
||||||
|
color:#fff;
|
||||||
|
padding:2px 4px;
|
||||||
|
border-radius:3px;
|
||||||
|
margin-right:2px;
|
||||||
|
font-size:0.8em;">
|
||||||
|
${escapeHTML(tag.name)}
|
||||||
|
</span>`;
|
||||||
});
|
});
|
||||||
tagBadgesHTML += `</div>`;
|
tagBadgesHTML += `</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// card with checkbox, preview, info, buttons
|
||||||
galleryHTML += `
|
galleryHTML += `
|
||||||
<div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;">
|
<div class="gallery-card"
|
||||||
<div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')">
|
style="position:relative; border:1px solid #ccc; padding:5px; text-align:center;">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="file-checkbox"
|
||||||
|
id="cb-${idSafe}"
|
||||||
|
value="${escapeHTML(file.name)}"
|
||||||
|
style="position:absolute; top:5px; left:5px; z-index:10;">
|
||||||
|
<label for="cb-${idSafe}"
|
||||||
|
style="position:absolute; top:5px; left:5px; width:16px; height:16px;"></label>
|
||||||
|
|
||||||
|
<div class="gallery-preview"
|
||||||
|
style="cursor:pointer;"
|
||||||
|
onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t='+Date.now(), '${file.name}')">
|
||||||
${thumbnail}
|
${thumbnail}
|
||||||
</div>
|
</div>
|
||||||
<div class="gallery-info" style="margin-top: 5px;">
|
|
||||||
<span class="gallery-file-name" style="display: block; white-space: normal; overflow-wrap: break-word; word-wrap: break-word;">${escapeHTML(file.name)}</span>
|
<div class="gallery-info" style="margin-top:5px;">
|
||||||
|
<span class="gallery-file-name"
|
||||||
|
style="display:block; white-space:normal; overflow-wrap:break-word;">
|
||||||
|
${escapeHTML(file.name)}
|
||||||
|
</span>
|
||||||
${tagBadgesHTML}
|
${tagBadgesHTML}
|
||||||
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
|
|
||||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
<div class="button-wrap" style="display:flex; justify-content:center; gap:5px; margin-top:5px;">
|
||||||
onclick="openDownloadModal('${file.name}', '${file.folder || 'root'}')"
|
<button type="button" class="btn btn-sm btn-success download-btn"
|
||||||
title="${t('download')}">
|
onclick="openDownloadModal('${file.name}', '${file.folder || "root"}')"
|
||||||
<i class="material-icons">file_download</i>
|
title="${t('download')}">
|
||||||
|
<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="${t('Edit')}">
|
<button class="btn btn-sm edit-btn"
|
||||||
<i class="material-icons">edit</i>
|
onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||||
</button>
|
title="${t('Edit')}">
|
||||||
` : ""}
|
<i class="material-icons">edit</i>
|
||||||
<button class="btn btn-sm btn-warning rename-btn" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="${t('rename')}">
|
</button>` : ""}
|
||||||
<i class="material-icons">drive_file_rename_outline</i>
|
<button class="btn btn-sm btn-warning rename-btn"
|
||||||
|
onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})'
|
||||||
|
title="${t('rename')}">
|
||||||
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-secondary share-btn" data-file="${escapeHTML(file.name)}" title="${t('share')}">
|
<button class="btn btn-sm btn-secondary share-btn"
|
||||||
<i class="material-icons">share</i>
|
data-file="${escapeHTML(file.name)}"
|
||||||
|
title="${t('share')}">
|
||||||
|
<i class="material-icons">share</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>
|
||||||
|
`;
|
||||||
});
|
});
|
||||||
galleryHTML += "</div>"; // End gallery container.
|
|
||||||
|
|
||||||
|
galleryHTML += `</div>`; // end gallery-container
|
||||||
|
|
||||||
|
// bottom controls
|
||||||
|
galleryHTML += buildBottomControls(itemsPerPage);
|
||||||
|
|
||||||
|
// render
|
||||||
fileListContent.innerHTML = galleryHTML;
|
fileListContent.innerHTML = galleryHTML;
|
||||||
|
|
||||||
// Re-apply slider constraints for the newly rendered slider.
|
// ensure toggle button
|
||||||
updateSliderConstraints();
|
|
||||||
createViewToggleButton();
|
createViewToggleButton();
|
||||||
// Attach share button event listeners.
|
|
||||||
document.querySelectorAll(".share-btn").forEach(btn => {
|
// attach listeners
|
||||||
btn.addEventListener("click", e => {
|
|
||||||
e.stopPropagation();
|
// checkboxes
|
||||||
const fileName = btn.getAttribute("data-file");
|
document.querySelectorAll(".file-checkbox").forEach(cb => {
|
||||||
const file = fileData.find(f => f.name === fileName);
|
cb.addEventListener("change", () => updateFileActionButtons());
|
||||||
if (file) {
|
|
||||||
import('./filePreview.js').then(module => {
|
|
||||||
module.openShareModal(file, folder);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Slider Event Listener ---
|
// slider
|
||||||
const slider = document.getElementById("galleryColumnsSlider");
|
const slider = document.getElementById("galleryColumnsSlider");
|
||||||
if (slider) {
|
if (slider) {
|
||||||
slider.addEventListener("input", function () {
|
slider.addEventListener("input", () => {
|
||||||
const value = this.value;
|
const v = +slider.value;
|
||||||
document.getElementById("galleryColumnsValue").textContent = value;
|
document.getElementById("galleryColumnsValue").textContent = v;
|
||||||
window.galleryColumns = value;
|
window.galleryColumns = v;
|
||||||
const galleryContainer = document.querySelector(".gallery-container");
|
document.querySelector(".gallery-container")
|
||||||
if (galleryContainer) {
|
.style.gridTemplateColumns = `repeat(${v},1fr)`;
|
||||||
galleryContainer.style.gridTemplateColumns = `repeat(${value}, 1fr)`;
|
document.querySelectorAll(".gallery-thumbnail")
|
||||||
}
|
.forEach(img => img.style.maxHeight = getMaxImageHeight() + "px");
|
||||||
const newMaxHeight = getMaxImageHeight();
|
|
||||||
document.querySelectorAll(".gallery-thumbnail").forEach(img => {
|
|
||||||
img.style.maxHeight = newMaxHeight + "px";
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pagination
|
||||||
|
window.changePage = newPage => {
|
||||||
|
window.currentPage = newPage;
|
||||||
|
if (window.viewMode === "gallery") renderGalleryView(folder);
|
||||||
|
else renderFileTable(folder);
|
||||||
|
};
|
||||||
|
|
||||||
|
// items per page
|
||||||
|
window.changeItemsPerPage = cnt => {
|
||||||
|
window.itemsPerPage = +cnt;
|
||||||
|
localStorage.setItem("itemsPerPage", cnt);
|
||||||
|
window.currentPage = 1;
|
||||||
|
if (window.viewMode === "gallery") renderGalleryView(folder);
|
||||||
|
else renderFileTable(folder);
|
||||||
|
};
|
||||||
|
|
||||||
|
// update toolbar buttons
|
||||||
|
updateFileActionButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsive slider constraints based on screen size.
|
// Responsive slider constraints based on screen size.
|
||||||
@@ -530,7 +634,7 @@ function updateSliderConstraints() {
|
|||||||
|
|
||||||
// Set maximum based on screen size.
|
// Set maximum based on screen size.
|
||||||
if (width < 600) { // small devices (phones)
|
if (width < 600) { // small devices (phones)
|
||||||
max = 2;
|
max = 1;
|
||||||
} else if (width < 1024) { // medium devices
|
} else if (width < 1024) { // medium devices
|
||||||
max = 3;
|
max = 3;
|
||||||
} else if (width < 1440) { // between medium and large devices
|
} else if (width < 1440) { // between medium and large devices
|
||||||
@@ -638,12 +742,22 @@ export function canEditFile(fileName) {
|
|||||||
// Expose global functions for pagination and preview.
|
// Expose global functions for pagination and preview.
|
||||||
window.changePage = function (newPage) {
|
window.changePage = function (newPage) {
|
||||||
window.currentPage = newPage;
|
window.currentPage = newPage;
|
||||||
renderFileTable(window.currentFolder);
|
if (window.viewMode === 'gallery') {
|
||||||
|
renderGalleryView(window.currentFolder);
|
||||||
|
} else {
|
||||||
|
renderFileTable(window.currentFolder);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.changeItemsPerPage = function (newCount) {
|
window.changeItemsPerPage = function (newCount) {
|
||||||
window.itemsPerPage = parseInt(newCount);
|
window.itemsPerPage = parseInt(newCount, 10);
|
||||||
|
localStorage.setItem('itemsPerPage', newCount);
|
||||||
window.currentPage = 1;
|
window.currentPage = 1;
|
||||||
renderFileTable(window.currentFolder);
|
if (window.viewMode === 'gallery') {
|
||||||
|
renderGalleryView(window.currentFolder);
|
||||||
|
} else {
|
||||||
|
renderFileTable(window.currentFolder);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// fileListView.js (bottom)
|
// fileListView.js (bottom)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function openShareModal(file, folder) {
|
|||||||
document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
|
document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
|
||||||
const expiration = document.getElementById("shareExpiration").value;
|
const expiration = document.getElementById("shareExpiration").value;
|
||||||
const password = document.getElementById("sharePassword").value;
|
const password = document.getElementById("sharePassword").value;
|
||||||
fetch("api/file/createShareLink.php", {
|
fetch("/api/file/createShareLink.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// filtering the file list by tag, and persisting tag data.
|
// filtering the file list by tag, and persisting tag data.
|
||||||
import { escapeHTML } from './domUtils.js';
|
import { escapeHTML } from './domUtils.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
|
import { renderFileTable, renderGalleryView } from './fileListView.js';
|
||||||
|
|
||||||
export function openTagModal(file) {
|
export function openTagModal(file) {
|
||||||
// Create the modal element.
|
// Create the modal element.
|
||||||
@@ -63,6 +64,11 @@ export function openTagModal(file) {
|
|||||||
updateTagModalDisplay(file);
|
updateTagModalDisplay(file);
|
||||||
updateFileRowTagDisplay(file);
|
updateFileRowTagDisplay(file);
|
||||||
saveFileTags(file);
|
saveFileTags(file);
|
||||||
|
if (window.viewMode === 'gallery') {
|
||||||
|
renderGalleryView(window.currentFolder);
|
||||||
|
} else {
|
||||||
|
renderFileTable(window.currentFolder);
|
||||||
|
}
|
||||||
document.getElementById('tagNameInput').value = '';
|
document.getElementById('tagNameInput').value = '';
|
||||||
updateCustomTagDropdown();
|
updateCustomTagDropdown();
|
||||||
});
|
});
|
||||||
@@ -125,6 +131,11 @@ export function openMultiTagModal(files) {
|
|||||||
saveFileTags(file);
|
saveFileTags(file);
|
||||||
});
|
});
|
||||||
modal.remove();
|
modal.remove();
|
||||||
|
if (window.viewMode === 'gallery') {
|
||||||
|
renderGalleryView(window.currentFolder);
|
||||||
|
} else {
|
||||||
|
renderFileTable(window.currentFolder);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +272,7 @@ function removeGlobalTag(tagName) {
|
|||||||
|
|
||||||
// NEW: Save global tag removal to the server.
|
// NEW: Save global tag removal to the server.
|
||||||
function saveGlobalTagRemoval(tagName) {
|
function saveGlobalTagRemoval(tagName) {
|
||||||
fetch("api/file/saveFileTag.php", {
|
fetch("/api/file/saveFileTag.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -305,7 +316,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("api/file/getFileTag.php", { credentials: "include" })
|
fetch("/api/file/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.
|
||||||
@@ -438,7 +449,7 @@ export function saveFileTags(file, deleteGlobal = false, tagToDelete = null) {
|
|||||||
payload.deleteGlobal = true;
|
payload.deleteGlobal = true;
|
||||||
payload.tagToDelete = tagToDelete;
|
payload.tagToDelete = tagToDelete;
|
||||||
}
|
}
|
||||||
fetch("api/file/saveFileTag.php", {
|
fetch("/api/file/saveFileTag.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ function breadcrumbDropHandler(e) {
|
|||||||
}
|
}
|
||||||
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
||||||
if (filesToMove.length === 0) return;
|
if (filesToMove.length === 0) return;
|
||||||
fetch("api/file/moveFiles.php", {
|
fetch("/api/file/moveFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -202,7 +202,7 @@ function checkUserFolderPermission() {
|
|||||||
window.currentFolder = username;
|
window.currentFolder = username;
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
return fetch("api/getUserPermissions.php", { credentials: "include" })
|
return fetch("/api/getUserPermissions.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(permissionsData => {
|
.then(permissionsData => {
|
||||||
console.log("checkUserFolderPermission: permissionsData =", permissionsData);
|
console.log("checkUserFolderPermission: permissionsData =", permissionsData);
|
||||||
@@ -302,7 +302,7 @@ function folderDropHandler(event) {
|
|||||||
}
|
}
|
||||||
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
||||||
if (filesToMove.length === 0) return;
|
if (filesToMove.length === 0) return;
|
||||||
fetch("api/file/moveFiles.php", {
|
fetch("/api/file/moveFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -353,7 +353,7 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build fetch URL.
|
// Build fetch URL.
|
||||||
let fetchUrl = 'api/folder/getFolderList.php';
|
let fetchUrl = '/api/folder/getFolderList.php';
|
||||||
if (window.userFolderOnly) {
|
if (window.userFolderOnly) {
|
||||||
fetchUrl += '?restricted=1';
|
fetchUrl += '?restricted=1';
|
||||||
}
|
}
|
||||||
@@ -547,7 +547,7 @@ document.getElementById("submitRenameFolder").addEventListener("click", function
|
|||||||
showToast("CSRF token not loaded yet! Please try again.");
|
showToast("CSRF token not loaded yet! Please try again.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("api/folder/renameFolder.php", {
|
fetch("/api/folder/renameFolder.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -592,7 +592,7 @@ attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder");
|
|||||||
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
|
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
|
||||||
const selectedFolder = window.currentFolder || "root";
|
const selectedFolder = window.currentFolder || "root";
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||||
fetch("api/folder/deleteFolder.php", {
|
fetch("/api/folder/deleteFolder.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -639,7 +639,7 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
|
|||||||
fullFolderName = selectedFolder + "/" + folderInput;
|
fullFolderName = selectedFolder + "/" + folderInput;
|
||||||
}
|
}
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||||
fetch("api/folder/createFolder.php", {
|
fetch("/api/folder/createFolder.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function openFolderShareModal(folder) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Post to the createFolderShareLink endpoint.
|
// Post to the createFolderShareLink endpoint.
|
||||||
fetch("api/folder/createShareFolderLink.php", {
|
fetch("/api/folder/createShareFolderLink.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -237,7 +237,8 @@ const translations = {
|
|||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
"items_per_page": "items per page",
|
"items_per_page": "items per page",
|
||||||
"columns":"Columns"
|
"columns":"Columns",
|
||||||
|
"api_docs": "API Docs"
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { t, applyTranslations, setLocale } from './i18n.js';
|
|||||||
|
|
||||||
// Remove the retry logic version and just use loadCsrfToken directly:
|
// Remove the retry logic version and just use loadCsrfToken directly:
|
||||||
function loadCsrfToken() {
|
function loadCsrfToken() {
|
||||||
return fetch('api/auth/token.php', { credentials: 'include' })
|
return fetch('/api/auth/token.php', { credentials: 'include' })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Token fetch failed with status: " + response.status);
|
throw new Error("Token fetch failed with status: " + response.status);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function setupTrashRestoreDelete() {
|
|||||||
showToast(t("no_trash_selected"));
|
showToast(t("no_trash_selected"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("api/file/restoreFiles.php", {
|
fetch("/api/file/restoreFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -109,7 +109,7 @@ export function setupTrashRestoreDelete() {
|
|||||||
showToast(t("trash_empty"));
|
showToast(t("trash_empty"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("api/file/restoreFiles.php", {
|
fetch("/api/file/restoreFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -151,7 +151,7 @@ export function setupTrashRestoreDelete() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showConfirm("Are you sure you want to permanently delete the selected trash items?", () => {
|
showConfirm("Are you sure you want to permanently delete the selected trash items?", () => {
|
||||||
fetch("api/file/deleteTrashFiles.php", {
|
fetch("/api/file/deleteTrashFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -186,7 +186,7 @@ export function setupTrashRestoreDelete() {
|
|||||||
if (deleteAllBtn) {
|
if (deleteAllBtn) {
|
||||||
deleteAllBtn.addEventListener("click", () => {
|
deleteAllBtn.addEventListener("click", () => {
|
||||||
showConfirm("Are you sure you want to permanently delete all trash items? This action cannot be undone.", () => {
|
showConfirm("Are you sure you want to permanently delete all trash items? This action cannot be undone.", () => {
|
||||||
fetch("api/file/deleteTrashFiles.php", {
|
fetch("/api/file/deleteTrashFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -234,7 +234,7 @@ export function setupTrashRestoreDelete() {
|
|||||||
* Loads trash items from the server and updates the restore modal list.
|
* Loads trash items from the server and updates the restore modal list.
|
||||||
*/
|
*/
|
||||||
export function loadTrashItems() {
|
export function loadTrashItems() {
|
||||||
fetch("api/file/getTrashItems.php", { credentials: "include" })
|
fetch("/api/file/getTrashItems.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(trashItems => {
|
.then(trashItems => {
|
||||||
const listContainer = document.getElementById("restoreFilesList");
|
const listContainer = document.getElementById("restoreFilesList");
|
||||||
@@ -271,7 +271,7 @@ export function loadTrashItems() {
|
|||||||
* Automatically purges (permanently deletes) trash items older than 3 days.
|
* Automatically purges (permanently deletes) trash items older than 3 days.
|
||||||
*/
|
*/
|
||||||
function autoPurgeOldTrash() {
|
function autoPurgeOldTrash() {
|
||||||
fetch("api/file/getTrashItems.php", { credentials: "include" })
|
fetch("/api/file/getTrashItems.php", { credentials: "include" })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(trashItems => {
|
.then(trashItems => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -279,7 +279,7 @@ function autoPurgeOldTrash() {
|
|||||||
const oldItems = trashItems.filter(item => (now - (item.trashedAt * 1000)) > threeDays);
|
const oldItems = trashItems.filter(item => (now - (item.trashedAt * 1000)) > threeDays);
|
||||||
if (oldItems.length > 0) {
|
if (oldItems.length > 0) {
|
||||||
const files = oldItems.map(item => item.trashName);
|
const files = oldItems.map(item => item.trashName);
|
||||||
fetch("api/file/deleteTrashFiles.php", {
|
fetch("/api/file/deleteTrashFiles.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ function removeChunkFolderRepeatedly(identifier, csrfToken, maxAttempts = 3, int
|
|||||||
// Prefix with "resumable_" to match your PHP regex.
|
// Prefix with "resumable_" to match your PHP regex.
|
||||||
params.append('folder', 'resumable_' + identifier);
|
params.append('folder', 'resumable_' + identifier);
|
||||||
params.append('csrf_token', csrfToken);
|
params.append('csrf_token', csrfToken);
|
||||||
fetch('api/upload/removeChunks.php', {
|
fetch('/api/upload/removeChunks.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -664,7 +664,7 @@ function submitFiles(allFiles) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.open("POST", "api/upload/upload.php", true);
|
xhr.open("POST", "/api/upload/upload.php", true);
|
||||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
});
|
});
|
||||||
|
|||||||
2599
public/openapi.json
Normal file
2599
public/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
61
public/webdav.php
Normal file
61
public/webdav.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
// public/webdav.php
|
||||||
|
|
||||||
|
if (
|
||||||
|
empty($_SERVER['PHP_AUTH_USER'])
|
||||||
|
&& !empty($_SERVER['HTTP_AUTHORIZATION'])
|
||||||
|
&& preg_match('#Basic\s+(.*)$#i', $_SERVER['HTTP_AUTHORIZATION'], $m)
|
||||||
|
) {
|
||||||
|
[$u, $p] = explode(':', base64_decode($m[1]), 2) + ['', ''];
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = $u;
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = $p;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config/config.php'; // UPLOAD_DIR, META_DIR, DATE_TIME_FORMAT
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php'; // Composer & SabreDAV
|
||||||
|
require_once __DIR__ . '/../src/models/AuthModel.php'; // AuthModel::authenticate(), getUserRole(), loadFolderPermission()
|
||||||
|
|
||||||
|
// ─── 3) Load your WebDAV directory implementation ──────────────────────────
|
||||||
|
require_once __DIR__ . '/../src/webdav/FileRiseDirectory.php';
|
||||||
|
use Sabre\DAV\Server;
|
||||||
|
use Sabre\DAV\Auth\Backend\BasicCallBack;
|
||||||
|
use Sabre\DAV\Auth\Plugin as AuthPlugin;
|
||||||
|
use Sabre\DAV\Browser\Plugin as BrowserPlugin;
|
||||||
|
use Sabre\DAV\Locks\Plugin as LocksPlugin;
|
||||||
|
use Sabre\DAV\Locks\Backend\File as LocksFileBackend;
|
||||||
|
use FileRise\WebDAV\FileRiseDirectory;
|
||||||
|
|
||||||
|
$authBackend = new BasicCallBack(function(string $user, string $pass) {
|
||||||
|
return \AuthModel::authenticate($user, $pass) !== false;
|
||||||
|
});
|
||||||
|
$authPlugin = new AuthPlugin($authBackend, 'FileRise');
|
||||||
|
|
||||||
|
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
|
||||||
|
$isAdmin = (\AuthModel::getUserRole($user) === '1');
|
||||||
|
$folderOnly = (bool)\AuthModel::loadFolderPermission($user);
|
||||||
|
|
||||||
|
if ($isAdmin || !$folderOnly) {
|
||||||
|
// admins or unrestricted users see the full /uploads
|
||||||
|
$rootPath = rtrim(UPLOAD_DIR, '/\\');
|
||||||
|
} else {
|
||||||
|
// folder‑only users see only /uploads/{username}
|
||||||
|
$rootPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $user;
|
||||||
|
if (!is_dir($rootPath)) {
|
||||||
|
mkdir($rootPath, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$server = new Server([
|
||||||
|
new FileRiseDirectory($rootPath, $user, $folderOnly),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$server->addPlugin($authPlugin);
|
||||||
|
//$server->addPlugin(new BrowserPlugin()); // optional HTML browser UI
|
||||||
|
$server->addPlugin(
|
||||||
|
new LocksPlugin(
|
||||||
|
new LocksFileBackend(sys_get_temp_dir() . '/sabre-locksdb')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$server->setBaseUri('/webdav.php/');
|
||||||
|
$server->exec();
|
||||||
@@ -84,7 +84,7 @@ class AuthController
|
|||||||
if ($totpCode && isset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'])) {
|
if ($totpCode && isset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'])) {
|
||||||
$username = $_SESSION['pending_login_user'];
|
$username = $_SESSION['pending_login_user'];
|
||||||
$secret = $_SESSION['pending_login_secret'];
|
$secret = $_SESSION['pending_login_secret'];
|
||||||
|
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
||||||
$tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider(), 'FileRise', 6, 30, Algorithm::Sha1);
|
$tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider(), 'FileRise', 6, 30, Algorithm::Sha1);
|
||||||
if (! $tfa->verifyCode($secret, $totpCode)) {
|
if (! $tfa->verifyCode($secret, $totpCode)) {
|
||||||
echo json_encode(['error' => 'Invalid TOTP code']);
|
echo json_encode(['error' => 'Invalid TOTP code']);
|
||||||
@@ -203,6 +203,7 @@ class AuthController
|
|||||||
if (! empty($user['totp_secret'])) {
|
if (! empty($user['totp_secret'])) {
|
||||||
$_SESSION['pending_login_user'] = $username;
|
$_SESSION['pending_login_user'] = $username;
|
||||||
$_SESSION['pending_login_secret'] = $user['totp_secret'];
|
$_SESSION['pending_login_secret'] = $user['totp_secret'];
|
||||||
|
$_SESSION['pending_login_remember_me'] = $rememberMe;
|
||||||
echo json_encode(['totp_required' => true]);
|
echo json_encode(['totp_required' => true]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -237,22 +238,39 @@ class AuthController
|
|||||||
$token = bin2hex(random_bytes(32));
|
$token = bin2hex(random_bytes(32));
|
||||||
$expiry = time() + 30 * 24 * 60 * 60;
|
$expiry = time() + 30 * 24 * 60 * 60;
|
||||||
$all = [];
|
$all = [];
|
||||||
|
|
||||||
if (file_exists($tokFile)) {
|
if (file_exists($tokFile)) {
|
||||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||||
$all = json_decode($dec, true) ?: [];
|
$all = json_decode($dec, true) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$all[$token] = [
|
$all[$token] = [
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'expiry' => $expiry,
|
'expiry' => $expiry,
|
||||||
'isAdmin' => $_SESSION['isAdmin']
|
'isAdmin' => $_SESSION['isAdmin']
|
||||||
];
|
];
|
||||||
|
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
$tokFile,
|
$tokFile,
|
||||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||||
LOCK_EX
|
LOCK_EX
|
||||||
);
|
);
|
||||||
$secure = (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
|
||||||
|
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
|
|
||||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||||
|
|
||||||
|
setcookie(
|
||||||
|
session_name(),
|
||||||
|
session_id(),
|
||||||
|
$expiry,
|
||||||
|
'/',
|
||||||
|
'',
|
||||||
|
$secure,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
session_regenerate_id(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
|
|||||||
@@ -450,56 +450,57 @@ class FileController {
|
|||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// --- CSRF Protection ---
|
// --- CSRF Protection ---
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
$receivedToken = $headersArr['x-csrf-token'] ?? '';
|
||||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user is authenticated.
|
// --- Authentication Check ---
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is allowed to save files (not read-only).
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
// --- Read‑only check ---
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if ($username && !empty($userPermissions['readOnly'])) {
|
||||||
echo json_encode(["error" => "Read-only users are not allowed to save files."]);
|
echo json_encode(["error" => "Read-only users are not allowed to save files."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get JSON input.
|
// --- Input parsing ---
|
||||||
$data = json_decode(file_get_contents("php://input"), true);
|
$data = json_decode(file_get_contents("php://input"), true);
|
||||||
|
if (empty($data) || !isset($data["fileName"], $data["content"])) {
|
||||||
if (!$data) {
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "No data received"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($data["fileName"]) || !isset($data["content"])) {
|
|
||||||
echo json_encode(["error" => "Invalid request data", "received" => $data]);
|
echo json_encode(["error" => "Invalid request data", "received" => $data]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fileName = basename($data["fileName"]);
|
$fileName = basename($data["fileName"]);
|
||||||
// Determine the folder. Default to "root" if not provided.
|
$folder = isset($data["folder"]) ? trim($data["folder"]) : "root";
|
||||||
$folder = isset($data["folder"]) ? trim($data["folder"]) : "root";
|
|
||||||
|
|
||||||
// Validate folder if not root.
|
// --- Folder validation ---
|
||||||
if (strtolower($folder) !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if (strtolower($folder) !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
echo json_encode(["error" => "Invalid folder name"]);
|
echo json_encode(["error" => "Invalid folder name"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$folder = trim($folder, "/\\ ");
|
$folder = trim($folder, "/\\ ");
|
||||||
|
|
||||||
// Delegate to the model.
|
// --- Delegate to model, passing the uploader ---
|
||||||
$result = FileModel::saveFile($folder, $fileName, $data["content"]);
|
// Make sure FileModel::saveFile signature is:
|
||||||
|
// saveFile(string $folder, string $fileName, $content, ?string $uploader = null)
|
||||||
|
$result = FileModel::saveFile(
|
||||||
|
$folder,
|
||||||
|
$fileName,
|
||||||
|
$data["content"],
|
||||||
|
$username // ← pass the real uploader here
|
||||||
|
);
|
||||||
|
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -847,104 +847,147 @@ class UserController
|
|||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function verifyTOTP()
|
public function verifyTOTP()
|
||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
// Set CSP headers if desired:
|
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';");
|
|
||||||
|
// Rate‑limit
|
||||||
// Rate‑limit: initialize totp_failures if not set.
|
if (!isset($_SESSION['totp_failures'])) {
|
||||||
if (!isset($_SESSION['totp_failures'])) {
|
$_SESSION['totp_failures'] = 0;
|
||||||
$_SESSION['totp_failures'] = 0;
|
}
|
||||||
}
|
if ($_SESSION['totp_failures'] >= 5) {
|
||||||
if ($_SESSION['totp_failures'] >= 5) {
|
http_response_code(429);
|
||||||
http_response_code(429);
|
echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
|
exit;
|
||||||
exit;
|
}
|
||||||
}
|
|
||||||
|
// Must be authenticated OR pending login
|
||||||
// Must be authenticated OR have a pending login.
|
if (!((!empty($_SESSION['authenticated'])) || isset($_SESSION['pending_login_user']))) {
|
||||||
if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) {
|
http_response_code(403);
|
||||||
http_response_code(403);
|
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
exit;
|
||||||
exit;
|
}
|
||||||
}
|
|
||||||
|
// CSRF check
|
||||||
// CSRF check.
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
$csrfHeader = $headersArr['x-csrf-token'] ?? '';
|
||||||
$csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||||
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
http_response_code(403);
|
||||||
http_response_code(403);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
|
exit;
|
||||||
exit;
|
}
|
||||||
}
|
|
||||||
|
// Parse and validate input
|
||||||
// Parse input.
|
$inputData = json_decode(file_get_contents("php://input"), true);
|
||||||
$inputData = json_decode(file_get_contents("php://input"), true);
|
$code = trim($inputData['totp_code'] ?? '');
|
||||||
$code = trim($inputData['totp_code'] ?? '');
|
if (!preg_match('/^\d{6}$/', $code)) {
|
||||||
if (!preg_match('/^\d{6}$/', $code)) {
|
http_response_code(400);
|
||||||
http_response_code(400);
|
echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
|
exit;
|
||||||
exit;
|
}
|
||||||
}
|
|
||||||
|
// TFA helper
|
||||||
// Create TFA object.
|
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
|
||||||
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
|
'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
|
||||||
'FileRise',
|
);
|
||||||
6,
|
|
||||||
30,
|
// Pending‑login flow (first password step passed)
|
||||||
\RobThree\Auth\Algorithm::Sha1
|
if (isset($_SESSION['pending_login_user'])) {
|
||||||
);
|
$username = $_SESSION['pending_login_user'];
|
||||||
|
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
||||||
// Check if we are in pending login flow.
|
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
||||||
if (isset($_SESSION['pending_login_user'])) {
|
|
||||||
$username = $_SESSION['pending_login_user'];
|
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
||||||
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
$_SESSION['totp_failures']++;
|
||||||
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
http_response_code(400);
|
||||||
$_SESSION['totp_failures']++;
|
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||||
http_response_code(400);
|
exit;
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
}
|
||||||
exit;
|
|
||||||
}
|
// === Issue “remember me” token if requested ===
|
||||||
// Successful pending login: finalize login.
|
if ($rememberMe) {
|
||||||
session_regenerate_id(true);
|
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
$_SESSION['authenticated'] = true;
|
$token = bin2hex(random_bytes(32));
|
||||||
$_SESSION['username'] = $username;
|
$expiry = time() + 30 * 24 * 60 * 60;
|
||||||
// Set isAdmin based on user role.
|
$all = [];
|
||||||
$_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1");
|
|
||||||
// Load additional permissions (e.g., folderOnly) as needed.
|
if (file_exists($tokFile)) {
|
||||||
$_SESSION['folderOnly'] = loadUserPermissions($username);
|
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||||
unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'], $_SESSION['totp_failures']);
|
$all = json_decode($dec, true) ?: [];
|
||||||
echo json_encode(['status' => 'ok', 'message' => 'Login successful']);
|
}
|
||||||
exit;
|
$all[$token] = [
|
||||||
}
|
'username' => $username,
|
||||||
|
'expiry' => $expiry,
|
||||||
// Otherwise, we are in setup/verification flow.
|
'isAdmin' => $_SESSION['isAdmin']
|
||||||
$username = $_SESSION['username'] ?? '';
|
];
|
||||||
if (!$username) {
|
file_put_contents(
|
||||||
http_response_code(400);
|
$tokFile,
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||||
exit;
|
LOCK_EX
|
||||||
}
|
);
|
||||||
|
|
||||||
// Retrieve the user's TOTP secret from the model.
|
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
$totpSecret = userModel::getTOTPSecret($username);
|
|
||||||
if (!$totpSecret) {
|
// Persistent cookie
|
||||||
http_response_code(500);
|
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
|
||||||
exit;
|
// Re‑issue PHP session cookie
|
||||||
}
|
setcookie(
|
||||||
|
session_name(),
|
||||||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
session_id(),
|
||||||
$_SESSION['totp_failures']++;
|
$expiry,
|
||||||
http_response_code(400);
|
'/',
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
'',
|
||||||
exit;
|
$secure,
|
||||||
}
|
true
|
||||||
|
);
|
||||||
// Successful verification.
|
}
|
||||||
unset($_SESSION['totp_failures']);
|
|
||||||
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
// Finalize login
|
||||||
}
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['authenticated'] = true;
|
||||||
|
$_SESSION['username'] = $username;
|
||||||
|
$_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1");
|
||||||
|
$_SESSION['folderOnly'] = loadUserPermissions($username);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
unset(
|
||||||
|
$_SESSION['pending_login_user'],
|
||||||
|
$_SESSION['pending_login_secret'],
|
||||||
|
$_SESSION['pending_login_remember_me'],
|
||||||
|
$_SESSION['totp_failures']
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'ok', 'message' => 'Login successful']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup/verification flow (not pending)
|
||||||
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
if (!$username) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totpSecret = userModel::getTOTPSecret($username);
|
||||||
|
if (!$totpSecret) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||||
|
$_SESSION['totp_failures']++;
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successful setup/verification
|
||||||
|
unset($_SESSION['totp_failures']);
|
||||||
|
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,88 +383,95 @@ class FileModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Saves file content to disk and updates folder metadata.
|
* Save a file’s contents *and* record its metadata, including who uploaded it.
|
||||||
*
|
*
|
||||||
* @param string $folder The target folder where the file is to be saved (e.g. "root" or a subfolder).
|
* @param string $folder Folder key (e.g. "root" or "invoices/2025")
|
||||||
* @param string $fileName The name of the file.
|
* @param string $fileName Basename of the file
|
||||||
* @param string $content The file content.
|
* @param resource|string $content File contents (stream or string)
|
||||||
* @return array Returns an associative array with either a "success" key or an "error" key.
|
* @param string|null $uploader Username of uploader (if null, falls back to session)
|
||||||
*/
|
* @return array ["success"=>"…"] or ["error"=>"…"]
|
||||||
public static function saveFile($folder, $fileName, $content) {
|
*/
|
||||||
// Sanitize and determine the folder name.
|
public static function saveFile(string $folder, string $fileName, $content, ?string $uploader = null): array {
|
||||||
$folder = trim($folder) ?: 'root';
|
// Sanitize inputs
|
||||||
$fileName = basename(trim($fileName));
|
$folder = trim($folder) ?: 'root';
|
||||||
|
$fileName = basename(trim($fileName));
|
||||||
|
|
||||||
// Validate folder: if not "root", must match REGEX_FOLDER_NAME.
|
// Validate folder name
|
||||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
return ["error" => "Invalid folder name"];
|
return ["error" => "Invalid folder name"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine target directory
|
||||||
|
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||||
|
$targetDir = strtolower($folder) === 'root'
|
||||||
|
? $baseDir . DIRECTORY_SEPARATOR
|
||||||
|
: $baseDir . DIRECTORY_SEPARATOR . trim($folder, "/\\ ") . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (strpos(realpath($targetDir), realpath($baseDir)) !== 0) {
|
||||||
|
return ["error" => "Invalid folder path"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true)) {
|
||||||
|
return ["error" => "Failed to create destination folder"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $targetDir . $fileName;
|
||||||
|
|
||||||
|
// ——— STREAM TO DISK ———
|
||||||
|
if (is_resource($content)) {
|
||||||
|
$out = fopen($filePath, 'wb');
|
||||||
|
if ($out === false) {
|
||||||
|
return ["error" => "Unable to open file for writing"];
|
||||||
}
|
}
|
||||||
|
stream_copy_to_stream($content, $out);
|
||||||
// Determine base upload directory.
|
fclose($out);
|
||||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
} else {
|
||||||
if (strtolower($folder) === 'root' || $folder === "") {
|
if (file_put_contents($filePath, (string)$content) === false) {
|
||||||
$targetDir = $baseDir . DIRECTORY_SEPARATOR;
|
|
||||||
} else {
|
|
||||||
$targetDir = $baseDir . DIRECTORY_SEPARATOR . trim($folder, "/\\ ") . DIRECTORY_SEPARATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// (Optional security check to ensure targetDir is within baseDir.)
|
|
||||||
if (strpos(realpath($targetDir), realpath($baseDir)) !== 0) {
|
|
||||||
return ["error" => "Invalid folder path"];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create target directory if it doesn't exist.
|
|
||||||
if (!is_dir($targetDir)) {
|
|
||||||
if (!mkdir($targetDir, 0775, true)) {
|
|
||||||
return ["error" => "Failed to create destination folder"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$filePath = $targetDir . $fileName;
|
|
||||||
// Attempt to save the file.
|
|
||||||
if (file_put_contents($filePath, $content) === false) {
|
|
||||||
return ["error" => "Error saving file"];
|
return ["error" => "Error saving file"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update metadata.
|
|
||||||
// Build metadata file path for the folder.
|
|
||||||
$metadataKey = (strtolower($folder) === "root" || $folder === "") ? "root" : $folder;
|
|
||||||
$metadataFileName = str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json';
|
|
||||||
$metadataFilePath = META_DIR . $metadataFileName;
|
|
||||||
|
|
||||||
if (file_exists($metadataFilePath)) {
|
|
||||||
$metadata = json_decode(file_get_contents($metadataFilePath), true);
|
|
||||||
} else {
|
|
||||||
$metadata = [];
|
|
||||||
}
|
|
||||||
if (!is_array($metadata)) {
|
|
||||||
$metadata = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentTime = date(DATE_TIME_FORMAT);
|
|
||||||
$uploader = $_SESSION['username'] ?? "Unknown";
|
|
||||||
|
|
||||||
// Update metadata for the file. If already exists, update its "modified" timestamp.
|
|
||||||
if (isset($metadata[$fileName])) {
|
|
||||||
$metadata[$fileName]['modified'] = $currentTime;
|
|
||||||
$metadata[$fileName]['uploader'] = $uploader; // optional: update uploader if desired.
|
|
||||||
} else {
|
|
||||||
$metadata[$fileName] = [
|
|
||||||
"uploaded" => $currentTime,
|
|
||||||
"modified" => $currentTime,
|
|
||||||
"uploader" => $uploader
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write updated metadata.
|
|
||||||
if (file_put_contents($metadataFilePath, json_encode($metadata, JSON_PRETTY_PRINT)) === false) {
|
|
||||||
return ["error" => "Failed to update metadata"];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ["success" => "File saved successfully"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ——— UPDATE METADATA ———
|
||||||
|
$metadataKey = strtolower($folder) === "root" ? "root" : $folder;
|
||||||
|
$metadataFileName = str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json';
|
||||||
|
$metadataFilePath = META_DIR . $metadataFileName;
|
||||||
|
|
||||||
|
// Load existing metadata
|
||||||
|
$metadata = [];
|
||||||
|
if (file_exists($metadataFilePath)) {
|
||||||
|
$existing = @json_decode(file_get_contents($metadataFilePath), true);
|
||||||
|
if (is_array($existing)) {
|
||||||
|
$metadata = $existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentTime = date(DATE_TIME_FORMAT);
|
||||||
|
// Use passed-in uploader, or fall back to session
|
||||||
|
if ($uploader === null) {
|
||||||
|
$uploader = $_SESSION['username'] ?? "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($metadata[$fileName])) {
|
||||||
|
$metadata[$fileName]['modified'] = $currentTime;
|
||||||
|
$metadata[$fileName]['uploader'] = $uploader;
|
||||||
|
} else {
|
||||||
|
$metadata[$fileName] = [
|
||||||
|
"uploaded" => $currentTime,
|
||||||
|
"modified" => $currentTime,
|
||||||
|
"uploader" => $uploader
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($metadataFilePath, json_encode($metadata, JSON_PRETTY_PRINT)) === false) {
|
||||||
|
return ["error" => "Failed to update metadata"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ["success" => "File saved successfully"];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and retrieves information needed to download a file.
|
* Validates and retrieves information needed to download a file.
|
||||||
*
|
*
|
||||||
|
|||||||
16
src/webdav/CurrentUser.php
Normal file
16
src/webdav/CurrentUser.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
// src/webdav/CurrentUser.php
|
||||||
|
namespace FileRise\WebDAV;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton holder for the current WebDAV username.
|
||||||
|
*/
|
||||||
|
class CurrentUser {
|
||||||
|
private static string $user = 'Unknown';
|
||||||
|
public static function set(string $u): void {
|
||||||
|
self::$user = $u;
|
||||||
|
}
|
||||||
|
public static function get(): string {
|
||||||
|
return self::$user;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/webdav/FileRiseDirectory.php
Normal file
110
src/webdav/FileRiseDirectory.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
namespace FileRise\WebDAV;
|
||||||
|
|
||||||
|
// Bootstrap constants and models
|
||||||
|
require_once __DIR__ . '/../../config/config.php'; // UPLOAD_DIR, META_DIR, DATE_TIME_FORMAT
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php'; // SabreDAV
|
||||||
|
require_once __DIR__ . '/../../src/models/FolderModel.php';
|
||||||
|
require_once __DIR__ . '/../../src/models/FileModel.php';
|
||||||
|
require_once __DIR__ . '/FileRiseFile.php';
|
||||||
|
|
||||||
|
use Sabre\DAV\ICollection;
|
||||||
|
use Sabre\DAV\INode;
|
||||||
|
use Sabre\DAV\Exception\NotFound;
|
||||||
|
use Sabre\DAV\Exception\Forbidden;
|
||||||
|
use FileRise\WebDAV\FileRiseFile;
|
||||||
|
use FolderModel;
|
||||||
|
use FileModel;
|
||||||
|
|
||||||
|
class FileRiseDirectory implements ICollection, INode {
|
||||||
|
private string $path;
|
||||||
|
private string $user;
|
||||||
|
private bool $folderOnly;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path Absolute filesystem path (no trailing slash)
|
||||||
|
* @param string $user Authenticated username
|
||||||
|
* @param bool $folderOnly If true, non‑admins only see $path/{user}
|
||||||
|
*/
|
||||||
|
public function __construct(string $path, string $user, bool $folderOnly) {
|
||||||
|
$this->path = rtrim($path, '/\\');
|
||||||
|
$this->user = $user;
|
||||||
|
$this->folderOnly = $folderOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── INode ───────────────────────────────────────────
|
||||||
|
|
||||||
|
public function getName(): string {
|
||||||
|
return basename($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastModified(): int {
|
||||||
|
return filemtime($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(): void {
|
||||||
|
throw new Forbidden('Cannot delete this node');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName($name): void {
|
||||||
|
throw new Forbidden('Renaming not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ICollection ────────────────────────────────────
|
||||||
|
|
||||||
|
public function getChildren(): array {
|
||||||
|
$nodes = [];
|
||||||
|
foreach (new \DirectoryIterator($this->path) as $item) {
|
||||||
|
if ($item->isDot()) continue;
|
||||||
|
$full = $item->getPathname();
|
||||||
|
if ($item->isDir()) {
|
||||||
|
$nodes[] = new self($full, $this->user, $this->folderOnly);
|
||||||
|
} else {
|
||||||
|
$nodes[] = new FileRiseFile($full, $this->user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Apply folder‑only at the top level
|
||||||
|
if (
|
||||||
|
$this->folderOnly
|
||||||
|
&& realpath($this->path) === realpath(rtrim(UPLOAD_DIR,'/\\'))
|
||||||
|
) {
|
||||||
|
$nodes = array_filter($nodes, fn(INode $n)=> $n->getName() === $this->user);
|
||||||
|
}
|
||||||
|
return array_values($nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function childExists($name): bool {
|
||||||
|
return file_exists($this->path . DIRECTORY_SEPARATOR . $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChild($name): INode {
|
||||||
|
$full = $this->path . DIRECTORY_SEPARATOR . $name;
|
||||||
|
if (!file_exists($full)) throw new NotFound("Not found: $name");
|
||||||
|
return is_dir($full)
|
||||||
|
? new self($full, $this->user, $this->folderOnly)
|
||||||
|
: new FileRiseFile($full, $this->user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createFile($name, $data = null): INode {
|
||||||
|
$full = $this->path . DIRECTORY_SEPARATOR . $name;
|
||||||
|
$content = is_resource($data) ? stream_get_contents($data) : (string)$data;
|
||||||
|
|
||||||
|
// Compute folder‑key relative to UPLOAD_DIR
|
||||||
|
$rel = substr($full, strlen(rtrim(UPLOAD_DIR,'/\\'))+1);
|
||||||
|
$parts = explode('/', str_replace('\\','/',$rel));
|
||||||
|
$filename = array_pop($parts);
|
||||||
|
$folder = empty($parts) ? 'root' : implode('/', $parts);
|
||||||
|
|
||||||
|
FileModel::saveFile($folder, $filename, $content, $this->user);
|
||||||
|
return new FileRiseFile($full, $this->user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDirectory($name): INode {
|
||||||
|
$full = $this->path . DIRECTORY_SEPARATOR . $name;
|
||||||
|
$rel = substr($full, strlen(rtrim(UPLOAD_DIR,'/\\'))+1);
|
||||||
|
$parent = dirname(str_replace('\\','/',$rel));
|
||||||
|
if ($parent === '.' || $parent === '/') $parent = '';
|
||||||
|
FolderModel::createFolder($name, $parent, $this->user);
|
||||||
|
return new self($full, $this->user, $this->folderOnly);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/webdav/FileRiseFile.php
Normal file
115
src/webdav/FileRiseFile.php
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
// src/webdav/FileRiseFile.php
|
||||||
|
|
||||||
|
namespace FileRise\WebDAV;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../../src/models/FileModel.php';
|
||||||
|
|
||||||
|
use Sabre\DAV\IFile;
|
||||||
|
use Sabre\DAV\INode;
|
||||||
|
use Sabre\DAV\Exception\Forbidden;
|
||||||
|
use FileModel;
|
||||||
|
|
||||||
|
class FileRiseFile implements IFile, INode {
|
||||||
|
private string $path;
|
||||||
|
|
||||||
|
public function __construct(string $path) {
|
||||||
|
$this->path = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── INode ───────────────────────────────────────────
|
||||||
|
|
||||||
|
public function getName(): string {
|
||||||
|
return basename($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastModified(): int {
|
||||||
|
return filemtime($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(): void {
|
||||||
|
$base = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||||
|
$rel = substr($this->path, strlen($base));
|
||||||
|
$parts = explode(DIRECTORY_SEPARATOR, $rel);
|
||||||
|
$file = array_pop($parts);
|
||||||
|
$folder = empty($parts) ? 'root' : $parts[0];
|
||||||
|
FileModel::deleteFiles($folder, [$file]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName($newName): void {
|
||||||
|
throw new Forbidden('Renaming files not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── IFile ───────────────────────────────────────────
|
||||||
|
|
||||||
|
public function get() {
|
||||||
|
return fopen($this->path, 'rb');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put($data): ?string {
|
||||||
|
// 1) Save incoming data
|
||||||
|
file_put_contents(
|
||||||
|
$this->path,
|
||||||
|
is_resource($data) ? stream_get_contents($data) : (string)$data
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2) Update metadata with CurrentUser
|
||||||
|
$this->updateMetadata();
|
||||||
|
|
||||||
|
// 3) Flush to client fast
|
||||||
|
if (function_exists('fastcgi_finish_request')) {
|
||||||
|
fastcgi_finish_request();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // no ETag
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSize(): int {
|
||||||
|
return filesize($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getETag(): string {
|
||||||
|
return '"' . md5($this->getLastModified() . $this->getSize()) . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContentType(): ?string {
|
||||||
|
return mime_content_type($this->path) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Metadata helper ───────────────────────────────────
|
||||||
|
|
||||||
|
private function updateMetadata(): void {
|
||||||
|
$base = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||||
|
$rel = substr($this->path, strlen($base));
|
||||||
|
$parts = explode(DIRECTORY_SEPARATOR, $rel);
|
||||||
|
$fileName = array_pop($parts);
|
||||||
|
$folder = empty($parts) ? 'root' : $parts[0];
|
||||||
|
|
||||||
|
$metaFile = META_DIR
|
||||||
|
. ($folder === 'root'
|
||||||
|
? 'root_metadata.json'
|
||||||
|
: str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json');
|
||||||
|
|
||||||
|
$metadata = [];
|
||||||
|
if (file_exists($metaFile)) {
|
||||||
|
$decoded = json_decode(file_get_contents($metaFile), true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$metadata = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date(DATE_TIME_FORMAT);
|
||||||
|
$uploaded = $metadata[$fileName]['uploaded'] ?? $now;
|
||||||
|
$uploader = CurrentUser::get();
|
||||||
|
|
||||||
|
$metadata[$fileName] = [
|
||||||
|
'uploaded' => $uploaded,
|
||||||
|
'modified' => $now,
|
||||||
|
'uploader' => $uploader,
|
||||||
|
];
|
||||||
|
|
||||||
|
file_put_contents($metaFile, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user