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
|
||||
|
||||
## 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**
|
||||
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
|
||||
|
||||
# Set basic environment variables
|
||||
# Set basic environment variables (these can be overridden via the Unraid template)
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
HOME=/root \
|
||||
LC_ALL=C.UTF-8 \
|
||||
@@ -41,32 +41,48 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
||||
UPLOAD_MAX_FILESIZE=5G \
|
||||
POST_MAX_SIZE=5G \
|
||||
TOTAL_UPLOAD_SIZE=5G \
|
||||
PERSISTENT_TOKENS_KEY=default_please_change_this_key
|
||||
|
||||
ARG PUID=99
|
||||
ARG PGID=100
|
||||
PERSISTENT_TOKENS_KEY=default_please_change_this_key \
|
||||
PUID=99 \
|
||||
PGID=100
|
||||
|
||||
# Install Apache, PHP, and required extensions
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
apache2 php php-json php-curl php-zip php-mbstring php-gd \
|
||||
ca-certificates curl git openssl && \
|
||||
apache2 \
|
||||
php \
|
||||
php-json \
|
||||
php-curl \
|
||||
php-zip \
|
||||
php-mbstring \
|
||||
php-gd \
|
||||
php-xml \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
openssl && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Fix www-data UID/GID
|
||||
# Remap www-data to the PUID/PGID provided
|
||||
RUN set -eux; \
|
||||
if [ "$(id -u www-data)" != "${PUID}" ]; then usermod -u ${PUID} www-data || true; fi; \
|
||||
if [ "$(id -g www-data)" != "${PGID}" ]; then groupmod -g ${PGID} www-data || true; fi; \
|
||||
usermod -g ${PGID} www-data
|
||||
# only change the UID if it’s not already correct
|
||||
if [ "$(id -u www-data)" != "${PUID}" ]; then \
|
||||
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 --from=appsource /var/www /var/www
|
||||
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
|
||||
|
||||
# Create a symlink for uploads folder in public directory.
|
||||
@@ -90,7 +106,7 @@ EOF
|
||||
# Enable the rewrite and headers modules
|
||||
RUN a2enmod rewrite headers
|
||||
|
||||
# Expose ports and set up start script
|
||||
# Expose ports and set up the startup script
|
||||
EXPOSE 80 443
|
||||
COPY start.sh /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.
|
||||
|
||||
- 🔌 **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.
|
||||
|
||||
- 📝 **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.
|
||||
|
||||
- ⚙️ **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.)
|
||||
|
||||
@@ -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
|
||||
|
||||
- **“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)
|
||||
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.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
|
||||
|
||||
- **Google Fonts** – [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
|
||||
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
|
||||
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) – For code editing functionality.
|
||||
- **[Resumable.js](http://www.resumablejs.com/)** (v1.1.0) – For file uploads.
|
||||
- **[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.
|
||||
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"jumbojett/openid-connect-php": "^1.0.0",
|
||||
"phpseclib/phpseclib": "~3.0.7",
|
||||
"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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6b70aec0c1830ebb2b8f9bb625b04a22",
|
||||
"content-hash": "3a9b8d9fcfdaaa865ba03eab392e88fd",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@@ -451,6 +451,56 @@
|
||||
],
|
||||
"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",
|
||||
"version": "v3.0.2",
|
||||
@@ -531,6 +581,451 @@
|
||||
}
|
||||
],
|
||||
"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": [],
|
||||
|
||||
@@ -3,7 +3,7 @@ import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.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>`;
|
||||
|
||||
let lastLoginData = null;
|
||||
@@ -597,6 +597,7 @@ export function openAdminPanel() {
|
||||
}
|
||||
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||
|
||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||
const modalContentStyles = `
|
||||
@@ -611,6 +612,7 @@ export function openAdminPanel() {
|
||||
max-height: 90vh;
|
||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||
`;
|
||||
|
||||
let adminModal = document.getElementById("adminPanelModal");
|
||||
|
||||
if (!adminModal) {
|
||||
@@ -663,6 +665,28 @@ export function openAdminPanel() {
|
||||
<label for="disableOIDCLogin">${t("disable_oidc_login")}</label>
|
||||
</div>
|
||||
</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;">
|
||||
<legend>${t("oidc_configuration")}</legend>
|
||||
<div class="form-group">
|
||||
@@ -698,33 +722,34 @@ export function openAdminPanel() {
|
||||
`;
|
||||
document.body.appendChild(adminModal);
|
||||
|
||||
// Bind closing events that will use our enhanced close function.
|
||||
// Bind closing
|
||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||
adminModal.addEventListener("click", (e) => {
|
||||
if (e.target === adminModal) closeAdminPanel();
|
||||
});
|
||||
adminModal.addEventListener("click", e => { if (e.target === adminModal) closeAdminPanel(); });
|
||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||
|
||||
// Bind other buttons.
|
||||
// Bind other buttons
|
||||
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
||||
if (typeof window.loadUserList === "function") {
|
||||
window.loadUserList();
|
||||
}
|
||||
if (typeof window.loadUserList === "function") window.loadUserList();
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
||||
openUserPermissionsModal();
|
||||
});
|
||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
||||
|
||||
// Save handler
|
||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||
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) {
|
||||
showToast(t("at_least_one_login_method"));
|
||||
disableOIDCLoginCheckbox.checked = false;
|
||||
@@ -738,8 +763,8 @@ export function openAdminPanel() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
|
||||
|
||||
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
|
||||
const newOIDCConfig = {
|
||||
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||
@@ -749,13 +774,18 @@ export function openAdminPanel() {
|
||||
const disableFormLogin = disableFormLoginCheckbox.checked;
|
||||
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
||||
const enableWebDAV = enableWebDAVCheckbox.checked;
|
||||
const sharedMaxUploadSize = parseInt(sharedMaxUploadSizeInput.value, 10) || 0;
|
||||
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
||||
|
||||
sendRequest("/api/admin/updateConfig.php", "POST", {
|
||||
header_title: newHeaderTitle,
|
||||
oidc: newOIDCConfig,
|
||||
disableFormLogin,
|
||||
disableBasicAuth,
|
||||
disableOIDCLogin,
|
||||
enableWebDAV,
|
||||
sharedMaxUploadSize,
|
||||
globalOtpauthUrl
|
||||
}, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(response => {
|
||||
@@ -764,26 +794,32 @@ export function openAdminPanel() {
|
||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||
localStorage.setItem("enableWebDAV", enableWebDAV);
|
||||
localStorage.setItem("sharedMaxUploadSize", sharedMaxUploadSize);
|
||||
if (typeof window.updateLoginOptionsUI === "function") {
|
||||
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||
window.updateLoginOptionsUI({
|
||||
disableFormLogin,
|
||||
disableBasicAuth,
|
||||
disableOIDCLogin
|
||||
});
|
||||
}
|
||||
// Update the captured initial state since the changes have now been saved.
|
||||
captureInitialAdminConfig();
|
||||
closeAdminPanel();
|
||||
loadAdminConfigFunc();
|
||||
|
||||
} else {
|
||||
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
});
|
||||
|
||||
// Enforce login option constraints.
|
||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||
function enforceLoginOptionConstraint(changedCheckbox) {
|
||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
||||
.filter(cb => cb.checked).length;
|
||||
if (changedCheckbox.checked && totalDisabled === 3) {
|
||||
showToast(t("at_least_one_login_method"));
|
||||
changedCheckbox.checked = false;
|
||||
@@ -793,13 +829,17 @@ export function openAdminPanel() {
|
||||
disableBasicAuthCheckbox.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("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === 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();
|
||||
|
||||
} else {
|
||||
// Update existing modal and show
|
||||
adminModal.style.backgroundColor = overlayBackground;
|
||||
const modalContent = adminModal.querySelector(".modal-content");
|
||||
if (modalContent) {
|
||||
@@ -815,6 +855,8 @@ export function openAdminPanel() {
|
||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||
adminModal.style.display = "flex";
|
||||
captureInitialAdminConfig();
|
||||
}
|
||||
@@ -837,6 +879,8 @@ export function openAdminPanel() {
|
||||
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "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";
|
||||
captureInitialAdminConfig();
|
||||
} else {
|
||||
|
||||
@@ -634,7 +634,7 @@ function updateSliderConstraints() {
|
||||
|
||||
// Set maximum based on screen size.
|
||||
if (width < 600) { // small devices (phones)
|
||||
max = 2;
|
||||
max = 1;
|
||||
} else if (width < 1024) { // medium devices
|
||||
max = 3;
|
||||
} 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="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(
|
||||
@@ -88,7 +90,9 @@ class AdminController
|
||||
* @OA\Property(property="disableBasicAuth", 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(
|
||||
@@ -149,7 +153,7 @@ class AdminController
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prepare configuration array.
|
||||
// Prepare existing settings
|
||||
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
||||
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
||||
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
||||
@@ -183,20 +187,38 @@ class AdminController
|
||||
}
|
||||
$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 = [
|
||||
'header_title' => $headerTitle,
|
||||
'oidc' => [
|
||||
'providerUrl' => $oidcProviderUrl,
|
||||
'clientId' => $oidcClientId,
|
||||
'clientSecret' => $oidcClientSecret,
|
||||
'redirectUri' => $oidcRedirectUri,
|
||||
'header_title' => $headerTitle,
|
||||
'oidc' => [
|
||||
'providerUrl' => $oidcProviderUrl,
|
||||
'clientId' => $oidcClientId,
|
||||
'clientSecret' => $oidcClientSecret,
|
||||
'redirectUri' => $oidcRedirectUri,
|
||||
],
|
||||
'loginOptions' => [
|
||||
'loginOptions' => [
|
||||
'disableFormLogin' => $disableFormLogin,
|
||||
'disableBasicAuth' => $disableBasicAuth,
|
||||
'disableOIDCLogin' => $disableOIDCLogin,
|
||||
],
|
||||
'globalOtpauthUrl' => $globalOtpauthUrl
|
||||
'globalOtpauthUrl' => $globalOtpauthUrl,
|
||||
'enableWebDAV' => $enableWebDAV,
|
||||
'sharedMaxUploadSize' => $sharedMaxUploadSize // ← NEW
|
||||
];
|
||||
|
||||
// Delegate to the model.
|
||||
@@ -207,4 +229,4 @@ class AdminController
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,56 +450,57 @@ class FileController {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = $headersArr['x-csrf-token'] ?? '';
|
||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
// --- Authentication Check ---
|
||||
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if the user is allowed to save files (not read-only).
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
// --- Read‑only check ---
|
||||
$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."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get JSON input.
|
||||
// --- Input parsing ---
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if (!$data) {
|
||||
echo json_encode(["error" => "No data received"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($data["fileName"]) || !isset($data["content"])) {
|
||||
if (empty($data) || !isset($data["fileName"], $data["content"])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid request data", "received" => $data]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$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)) {
|
||||
echo json_encode(["error" => "Invalid folder name"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = trim($folder, "/\\ ");
|
||||
|
||||
// Delegate to the model.
|
||||
$result = FileModel::saveFile($folder, $fileName, $data["content"]);
|
||||
// --- Delegate to model, passing the uploader ---
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -401,6 +401,20 @@ class FolderController
|
||||
*
|
||||
* @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
|
||||
{
|
||||
// Retrieve GET parameters.
|
||||
@@ -495,12 +509,14 @@ class FolderController
|
||||
exit;
|
||||
}
|
||||
|
||||
// Extract data for the HTML view.
|
||||
$folderName = $data['folder'];
|
||||
$files = $data['files'];
|
||||
$currentPage = $data['currentPage'];
|
||||
$totalPages = $data['totalPages'];
|
||||
// Load admin config so we can pull the sharedMaxUploadSize
|
||||
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||
$adminConfig = AdminModel::getConfig();
|
||||
$sharedMaxUploadSize = isset($adminConfig['sharedMaxUploadSize']) && is_numeric($adminConfig['sharedMaxUploadSize'])
|
||||
? (int)$adminConfig['sharedMaxUploadSize']
|
||||
: null;
|
||||
|
||||
// For human‐readable formatting
|
||||
function formatBytes($bytes)
|
||||
{
|
||||
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.
|
||||
header("Content-Type: text/html; charset=utf-8");
|
||||
?>
|
||||
@@ -717,7 +739,11 @@ class FolderController
|
||||
<!-- Upload Container (if uploads are allowed by the share record) -->
|
||||
<?php if (isset($data['record']['allowUpload']) && $data['record']['allowUpload'] == 1): ?>
|
||||
<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">
|
||||
<!-- Pass the share token so the upload endpoint can verify -->
|
||||
<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
|
||||
{
|
||||
/**
|
||||
* 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.
|
||||
@@ -24,6 +41,28 @@ class AdminModel
|
||||
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.
|
||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||
if ($plainTextConfig === false) {
|
||||
@@ -59,7 +98,8 @@ class AdminModel
|
||||
*
|
||||
* @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';
|
||||
if (file_exists($configFile)) {
|
||||
$encryptedContent = file_get_contents($configFile);
|
||||
@@ -72,10 +112,9 @@ class AdminModel
|
||||
if (!is_array($config)) {
|
||||
$config = [];
|
||||
}
|
||||
|
||||
// Normalize login options.
|
||||
|
||||
// Normalize login options if missing
|
||||
if (!isset($config['loginOptions'])) {
|
||||
// Create loginOptions array from top-level keys if missing.
|
||||
$config['loginOptions'] = [
|
||||
'disableFormLogin' => isset($config['disableFormLogin']) ? (bool)$config['disableFormLogin'] : false,
|
||||
'disableBasicAuth' => isset($config['disableBasicAuth']) ? (bool)$config['disableBasicAuth'] : false,
|
||||
@@ -88,31 +127,43 @@ class AdminModel
|
||||
$config['loginOptions']['disableBasicAuth'] = (bool)$config['loginOptions']['disableBasicAuth'];
|
||||
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
||||
}
|
||||
|
||||
|
||||
// Default values for other keys
|
||||
if (!isset($config['globalOtpauthUrl'])) {
|
||||
$config['globalOtpauthUrl'] = "";
|
||||
}
|
||||
if (!isset($config['header_title']) || empty($config['header_title'])) {
|
||||
$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;
|
||||
} else {
|
||||
// Return defaults.
|
||||
return [
|
||||
'header_title' => "FileRise",
|
||||
'oidc' => [
|
||||
'header_title' => "FileRise",
|
||||
'oidc' => [
|
||||
'providerUrl' => 'https://your-oidc-provider.com',
|
||||
'clientId' => 'YOUR_CLIENT_ID',
|
||||
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
||||
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
||||
],
|
||||
'loginOptions' => [
|
||||
'loginOptions' => [
|
||||
'disableFormLogin' => false,
|
||||
'disableBasicAuth' => 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.
|
||||
*
|
||||
* @param string $folder The target folder where the file is to be saved (e.g. "root" or a subfolder).
|
||||
* @param string $fileName The name of the file.
|
||||
* @param string $content The file content.
|
||||
* @return array Returns an associative array with either a "success" key or an "error" key.
|
||||
*/
|
||||
public static function saveFile($folder, $fileName, $content) {
|
||||
// Sanitize and determine the folder name.
|
||||
$folder = trim($folder) ?: 'root';
|
||||
$fileName = basename(trim($fileName));
|
||||
/*
|
||||
* Save a file’s contents *and* record its metadata, including who uploaded it.
|
||||
*
|
||||
* @param string $folder Folder key (e.g. "root" or "invoices/2025")
|
||||
* @param string $fileName Basename of the file
|
||||
* @param resource|string $content File contents (stream or string)
|
||||
* @param string|null $uploader Username of uploader (if null, falls back to session)
|
||||
* @return array ["success"=>"…"] or ["error"=>"…"]
|
||||
*/
|
||||
public static function saveFile(string $folder, string $fileName, $content, ?string $uploader = null): array {
|
||||
// Sanitize inputs
|
||||
$folder = trim($folder) ?: 'root';
|
||||
$fileName = basename(trim($fileName));
|
||||
|
||||
// Validate folder: if not "root", must match REGEX_FOLDER_NAME.
|
||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name"];
|
||||
// Validate folder name
|
||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
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"];
|
||||
}
|
||||
|
||||
// Determine base upload directory.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
if (strtolower($folder) === 'root' || $folder === "") {
|
||||
$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) {
|
||||
stream_copy_to_stream($content, $out);
|
||||
fclose($out);
|
||||
} else {
|
||||
if (file_put_contents($filePath, (string)$content) === false) {
|
||||
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.
|
||||
*
|
||||
|
||||
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 {} \;
|
||||
chown -R ${PUID:-99}:${PGID:-100} /var/www
|
||||
|
||||
echo "🔥 Final PHP configuration (90-custom.ini):"
|
||||
cat /etc/php/8.3/apache2/conf.d/90-custom.ini
|
||||
echo "🔥 Final PHP configuration (99-custom.ini):"
|
||||
cat /etc/php/8.3/apache2/conf.d/99-custom.ini
|
||||
|
||||
echo "🔥 Final Apache configuration (limit_request_body.conf):"
|
||||
cat /etc/apache2/conf-enabled/limit_request_body.conf
|
||||
|
||||
Reference in New Issue
Block a user