Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
242661a9c9 | ||
|
|
ca3e2f316c | ||
|
|
6ff4aa5f34 | ||
|
|
1eb54b8e6e | ||
|
|
4a6c424540 | ||
|
|
d23d5b7f3f | ||
|
|
a48ba09f02 |
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,6 +1,55 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Changes 4/19/2025
|
## Changes 4/22/2025 v1.2.3
|
||||||
|
|
||||||
|
- Support for custom PUID/PGID via `PUID`/`PGID` environment variables, replacing the need to run the container with `--user`
|
||||||
|
- New `PUID` and `PGID` config options in the Unraid Community Apps template
|
||||||
|
- Dockerfile:
|
||||||
|
- startup (`start.sh`) now runs as root to write `/etc/php` & `/etc/apache2` configs
|
||||||
|
- `www‑data` user is remapped at build‑time to the supplied `PUID:PGID`, then Apache drops privileges to that user
|
||||||
|
- Unraid template: removed recommendation to use `--user`; replaced with `PUID`, `PGID`, and `Container Port` variables
|
||||||
|
- “Permission denied” errors when forcing `--user 99:100` on Unraid by ensuring startup runs as root
|
||||||
|
- Dockerfile silence group issue
|
||||||
|
- `enableWebDAV` toggle in Admin Panel (default: disabled)
|
||||||
|
- **Admin Panel enhancements**
|
||||||
|
- New `enableWebDAV` boolean setting
|
||||||
|
- New `sharedMaxUploadSize` numeric setting (bytes)
|
||||||
|
- **Shared Folder upload size**
|
||||||
|
- `sharedMaxUploadSize` is now enforced in `FolderModel::uploadToSharedFolder`
|
||||||
|
- Upload form header on shared‑folder page dynamically shows “(X MB max size)”
|
||||||
|
- **API updates**
|
||||||
|
- `getConfig` and `updateConfig` endpoints now include `enableWebDAV` and `sharedMaxUploadSize`
|
||||||
|
- Updated `AdminModel` & `AdminController` to persist and validate new settings
|
||||||
|
- Enhanced `shareFolder()` view to pull from admin config and format the max‑upload‑size label
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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**
|
- **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)`.
|
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)`.
|
||||||
|
|||||||
44
Dockerfile
44
Dockerfile
@@ -31,7 +31,7 @@ FROM ubuntu:24.04
|
|||||||
|
|
||||||
LABEL by=error311
|
LABEL by=error311
|
||||||
|
|
||||||
# Set basic environment variables
|
# Set basic environment variables (these can be overridden via the Unraid template)
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
HOME=/root \
|
HOME=/root \
|
||||||
LC_ALL=C.UTF-8 \
|
LC_ALL=C.UTF-8 \
|
||||||
@@ -41,32 +41,48 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||||||
UPLOAD_MAX_FILESIZE=5G \
|
UPLOAD_MAX_FILESIZE=5G \
|
||||||
POST_MAX_SIZE=5G \
|
POST_MAX_SIZE=5G \
|
||||||
TOTAL_UPLOAD_SIZE=5G \
|
TOTAL_UPLOAD_SIZE=5G \
|
||||||
PERSISTENT_TOKENS_KEY=default_please_change_this_key
|
PERSISTENT_TOKENS_KEY=default_please_change_this_key \
|
||||||
|
PUID=99 \
|
||||||
ARG PUID=99
|
PGID=100
|
||||||
ARG PGID=100
|
|
||||||
|
|
||||||
# Install Apache, PHP, and required extensions
|
# Install Apache, PHP, and required extensions
|
||||||
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/*
|
||||||
|
|
||||||
# Fix www-data UID/GID
|
# Remap www-data to the PUID/PGID provided
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
if [ "$(id -u www-data)" != "${PUID}" ]; then usermod -u ${PUID} www-data || true; fi; \
|
# only change the UID if it’s not already correct
|
||||||
if [ "$(id -g www-data)" != "${PGID}" ]; then groupmod -g ${PGID} www-data || true; fi; \
|
if [ "$(id -u www-data)" != "${PUID}" ]; then \
|
||||||
usermod -g ${PGID} www-data
|
usermod -u "${PUID}" www-data; \
|
||||||
|
fi; \
|
||||||
|
# attempt to change the GID, but ignore “already exists” errors
|
||||||
|
if [ "$(id -g www-data)" != "${PGID}" ]; then \
|
||||||
|
groupmod -g "${PGID}" www-data 2>/dev/null || true; \
|
||||||
|
fi; \
|
||||||
|
# finally set www-data’s primary group to PGID (will succeed if the group exists)
|
||||||
|
usermod -g "${PGID}" www-data
|
||||||
|
|
||||||
# Copy application code and vendor directory
|
# Copy application tuning and code
|
||||||
COPY custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
|
COPY custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
|
||||||
COPY --from=appsource /var/www /var/www
|
COPY --from=appsource /var/www /var/www
|
||||||
COPY --from=composer /app/vendor /var/www/vendor
|
COPY --from=composer /app/vendor /var/www/vendor
|
||||||
|
|
||||||
# Fix ownership & permissions
|
# Ensure the webroot is owned by the remapped www-data user
|
||||||
RUN chown -R www-data:www-data /var/www && chmod -R 775 /var/www
|
RUN chown -R www-data:www-data /var/www && chmod -R 775 /var/www
|
||||||
|
|
||||||
# Create a symlink for uploads folder in public directory.
|
# Create a symlink for uploads folder in public directory.
|
||||||
@@ -90,7 +106,7 @@ EOF
|
|||||||
# Enable the rewrite and headers modules
|
# Enable the rewrite and headers modules
|
||||||
RUN a2enmod rewrite headers
|
RUN a2enmod rewrite headers
|
||||||
|
|
||||||
# Expose ports and set up start script
|
# Expose ports and set up the startup script
|
||||||
EXPOSE 80 443
|
EXPOSE 80 443
|
||||||
COPY start.sh /usr/local/bin/start.sh
|
COPY start.sh /usr/local/bin/start.sh
|
||||||
RUN chmod +x /usr/local/bin/start.sh
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|||||||
52
README.md
52
README.md
@@ -20,6 +20,8 @@ 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 use it head‑less from the CLI**. Standard WebDAV operations (upload / download / rename / delete) work in Cyberduck, WinSCP, GNOME Files, Finder, etc., and you can also script against it with `curl` – see the [WebDAV](https://github.com/error311/FileRise/wiki/WebDAV) + [curl](https://github.com/error311/FileRise/wiki/Accessing-FileRise-via-curl%C2%A0(WebDAV)) quick‑start for examples. Folder‑Only users are restricted to their personal directory, while admins and unrestricted users have full access.
|
||||||
|
|
||||||
- 📚 **API Documentation:** Fully auto‑generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
- 📚 **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.
|
||||||
@@ -34,7 +36,7 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
|||||||
|
|
||||||
- 🗑️ **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.)
|
||||||
|
|
||||||
@@ -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,13 +232,14 @@ 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
|
||||||
|
|
||||||
- **Google Fonts** – [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
|
- **Google Fonts** – [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
|
||||||
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
|
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
|
||||||
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) – For code editing functionality.
|
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) – For code editing functionality.
|
||||||
- **[Resumable.js](http://www.resumablejs.com/)** (v1.1.0) – For file uploads.
|
- **[Resumable.js](https://github.com/23/resumable.js/)** (v1.1.0) – For file uploads.
|
||||||
- **[DOMPurify](https://github.com/cure53/DOMPurify)** (v2.4.0) – For sanitizing HTML.
|
- **[DOMPurify](https://github.com/cure53/DOMPurify)** (v2.4.0) – For sanitizing HTML.
|
||||||
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
||||||
|
|
||||||
|
|||||||
@@ -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": [],
|
||||||
|
|||||||
@@ -3,7 +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.1"; // Update this version string as needed
|
const version = "v1.2.3"; // Update this version string as needed
|
||||||
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;
|
||||||
@@ -597,6 +597,7 @@ export function openAdminPanel() {
|
|||||||
}
|
}
|
||||||
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||||
|
|
||||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||||
const modalContentStyles = `
|
const modalContentStyles = `
|
||||||
@@ -611,6 +612,7 @@ export function openAdminPanel() {
|
|||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let adminModal = document.getElementById("adminPanelModal");
|
let adminModal = document.getElementById("adminPanelModal");
|
||||||
|
|
||||||
if (!adminModal) {
|
if (!adminModal) {
|
||||||
@@ -663,6 +665,28 @@ export function openAdminPanel() {
|
|||||||
<label for="disableOIDCLogin">${t("disable_oidc_login")}</label>
|
<label for="disableOIDCLogin">${t("disable_oidc_login")}</label>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- New WebDAV setting -->
|
||||||
|
<fieldset style="margin-bottom: 15px;">
|
||||||
|
<legend>WebDAV Access</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="enableWebDAV" />
|
||||||
|
<label for="enableWebDAV">Enable WebDAV</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<!-- End WebDAV setting -->
|
||||||
|
|
||||||
|
<!-- New Shared Max Upload Size setting -->
|
||||||
|
<fieldset style="margin-bottom: 15px;">
|
||||||
|
<legend>Shared Max Upload Size (bytes)</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="number" id="sharedMaxUploadSize" class="form-control"
|
||||||
|
placeholder="e.g. 52428800" />
|
||||||
|
<small>Enter maximum bytes allowed for shared-folder uploads</small>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<!-- End Shared Max Upload Size setting -->
|
||||||
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
<fieldset style="margin-bottom: 15px;">
|
||||||
<legend>${t("oidc_configuration")}</legend>
|
<legend>${t("oidc_configuration")}</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -698,33 +722,34 @@ export function openAdminPanel() {
|
|||||||
`;
|
`;
|
||||||
document.body.appendChild(adminModal);
|
document.body.appendChild(adminModal);
|
||||||
|
|
||||||
// Bind closing events that will use our enhanced close function.
|
// Bind closing
|
||||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||||
adminModal.addEventListener("click", (e) => {
|
adminModal.addEventListener("click", e => { if (e.target === adminModal) closeAdminPanel(); });
|
||||||
if (e.target === adminModal) closeAdminPanel();
|
|
||||||
});
|
|
||||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||||
|
|
||||||
// Bind other buttons.
|
// Bind other buttons
|
||||||
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById("newUsername").focus();
|
document.getElementById("newUsername").focus();
|
||||||
});
|
});
|
||||||
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
||||||
if (typeof window.loadUserList === "function") {
|
if (typeof window.loadUserList === "function") window.loadUserList();
|
||||||
window.loadUserList();
|
|
||||||
}
|
|
||||||
toggleVisibility("removeUserModal", true);
|
toggleVisibility("removeUserModal", true);
|
||||||
});
|
});
|
||||||
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
||||||
openUserPermissionsModal();
|
openUserPermissionsModal();
|
||||||
});
|
});
|
||||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
|
||||||
|
|
||||||
|
// Save handler
|
||||||
|
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");
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
const enableWebDAVCheckbox = document.getElementById("enableWebDAV");
|
||||||
|
const sharedMaxUploadSizeInput = document.getElementById("sharedMaxUploadSize");
|
||||||
|
|
||||||
|
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
||||||
|
.filter(cb => cb.checked).length;
|
||||||
if (totalDisabled === 3) {
|
if (totalDisabled === 3) {
|
||||||
showToast(t("at_least_one_login_method"));
|
showToast(t("at_least_one_login_method"));
|
||||||
disableOIDCLoginCheckbox.checked = false;
|
disableOIDCLoginCheckbox.checked = false;
|
||||||
@@ -738,8 +763,8 @@ 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(),
|
||||||
@@ -749,13 +774,18 @@ export function openAdminPanel() {
|
|||||||
const disableFormLogin = disableFormLoginCheckbox.checked;
|
const disableFormLogin = disableFormLoginCheckbox.checked;
|
||||||
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
||||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
||||||
|
const enableWebDAV = enableWebDAVCheckbox.checked;
|
||||||
|
const sharedMaxUploadSize = parseInt(sharedMaxUploadSizeInput.value, 10) || 0;
|
||||||
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,
|
||||||
disableBasicAuth,
|
disableBasicAuth,
|
||||||
disableOIDCLogin,
|
disableOIDCLogin,
|
||||||
|
enableWebDAV,
|
||||||
|
sharedMaxUploadSize,
|
||||||
globalOtpauthUrl
|
globalOtpauthUrl
|
||||||
}, { "X-CSRF-Token": window.csrfToken })
|
}, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -764,26 +794,32 @@ export function openAdminPanel() {
|
|||||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||||
|
localStorage.setItem("enableWebDAV", enableWebDAV);
|
||||||
|
localStorage.setItem("sharedMaxUploadSize", sharedMaxUploadSize);
|
||||||
if (typeof window.updateLoginOptionsUI === "function") {
|
if (typeof window.updateLoginOptionsUI === "function") {
|
||||||
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
window.updateLoginOptionsUI({
|
||||||
|
disableFormLogin,
|
||||||
|
disableBasicAuth,
|
||||||
|
disableOIDCLogin
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Update the captured initial state since the changes have now been saved.
|
|
||||||
captureInitialAdminConfig();
|
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")));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => { });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enforce login option constraints.
|
// Enforce login option constraints.
|
||||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||||
function enforceLoginOptionConstraint(changedCheckbox) {
|
function enforceLoginOptionConstraint(changedCheckbox) {
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
||||||
|
.filter(cb => cb.checked).length;
|
||||||
if (changedCheckbox.checked && totalDisabled === 3) {
|
if (changedCheckbox.checked && totalDisabled === 3) {
|
||||||
showToast(t("at_least_one_login_method"));
|
showToast(t("at_least_one_login_method"));
|
||||||
changedCheckbox.checked = false;
|
changedCheckbox.checked = false;
|
||||||
@@ -793,13 +829,17 @@ export function openAdminPanel() {
|
|||||||
disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||||
disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||||
|
|
||||||
|
// Initial checkbox and input states
|
||||||
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;
|
||||||
|
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||||
|
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||||
|
|
||||||
// Capture initial state after the modal loads.
|
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
// Update existing modal and show
|
||||||
adminModal.style.backgroundColor = overlayBackground;
|
adminModal.style.backgroundColor = overlayBackground;
|
||||||
const modalContent = adminModal.querySelector(".modal-content");
|
const modalContent = adminModal.querySelector(".modal-content");
|
||||||
if (modalContent) {
|
if (modalContent) {
|
||||||
@@ -815,6 +855,8 @@ 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;
|
||||||
|
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||||
|
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||||
adminModal.style.display = "flex";
|
adminModal.style.display = "flex";
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
}
|
}
|
||||||
@@ -837,6 +879,8 @@ export function openAdminPanel() {
|
|||||||
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
||||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
||||||
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
||||||
|
document.getElementById("enableWebDAV").checked = localStorage.getItem("enableWebDAV") === "true";
|
||||||
|
document.getElementById("sharedMaxUploadSize").value = localStorage.getItem("sharedMaxUploadSize") || "";
|
||||||
adminModal.style.display = "flex";
|
adminModal.style.display = "flex";
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -634,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
|
||||||
|
|||||||
74
public/webdav.php
Normal file
74
public/webdav.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
// public/webdav.php
|
||||||
|
|
||||||
|
// ─── 0) Forward Basic auth into PHP_AUTH_* for every HTTP verb ─────────────
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 1) Bootstrap & load models ─────────────────────────────────────────────
|
||||||
|
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()
|
||||||
|
require_once __DIR__ . '/../src/models/AdminModel.php'; // AdminModel::getConfig()
|
||||||
|
|
||||||
|
// ─── 1.1) Global WebDAV feature toggle ──────────────────────────────────────
|
||||||
|
$adminConfig = AdminModel::getConfig();
|
||||||
|
$enableWebDAV = isset($adminConfig['enableWebDAV']) && $adminConfig['enableWebDAV'];
|
||||||
|
if (!$enableWebDAV) {
|
||||||
|
header('HTTP/1.1 403 Forbidden');
|
||||||
|
echo 'WebDAV access is currently disabled by administrator.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 2) Load 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\Locks\Plugin as LocksPlugin;
|
||||||
|
use Sabre\DAV\Locks\Backend\File as LocksFileBackend;
|
||||||
|
use FileRise\WebDAV\FileRiseDirectory;
|
||||||
|
|
||||||
|
// ─── 3) HTTP‑Basic backend ─────────────────────────────────────────────────
|
||||||
|
$authBackend = new BasicCallBack(function(string $user, string $pass) {
|
||||||
|
return \AuthModel::authenticate($user, $pass) !== false;
|
||||||
|
});
|
||||||
|
$authPlugin = new AuthPlugin($authBackend, 'FileRise');
|
||||||
|
|
||||||
|
// ─── 4) Determine user scope ────────────────────────────────────────────────
|
||||||
|
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
|
||||||
|
$isAdmin = (\AuthModel::getUserRole($user) === '1');
|
||||||
|
$folderOnly = (bool)\AuthModel::loadFolderPermission($user);
|
||||||
|
|
||||||
|
if ($isAdmin || !$folderOnly) {
|
||||||
|
// Admins (or users without folder-only restriction) 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 5) Spin up SabreDAV ────────────────────────────────────────────────────
|
||||||
|
$server = new Server([
|
||||||
|
new FileRiseDirectory($rootPath, $user, $folderOnly),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$server->addPlugin($authPlugin);
|
||||||
|
$server->addPlugin(
|
||||||
|
new LocksPlugin(
|
||||||
|
new LocksFileBackend(sys_get_temp_dir() . '/sabre-locksdb')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$server->setBaseUri('/webdav.php/');
|
||||||
|
$server->exec();
|
||||||
@@ -35,7 +35,9 @@ class AdminController
|
|||||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||||
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
||||||
* ),
|
* ),
|
||||||
* @OA\Property(property="globalOtpauthUrl", type="string", example="")
|
* @OA\Property(property="globalOtpauthUrl", type="string", example=""),
|
||||||
|
* @OA\Property(property="enableWebDAV", type="boolean", example=false),
|
||||||
|
* @OA\Property(property="sharedMaxUploadSize", type="integer", example=52428800)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
@@ -88,7 +90,9 @@ class AdminController
|
|||||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||||
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
||||||
* ),
|
* ),
|
||||||
* @OA\Property(property="globalOtpauthUrl", type="string", example="")
|
* @OA\Property(property="globalOtpauthUrl", type="string", example=""),
|
||||||
|
* @OA\Property(property="enableWebDAV", type="boolean", example=false),
|
||||||
|
* @OA\Property(property="sharedMaxUploadSize", type="integer", example=52428800)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
@@ -149,7 +153,7 @@ class AdminController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare configuration array.
|
// Prepare existing settings
|
||||||
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
||||||
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
||||||
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
||||||
@@ -183,20 +187,38 @@ class AdminController
|
|||||||
}
|
}
|
||||||
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
|
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
|
||||||
|
|
||||||
|
// ── NEW: enableWebDAV flag ──────────────────────────────────────
|
||||||
|
$enableWebDAV = false;
|
||||||
|
if (array_key_exists('enableWebDAV', $data)) {
|
||||||
|
$enableWebDAV = filter_var($data['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
|
||||||
|
} elseif (isset($data['features']['enableWebDAV'])) {
|
||||||
|
$enableWebDAV = filter_var($data['features']['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NEW: sharedMaxUploadSize ──────────────────────────────────────
|
||||||
|
$sharedMaxUploadSize = null;
|
||||||
|
if (array_key_exists('sharedMaxUploadSize', $data)) {
|
||||||
|
$sharedMaxUploadSize = filter_var($data['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
|
||||||
|
} elseif (isset($data['features']['sharedMaxUploadSize'])) {
|
||||||
|
$sharedMaxUploadSize = filter_var($data['features']['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
|
||||||
|
}
|
||||||
|
|
||||||
$configUpdate = [
|
$configUpdate = [
|
||||||
'header_title' => $headerTitle,
|
'header_title' => $headerTitle,
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
'providerUrl' => $oidcProviderUrl,
|
'providerUrl' => $oidcProviderUrl,
|
||||||
'clientId' => $oidcClientId,
|
'clientId' => $oidcClientId,
|
||||||
'clientSecret' => $oidcClientSecret,
|
'clientSecret' => $oidcClientSecret,
|
||||||
'redirectUri' => $oidcRedirectUri,
|
'redirectUri' => $oidcRedirectUri,
|
||||||
],
|
],
|
||||||
'loginOptions' => [
|
'loginOptions' => [
|
||||||
'disableFormLogin' => $disableFormLogin,
|
'disableFormLogin' => $disableFormLogin,
|
||||||
'disableBasicAuth' => $disableBasicAuth,
|
'disableBasicAuth' => $disableBasicAuth,
|
||||||
'disableOIDCLogin' => $disableOIDCLogin,
|
'disableOIDCLogin' => $disableOIDCLogin,
|
||||||
],
|
],
|
||||||
'globalOtpauthUrl' => $globalOtpauthUrl
|
'globalOtpauthUrl' => $globalOtpauthUrl,
|
||||||
|
'enableWebDAV' => $enableWebDAV,
|
||||||
|
'sharedMaxUploadSize' => $sharedMaxUploadSize // ← NEW
|
||||||
];
|
];
|
||||||
|
|
||||||
// Delegate to the model.
|
// Delegate to the model.
|
||||||
@@ -207,4 +229,4 @@ class AdminController
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -401,6 +401,20 @@ class FolderController
|
|||||||
*
|
*
|
||||||
* @return void Outputs HTML content.
|
* @return void Outputs HTML content.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function formatBytes($bytes)
|
||||||
|
{
|
||||||
|
if ($bytes < 1024) {
|
||||||
|
return $bytes . " B";
|
||||||
|
} elseif ($bytes < 1024 * 1024) {
|
||||||
|
return round($bytes / 1024, 2) . " KB";
|
||||||
|
} elseif ($bytes < 1024 * 1024 * 1024) {
|
||||||
|
return round($bytes / (1024 * 1024), 2) . " MB";
|
||||||
|
} else {
|
||||||
|
return round($bytes / (1024 * 1024 * 1024), 2) . " GB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function shareFolder(): void
|
public function shareFolder(): void
|
||||||
{
|
{
|
||||||
// Retrieve GET parameters.
|
// Retrieve GET parameters.
|
||||||
@@ -495,12 +509,14 @@ class FolderController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract data for the HTML view.
|
// Load admin config so we can pull the sharedMaxUploadSize
|
||||||
$folderName = $data['folder'];
|
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||||
$files = $data['files'];
|
$adminConfig = AdminModel::getConfig();
|
||||||
$currentPage = $data['currentPage'];
|
$sharedMaxUploadSize = isset($adminConfig['sharedMaxUploadSize']) && is_numeric($adminConfig['sharedMaxUploadSize'])
|
||||||
$totalPages = $data['totalPages'];
|
? (int)$adminConfig['sharedMaxUploadSize']
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// For human‐readable formatting
|
||||||
function formatBytes($bytes)
|
function formatBytes($bytes)
|
||||||
{
|
{
|
||||||
if ($bytes < 1024) {
|
if ($bytes < 1024) {
|
||||||
@@ -514,6 +530,12 @@ class FolderController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract data for the HTML view.
|
||||||
|
$folderName = $data['folder'];
|
||||||
|
$files = $data['files'];
|
||||||
|
$currentPage = $data['currentPage'];
|
||||||
|
$totalPages = $data['totalPages'];
|
||||||
|
|
||||||
// Build the HTML view.
|
// Build the HTML view.
|
||||||
header("Content-Type: text/html; charset=utf-8");
|
header("Content-Type: text/html; charset=utf-8");
|
||||||
?>
|
?>
|
||||||
@@ -717,7 +739,11 @@ class FolderController
|
|||||||
<!-- Upload Container (if uploads are allowed by the share record) -->
|
<!-- Upload Container (if uploads are allowed by the share record) -->
|
||||||
<?php if (isset($data['record']['allowUpload']) && $data['record']['allowUpload'] == 1): ?>
|
<?php if (isset($data['record']['allowUpload']) && $data['record']['allowUpload'] == 1): ?>
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<h3>Upload File (50mb max size)</h3>
|
<h3>Upload File
|
||||||
|
<?php if ($sharedMaxUploadSize !== null): ?>
|
||||||
|
(<?php echo formatBytes($sharedMaxUploadSize); ?> max size)
|
||||||
|
<?php endif; ?>
|
||||||
|
</h3>
|
||||||
<form action="/api/folder/uploadToSharedFolder.php" method="post" enctype="multipart/form-data">
|
<form action="/api/folder/uploadToSharedFolder.php" method="post" enctype="multipart/form-data">
|
||||||
<!-- Pass the share token so the upload endpoint can verify -->
|
<!-- Pass the share token so the upload endpoint can verify -->
|
||||||
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
|
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
|||||||
@@ -5,6 +5,23 @@ require_once PROJECT_ROOT . '/config/config.php';
|
|||||||
|
|
||||||
class AdminModel
|
class AdminModel
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Parse a shorthand size value (e.g. "5G", "500M", "123K") into bytes.
|
||||||
|
*
|
||||||
|
* @param string $val
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function parseSize(string $val): int
|
||||||
|
{
|
||||||
|
$unit = strtolower(substr($val, -1));
|
||||||
|
$num = (int) rtrim($val, 'bkmgtpezyBKMGTPESY');
|
||||||
|
switch ($unit) {
|
||||||
|
case 'g': return $num * 1024 ** 3;
|
||||||
|
case 'm': return $num * 1024 ** 2;
|
||||||
|
case 'k': return $num * 1024;
|
||||||
|
default: return $num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the admin configuration file.
|
* Updates the admin configuration file.
|
||||||
@@ -24,6 +41,28 @@ class AdminModel
|
|||||||
return ["error" => "Incomplete OIDC configuration."];
|
return ["error" => "Incomplete OIDC configuration."];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure enableWebDAV flag is boolean (default to false if missing)
|
||||||
|
$configUpdate['enableWebDAV'] = isset($configUpdate['enableWebDAV'])
|
||||||
|
? (bool)$configUpdate['enableWebDAV']
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// Validate sharedMaxUploadSize if provided
|
||||||
|
if (isset($configUpdate['sharedMaxUploadSize'])) {
|
||||||
|
$sms = filter_var(
|
||||||
|
$configUpdate['sharedMaxUploadSize'],
|
||||||
|
FILTER_VALIDATE_INT,
|
||||||
|
["options" => ["min_range" => 1]]
|
||||||
|
);
|
||||||
|
if ($sms === false) {
|
||||||
|
return ["error" => "Invalid sharedMaxUploadSize."];
|
||||||
|
}
|
||||||
|
$totalBytes = self::parseSize(TOTAL_UPLOAD_SIZE);
|
||||||
|
if ($sms > $totalBytes) {
|
||||||
|
return ["error" => "sharedMaxUploadSize must be ≤ TOTAL_UPLOAD_SIZE."];
|
||||||
|
}
|
||||||
|
$configUpdate['sharedMaxUploadSize'] = $sms;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert configuration to JSON.
|
// Convert configuration to JSON.
|
||||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||||
if ($plainTextConfig === false) {
|
if ($plainTextConfig === false) {
|
||||||
@@ -59,7 +98,8 @@ class AdminModel
|
|||||||
*
|
*
|
||||||
* @return array The configuration array, or defaults if not found.
|
* @return array The configuration array, or defaults if not found.
|
||||||
*/
|
*/
|
||||||
public static function getConfig(): array {
|
public static function getConfig(): array
|
||||||
|
{
|
||||||
$configFile = USERS_DIR . 'adminConfig.json';
|
$configFile = USERS_DIR . 'adminConfig.json';
|
||||||
if (file_exists($configFile)) {
|
if (file_exists($configFile)) {
|
||||||
$encryptedContent = file_get_contents($configFile);
|
$encryptedContent = file_get_contents($configFile);
|
||||||
@@ -72,10 +112,9 @@ class AdminModel
|
|||||||
if (!is_array($config)) {
|
if (!is_array($config)) {
|
||||||
$config = [];
|
$config = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize login options.
|
// Normalize login options if missing
|
||||||
if (!isset($config['loginOptions'])) {
|
if (!isset($config['loginOptions'])) {
|
||||||
// Create loginOptions array from top-level keys if missing.
|
|
||||||
$config['loginOptions'] = [
|
$config['loginOptions'] = [
|
||||||
'disableFormLogin' => isset($config['disableFormLogin']) ? (bool)$config['disableFormLogin'] : false,
|
'disableFormLogin' => isset($config['disableFormLogin']) ? (bool)$config['disableFormLogin'] : false,
|
||||||
'disableBasicAuth' => isset($config['disableBasicAuth']) ? (bool)$config['disableBasicAuth'] : false,
|
'disableBasicAuth' => isset($config['disableBasicAuth']) ? (bool)$config['disableBasicAuth'] : false,
|
||||||
@@ -88,31 +127,43 @@ class AdminModel
|
|||||||
$config['loginOptions']['disableBasicAuth'] = (bool)$config['loginOptions']['disableBasicAuth'];
|
$config['loginOptions']['disableBasicAuth'] = (bool)$config['loginOptions']['disableBasicAuth'];
|
||||||
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default values for other keys
|
||||||
if (!isset($config['globalOtpauthUrl'])) {
|
if (!isset($config['globalOtpauthUrl'])) {
|
||||||
$config['globalOtpauthUrl'] = "";
|
$config['globalOtpauthUrl'] = "";
|
||||||
}
|
}
|
||||||
if (!isset($config['header_title']) || empty($config['header_title'])) {
|
if (!isset($config['header_title']) || empty($config['header_title'])) {
|
||||||
$config['header_title'] = "FileRise";
|
$config['header_title'] = "FileRise";
|
||||||
}
|
}
|
||||||
|
if (!isset($config['enableWebDAV'])) {
|
||||||
|
$config['enableWebDAV'] = false;
|
||||||
|
}
|
||||||
|
// Default sharedMaxUploadSize to 50MB or TOTAL_UPLOAD_SIZE if smaller
|
||||||
|
if (!isset($config['sharedMaxUploadSize'])) {
|
||||||
|
$defaultSms = min(50 * 1024 * 1024, self::parseSize(TOTAL_UPLOAD_SIZE));
|
||||||
|
$config['sharedMaxUploadSize'] = $defaultSms;
|
||||||
|
}
|
||||||
|
|
||||||
return $config;
|
return $config;
|
||||||
} else {
|
} else {
|
||||||
// Return defaults.
|
// Return defaults.
|
||||||
return [
|
return [
|
||||||
'header_title' => "FileRise",
|
'header_title' => "FileRise",
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
'providerUrl' => 'https://your-oidc-provider.com',
|
'providerUrl' => 'https://your-oidc-provider.com',
|
||||||
'clientId' => 'YOUR_CLIENT_ID',
|
'clientId' => 'YOUR_CLIENT_ID',
|
||||||
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
||||||
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
||||||
],
|
],
|
||||||
'loginOptions' => [
|
'loginOptions' => [
|
||||||
'disableFormLogin' => false,
|
'disableFormLogin' => false,
|
||||||
'disableBasicAuth' => false,
|
'disableBasicAuth' => false,
|
||||||
'disableOIDCLogin' => false
|
'disableOIDCLogin' => false
|
||||||
],
|
],
|
||||||
'globalOtpauthUrl' => ""
|
'globalOtpauthUrl' => "",
|
||||||
|
'enableWebDAV' => false,
|
||||||
|
'sharedMaxUploadSize' => min(50 * 1024 * 1024, self::parseSize(TOTAL_UPLOAD_SIZE))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
4
start.sh
4
start.sh
@@ -152,8 +152,8 @@ find /var/www -type f -exec chmod 664 {} \;
|
|||||||
find /var/www -type d -exec chmod 775 {} \;
|
find /var/www -type d -exec chmod 775 {} \;
|
||||||
chown -R ${PUID:-99}:${PGID:-100} /var/www
|
chown -R ${PUID:-99}:${PGID:-100} /var/www
|
||||||
|
|
||||||
echo "🔥 Final PHP configuration (90-custom.ini):"
|
echo "🔥 Final PHP configuration (99-custom.ini):"
|
||||||
cat /etc/php/8.3/apache2/conf.d/90-custom.ini
|
cat /etc/php/8.3/apache2/conf.d/99-custom.ini
|
||||||
|
|
||||||
echo "🔥 Final Apache configuration (limit_request_body.conf):"
|
echo "🔥 Final Apache configuration (limit_request_body.conf):"
|
||||||
cat /etc/apache2/conf-enabled/limit_request_body.conf
|
cat /etc/apache2/conf-enabled/limit_request_body.conf
|
||||||
|
|||||||
Reference in New Issue
Block a user