diff --git a/CHANGELOG.md b/CHANGELOG.md index 981a8ee..26489cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,31 @@ # Changelog -## Changes 4/19/2025 +## 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)`. diff --git a/Dockerfile b/Dockerfile index eca82a5..967c833 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,8 +50,18 @@ ARG PGID=100 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/* diff --git a/README.md b/README.md index 57e26dc..7f538b5 100644 --- a/README.md +++ b/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 via WebDAV. Standard file operations (upload/download/rename/delete) work from any WebDAV‑compatible client. Per‑user scoping ensures folderOnly users see only their own folder, while others 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,6 +232,7 @@ Areas where you can help: translations, bug fixes, UI improvements, or building - **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7) - **[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 diff --git a/composer.json b/composer.json index 2eb59e0..bddea99 100644 --- a/composer.json +++ b/composer.json @@ -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" } } \ No newline at end of file diff --git a/composer.lock b/composer.lock index 948e444..3b3c098 100644 --- a/composer.lock +++ b/composer.lock @@ -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": [], diff --git a/public/js/authModals.js b/public/js/authModals.js index 43d9f2d..6c1a029 100644 --- a/public/js/authModals.js +++ b/public/js/authModals.js @@ -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.2"; // Update this version string as needed const adminTitle = `${t("admin_panel")} ${version}`; let lastLoginData = null; diff --git a/public/js/fileListView.js b/public/js/fileListView.js index 2dc535f..8d2e43f 100644 --- a/public/js/fileListView.js +++ b/public/js/fileListView.js @@ -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 diff --git a/public/webdav.php b/public/webdav.php new file mode 100644 index 0000000..62d68a7 --- /dev/null +++ b/public/webdav.php @@ -0,0 +1,61 @@ +addPlugin($authPlugin); +//$server->addPlugin(new BrowserPlugin()); // optional HTML browser UI +$server->addPlugin( + new LocksPlugin( + new LocksFileBackend(sys_get_temp_dir() . '/sabre-locksdb') + ) +); + +$server->setBaseUri('/webdav.php/'); +$server->exec(); \ No newline at end of file diff --git a/src/controllers/fileController.php b/src/controllers/fileController.php index cdebfc8..02b0bfe 100644 --- a/src/controllers/fileController.php +++ b/src/controllers/fileController.php @@ -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); } diff --git a/src/models/FileModel.php b/src/models/FileModel.php index 0dae96a..caabcd6 100644 --- a/src/models/FileModel.php +++ b/src/models/FileModel.php @@ -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. * diff --git a/src/webdav/CurrentUser.php b/src/webdav/CurrentUser.php new file mode 100644 index 0000000..de0457e --- /dev/null +++ b/src/webdav/CurrentUser.php @@ -0,0 +1,16 @@ +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); + } +} \ No newline at end of file diff --git a/src/webdav/FileRiseFile.php b/src/webdav/FileRiseFile.php new file mode 100644 index 0000000..99dc490 --- /dev/null +++ b/src/webdav/FileRiseFile.php @@ -0,0 +1,115 @@ +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)); + } +} \ No newline at end of file