Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
078db33458 | ||
|
|
04f5cbe31f | ||
|
|
b5a7d8d559 | ||
|
|
58f8485b02 | ||
|
|
3e1da9c335 | ||
|
|
6bf6206e1c | ||
|
|
f9c60951c9 | ||
|
|
06b3f28df0 | ||
|
|
89f124250c | ||
|
|
66f13fd6a7 | ||
|
|
a81d9cb940 | ||
|
|
13b8871200 | ||
|
|
2792c05c1c | ||
|
|
6ccfc88acb | ||
|
|
7f1d59b33a | ||
|
|
e4e8b108d2 | ||
|
|
242661a9c9 | ||
|
|
ca3e2f316c | ||
|
|
6ff4aa5f34 | ||
|
|
1eb54b8e6e | ||
|
|
4a6c424540 | ||
|
|
d23d5b7f3f | ||
|
|
a48ba09f02 |
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# dockerignore
|
||||||
|
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.github
|
||||||
|
.github/**
|
||||||
|
Dockerfile*
|
||||||
|
resources/
|
||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
tmp/
|
||||||
|
.env
|
||||||
|
.vscode/
|
||||||
|
.DS_Store
|
||||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,2 +1,4 @@
|
|||||||
public/api.html linguist-documentation
|
public/api.html linguist-documentation
|
||||||
public/openapi.json linguist-documentation
|
public/openapi.json linguist-documentation
|
||||||
|
resources/ export-ignore
|
||||||
|
.github/ export-ignore
|
||||||
3
.github/workflows/sync-changelog.yml
vendored
3
.github/workflows/sync-changelog.yml
vendored
@@ -5,6 +5,9 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'CHANGELOG.md'
|
- 'CHANGELOG.md'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
123
CHANGELOG.md
123
CHANGELOG.md
@@ -1,6 +1,127 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Changes 4/19/2025
|
## Changes 4/24/2025 1.2.5
|
||||||
|
|
||||||
|
- Enhance README and wiki with expanded installation instructions
|
||||||
|
- Adjusted Dockerfile’s Apache vhost to:
|
||||||
|
- Alias `/uploads/` to `/var/www/uploads/` with PHP engine disabled and directory indexes off
|
||||||
|
- Disable HTTP TRACE and tune keep-alive (On, max 100 requests, 5s timeout) and server Timeout (60s)
|
||||||
|
- Add security headers (`X-Frame-Options`, `X-Content-Type-Options`, `X-XSS-Protection`, `Referrer-Policy`)
|
||||||
|
- Enable `mod_deflate` compression for HTML, plain text, CSS, JS and JSON
|
||||||
|
- Configure `mod_expires` caching for images (1 month), CSS (1 week) and JS (3 hour)
|
||||||
|
- Deny access to hidden files (dot-files)
|
||||||
|
- Add access control in public/.htaccess for api.html & openapi.json; update Nginx example in wiki
|
||||||
|
- Remove obsolete folders from repo root
|
||||||
|
- Embed API documentation (`api.html`) directly in the FileRise UI as a full-screen modal
|
||||||
|
- Introduced `openApiModalBtn` in the user panel to launch the API modal
|
||||||
|
- Added `#apiModal` container with a same-origin `<iframe src="api.html">` so session cookies authenticate automatically
|
||||||
|
- Close control uses the existing `.editor-close-btn` for consistent styling and hover effects
|
||||||
|
|
||||||
|
## Changes 4/23/2025 1.2.4
|
||||||
|
|
||||||
|
**AuthModel**
|
||||||
|
|
||||||
|
- **Added** `validateRememberToken(string $token): ?array`
|
||||||
|
- Reads and decrypts `persistent_tokens.json`
|
||||||
|
- Verifies token exists and hasn’t expired
|
||||||
|
- Returns stored payload (`username`, `expiry`, `isAdmin`, etc.) or `null` if invalid
|
||||||
|
|
||||||
|
**authController (checkAuth)**
|
||||||
|
|
||||||
|
- **Enhanced** “remember-me” re-login path at top of `checkAuth()`
|
||||||
|
- Calls `AuthModel::validateRememberToken()` when session is missing but `remember_me_token` cookie present
|
||||||
|
- Repopulates `$_SESSION['authenticated']`, `username`, `isAdmin`, `folderOnly`, `readOnly`, `disableUpload` from payload
|
||||||
|
- Regenerates session ID and CSRF token, then immediately returns JSON and exits
|
||||||
|
|
||||||
|
- **Updated** `userController.php`
|
||||||
|
- Fixed totp isAdmin when session is missing but `remember_me_token` cookie present
|
||||||
|
|
||||||
|
- **loadCsrfToken()**
|
||||||
|
- Now reads `X-CSRF-Token` response header first, falls back to JSON `csrf_token` if header absent
|
||||||
|
- Updates `window.csrfToken`, `window.SHARE_URL`, and `<meta>` tags with the new values
|
||||||
|
- **fetchWithCsrf(url, options)**
|
||||||
|
- Sends `credentials: 'include'` and current `X-CSRF-Token` on every request
|
||||||
|
- Handles “soft-failure” JSON (`{ csrf_expired: true, csrf_token }`): updates token and retries once without a 403 in DevTools
|
||||||
|
- On HTTP 403 fallback: reads new token from header or `/api/auth/token.php`, updates token, and retries once
|
||||||
|
|
||||||
|
- **start.sh**
|
||||||
|
- Session directory setup
|
||||||
|
|
||||||
|
- Always sends `credentials: 'include'` and `X-CSRF-Token: window.csrfToken` s
|
||||||
|
- On HTTP 403, automatically fetches a fresh CSRF token (from the response header or `/api/auth/token.php`) and retries the request once
|
||||||
|
- Always returns the real `Response` object (no more “clone.json” on every 200)
|
||||||
|
- Now calls `fetchWithCsrf('/api/auth/token.php')` to guarantee a fresh token
|
||||||
|
- Checks `res.ok`, then parses JSON to extract `csrf_token` and `share_url`
|
||||||
|
- Updates both `window.csrfToken` and the `<meta name="csrf-token">` & `<meta name="share-url">` tags
|
||||||
|
- Removed Old CSRF logic that cloned every successful response and parsed its JSON body
|
||||||
|
- Removed Any “soft-failure” JSON peek on non-403 responses
|
||||||
|
- Add missing permissions in `UserModel.php` for TOTP login.
|
||||||
|
- **Prevent XSS in breadcrumbs**
|
||||||
|
- Replaced `innerHTML` calls in `fileListTitle` with a new `updateBreadcrumbTitle()` helper that uses `textContent` + `DocumentFragment`.
|
||||||
|
- Introduced `renderBreadcrumbFragment()` to build each breadcrumb segment as a `<span class="breadcrumb-link" data-folder="…">` node.
|
||||||
|
- Added `setupBreadcrumbDelegation()` to handle clicks via event delegation on the container, eliminating per-element listeners.
|
||||||
|
- Removed any raw HTML concatenation to satisfy CodeQL and ensure all breadcrumb text is safely escaped.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- Restored the MIT license copyright line that was inadvertently removed.
|
||||||
|
- Move .htaccess to public folder this was mistake since API refactor.
|
||||||
|
- gitattributes to ignore resources/ & .github/ on export
|
||||||
|
- Hardened `Dockerfile` permissions: all code files owned by `root:www-data` (dirs `755`, files `644`), only `uploads/`, `users/` and `metadata/` are writable by `www-data` (`775`)
|
||||||
|
- `.dockerignore` entry to exclude the `.github` directory from build context
|
||||||
|
- `start.sh`:
|
||||||
|
- Creates and secures `metadata/log` for Apache logs
|
||||||
|
- Dynamically creates and sets permissions on `uploads`, `users`, and `metadata` directories at startup
|
||||||
|
- Apache VirtualHost updated to redirect `ErrorLog` and `CustomLog` into `/var/www/metadata/log`
|
||||||
|
- docker: remove symlink add alias for uploads folder
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 4/21/2025 v1.2.2
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`src/webdav/CurrentUser.php`**
|
||||||
|
– Introduces a `CurrentUser` singleton to capture and expose the authenticated WebDAV username for use in other components.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **`src/webdav/FileRiseDirectory.php`**
|
||||||
|
– Constructor now takes three parameters (`$path`, `$user`, `$folderOnly`).
|
||||||
|
– Implements “folder‑only” mode: non‑admin users only see their own subfolder under the uploads root.
|
||||||
|
– Passes the current user through to `FileRiseFile` so that uploads/deletions are attributed correctly.
|
||||||
|
|
||||||
|
- **`src/webdav/FileRiseFile.php`**
|
||||||
|
– Uses `CurrentUser::get()` when writing metadata to populate the `uploader` field.
|
||||||
|
– Metadata helper (`updateMetadata`) now records both upload and modified timestamps along with the actual username.
|
||||||
|
|
||||||
|
- **`public/webdav.php`**
|
||||||
|
– Adds a header‐shim at the top to pull Basic‑Auth credentials out of `Authorization` for all HTTP methods.
|
||||||
|
– In the auth callback, sets the `CurrentUser` for the rest of the request.
|
||||||
|
- Admins & unrestricted users see the full `/uploads` directory.
|
||||||
|
- “Folder‑only” users are scoped to `/uploads/{username}`.
|
||||||
|
– Configures SabreDAV with the new `FileRiseDirectory($rootPath, $user, $folderOnly)` signature and sets the base URI to `/webdav.php/`.
|
||||||
|
|
||||||
|
## Changes 4/19/2025 v1.2.1
|
||||||
|
|
||||||
- **Extended “Remember Me” cookie behavior**
|
- **Extended “Remember Me” cookie behavior**
|
||||||
In `AuthController::finalizeLogin()`, after setting `remember_me_token` re‑issued the PHP session cookie with the same 30‑day expiry and called `session_regenerate_id(true)`.
|
In `AuthController::finalizeLogin()`, after setting `remember_me_token` re‑issued the PHP session cookie with the same 30‑day expiry and called `session_regenerate_id(true)`.
|
||||||
|
|||||||
117
Dockerfile
117
Dockerfile
@@ -6,12 +6,9 @@
|
|||||||
FROM ubuntu:24.04 AS appsource
|
FROM ubuntu:24.04 AS appsource
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends ca-certificates && \
|
apt-get install -y --no-install-recommends ca-certificates && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/* # clean up apt cache
|
||||||
|
|
||||||
# prepare the folder and remove Apache’s default index
|
|
||||||
RUN mkdir -p /var/www && rm -f /var/www/html/index.html
|
RUN mkdir -p /var/www && rm -f /var/www/html/index.html
|
||||||
|
|
||||||
# **Copy the FileRise source** (where your composer.json lives)
|
|
||||||
COPY . /var/www
|
COPY . /var/www
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
@@ -19,78 +16,118 @@ COPY . /var/www
|
|||||||
#############################
|
#############################
|
||||||
FROM composer:2 AS composer
|
FROM composer:2 AS composer
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# **Copy composer files from the source** and install
|
|
||||||
COPY --from=appsource /var/www/composer.json /var/www/composer.lock ./
|
COPY --from=appsource /var/www/composer.json /var/www/composer.lock ./
|
||||||
RUN composer install --no-dev --optimize-autoloader
|
RUN composer install --no-dev --optimize-autoloader # production-ready autoloader
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# Final Stage – runtime image
|
# Final Stage – runtime image
|
||||||
#############################
|
#############################
|
||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
LABEL by=error311
|
LABEL by=error311
|
||||||
|
|
||||||
# Set basic environment variables
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
HOME=/root \
|
HOME=/root \
|
||||||
LC_ALL=C.UTF-8 \
|
LC_ALL=C.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 TERM=xterm \
|
||||||
LANG=en_US.UTF-8 \
|
UPLOAD_MAX_FILESIZE=5G POST_MAX_SIZE=5G TOTAL_UPLOAD_SIZE=5G \
|
||||||
LANGUAGE=en_US.UTF-8 \
|
PERSISTENT_TOKENS_KEY=default_please_change_this_key \
|
||||||
TERM=xterm \
|
PUID=99 PGID=100
|
||||||
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
|
|
||||||
|
|
||||||
# Install Apache, PHP, and required extensions
|
# Install Apache, PHP, and required extensions
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get upgrade -y && \
|
apt-get upgrade -y && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
apache2 php php-json php-curl php-zip php-mbstring php-gd \
|
apache2 php php-json php-curl php-zip php-mbstring php-gd php-xml \
|
||||||
ca-certificates curl git openssl && \
|
ca-certificates curl git openssl && \
|
||||||
apt-get clean && \
|
apt-get clean && rm -rf /var/lib/apt/lists/* # slim down image
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Fix www-data UID/GID
|
# Remap www-data to the PUID/PGID provided for safe bind mounts
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
if [ "$(id -u www-data)" != "${PUID}" ]; then usermod -u ${PUID} www-data || true; fi; \
|
if [ "$(id -u www-data)" != "${PUID}" ]; then usermod -u "${PUID}" www-data; fi; \
|
||||||
if [ "$(id -g www-data)" != "${PGID}" ]; then groupmod -g ${PGID} www-data || true; fi; \
|
if [ "$(id -g www-data)" != "${PGID}" ]; then groupmod -g "${PGID}" www-data 2>/dev/null || true; fi; \
|
||||||
usermod -g ${PGID} www-data
|
usermod -g "${PGID}" www-data
|
||||||
|
|
||||||
# Copy application code and vendor directory
|
# Copy config, code, and vendor
|
||||||
COPY custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
|
COPY custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
|
||||||
COPY --from=appsource /var/www /var/www
|
COPY --from=appsource /var/www /var/www
|
||||||
COPY --from=composer /app/vendor /var/www/vendor
|
COPY --from=composer /app/vendor /var/www/vendor
|
||||||
|
|
||||||
# Fix ownership & permissions
|
# Secure permissions: code read-only, only data dirs writable
|
||||||
RUN chown -R www-data:www-data /var/www && chmod -R 775 /var/www
|
RUN chown -R root:www-data /var/www && \
|
||||||
|
find /var/www -type d -exec chmod 755 {} \; && \
|
||||||
|
find /var/www -type f -exec chmod 644 {} \; && \
|
||||||
|
mkdir -p /var/www/public/uploads /var/www/users /var/www/metadata && \
|
||||||
|
chown -R www-data:www-data /var/www/public/uploads /var/www/users /var/www/metadata && \
|
||||||
|
chmod -R 775 /var/www/public/uploads /var/www/users /var/www/metadata # writable upload areas
|
||||||
|
|
||||||
# Create a symlink for uploads folder in public directory.
|
# Apache site configuration
|
||||||
RUN cd /var/www/public && ln -s ../uploads uploads
|
|
||||||
|
|
||||||
# Configure Apache
|
|
||||||
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
|
# Global settings
|
||||||
|
TraceEnable off
|
||||||
|
KeepAlive On
|
||||||
|
MaxKeepAliveRequests 100
|
||||||
|
KeepAliveTimeout 5
|
||||||
|
Timeout 60
|
||||||
|
|
||||||
ServerAdmin webmaster@localhost
|
ServerAdmin webmaster@localhost
|
||||||
DocumentRoot /var/www/public
|
DocumentRoot /var/www/public
|
||||||
|
|
||||||
|
# Security headers for all responses
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
<IfModule mod_expires.c>
|
||||||
|
ExpiresActive on
|
||||||
|
ExpiresByType image/jpeg "access plus 1 month"
|
||||||
|
ExpiresByType image/png "access plus 1 month"
|
||||||
|
ExpiresByType text/css "access plus 1 week"
|
||||||
|
ExpiresByType application/javascript "access plus 3 hour"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Protect uploads directory
|
||||||
|
Alias /uploads/ /var/www/uploads/
|
||||||
|
<Directory "/var/www/uploads/">
|
||||||
|
Options -Indexes
|
||||||
|
AllowOverride None
|
||||||
|
<IfModule mod_php7.c>
|
||||||
|
php_flag engine off
|
||||||
|
</IfModule>
|
||||||
|
<IfModule mod_php.c>
|
||||||
|
php_flag engine off
|
||||||
|
</IfModule>
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Public directory
|
||||||
<Directory "/var/www/public">
|
<Directory "/var/www/public">
|
||||||
AllowOverride All
|
AllowOverride All
|
||||||
Require all granted
|
Require all granted
|
||||||
DirectoryIndex index.php index.html
|
DirectoryIndex index.html index.php
|
||||||
</Directory>
|
</Directory>
|
||||||
ErrorLog /var/log/apache2/error.log
|
|
||||||
CustomLog /var/log/apache2/access.log combined
|
# Deny access to hidden files
|
||||||
|
<FilesMatch "^\.">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
ErrorLog /var/www/metadata/log/error.log
|
||||||
|
CustomLog /var/www/metadata/log/access.log combined
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Enable the rewrite and headers modules
|
# Enable required modules
|
||||||
RUN a2enmod rewrite headers
|
RUN a2enmod rewrite headers
|
||||||
|
|
||||||
# Expose ports and set up start script
|
|
||||||
EXPOSE 80 443
|
EXPOSE 80 443
|
||||||
COPY start.sh /usr/local/bin/start.sh
|
COPY start.sh /usr/local/bin/start.sh
|
||||||
RUN chmod +x /usr/local/bin/start.sh
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|||||||
1
LICENSE
1
LICENSE
@@ -1,5 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 SeNS
|
||||||
Copyright (c) 2025 FileRise
|
Copyright (c) 2025 FileRise
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
|||||||
64
README.md
64
README.md
@@ -1,7 +1,7 @@
|
|||||||
# FileRise
|
# FileRise
|
||||||
|
|
||||||
**Elevate your File Management** – A modern, self-hosted web file manager.
|
**Elevate your File Management** – A modern, self-hosted web file manager.
|
||||||
Upload, organize, and share files through a sleek web interface. **FileRise** is lightweight yet powerful: think of it as your personal cloud drive that you control. With drag-and-drop uploads, in-browser editing, secure user logins (with SSO and 2FA support), and one-click sharing, **FileRise** makes file management on your server a breeze.
|
Upload, organize, and share files or folders through a sleek web interface. **FileRise** is lightweight yet powerful: think of it as your personal cloud drive that you control. With drag-and-drop uploads, in-browser editing, secure user logins (with SSO and 2FA support), and one-click sharing, **FileRise** makes file management on your server a breeze.
|
||||||
|
|
||||||
**4/3/2025 Video demo:**
|
**4/3/2025 Video demo:**
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
|||||||
|
|
||||||
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) – convenient for sending individual files without exposing the whole app.
|
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) – convenient for sending individual files without exposing the whole app.
|
||||||
|
|
||||||
|
- 🔌 **WebDAV Support:** Mount FileRise as a network drive **or use it head‑less from the CLI**. Standard WebDAV operations (upload / download / rename / delete) work in Cyberduck, WinSCP, GNOME Files, Finder, etc., and you can also script against it with `curl` – see the [WebDAV](https://github.com/error311/FileRise/wiki/WebDAV) + [curl](https://github.com/error311/FileRise/wiki/Accessing-FileRise-via-curl%C2%A0(WebDAV)) quick‑start for examples. Folder‑Only users are restricted to their personal directory, while admins and unrestricted users have full access.
|
||||||
|
|
||||||
- 📚 **API Documentation:** Fully auto‑generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
- 📚 **API Documentation:** Fully auto‑generated OpenAPI spec (`openapi.json`) and interactive HTML docs (`api.html`) powered by Redoc.
|
||||||
|
|
||||||
- 📝 **Built-in Editor & Preview:** View images, videos, audio, and PDFs inline with a preview modal – no need to download just to see them. Edit text/code files right in your browser with a CodeMirror-based editor featuring syntax highlighting and line numbers. Great for config files or notes – tweak and save changes without leaving FileRise.
|
- 📝 **Built-in Editor & Preview:** View images, videos, audio, and PDFs inline with a preview modal – no need to download just to see them. Edit text/code files right in your browser with a CodeMirror-based editor featuring syntax highlighting and line numbers. Great for config files or notes – tweak and save changes without leaving FileRise.
|
||||||
@@ -34,7 +36,7 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
|||||||
|
|
||||||
- 🗑️ **Trash & File Recovery:** Mistakenly deleted files? No worries – deleted items go to the Trash instead of immediate removal. Admins can restore files from Trash or empty it to free space. FileRise auto-purges old trash entries (default 3 days) to keep your storage tidy.
|
- 🗑️ **Trash & File Recovery:** Mistakenly deleted files? No worries – deleted items go to the Trash instead of immediate removal. Admins can restore files from Trash or empty it to free space. FileRise auto-purges old trash entries (default 3 days) to keep your storage tidy.
|
||||||
|
|
||||||
- ⚙️ **Lightweight & Self-Contained:** FileRise runs on PHP 8.1+ with no external database required – data is stored in files (users, metadata) for simplicity. It’s a single-folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre-built image for a hassle-free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features.
|
- ⚙️ **Lightweight & Self‑Contained:** FileRise runs on PHP 8.1+ with no external database required – data is stored in files (users, metadata) for simplicity. It’s a single‑folder web app you can drop into any Apache/PHP server or run as a container. Docker & Unraid ready: use our pre‑built image for a hassle‑free setup. Memory and CPU footprint is minimal, yet the app scales to thousands of files with pagination and sorting features.
|
||||||
|
|
||||||
(For a full list of features and detailed changelogs, see the [Wiki](https://github.com/error311/FileRise/wiki), [changelog](https://github.com/error311/FileRise/blob/master/CHANGELOG.md) or the [releases](https://github.com/error311/FileRise/releases) pages.)
|
(For a full list of features and detailed changelogs, see the [Wiki](https://github.com/error311/FileRise/wiki), [changelog](https://github.com/error311/FileRise/blob/master/CHANGELOG.md) or the [releases](https://github.com/error311/FileRise/releases) pages.)
|
||||||
|
|
||||||
@@ -113,9 +115,9 @@ If you prefer to run FileRise on a traditional web server (LAMP stack or similar
|
|||||||
git clone https://github.com/error311/FileRise.git
|
git clone https://github.com/error311/FileRise.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Place the files into your web server’s directory (e.g., `/var/www/html/filerise`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
|
Place the files into your web server’s directory (e.g., `/var/www/public`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
|
||||||
|
|
||||||
- **Composer Dependencies:** If you plan to use OIDC (SSO login), install Composer and run `composer install` in the FileRise directory. (This pulls in a couple of PHP libraries like jumbojett/openid-connect for OAuth support.) If you skip this, FileRise will still work, but OIDC login won’t be available.
|
- **Composer Dependencies:** If you plan to use OIDC (SSO login), install Composer and run `composer install` in the FileRise directory. (This pulls in a couple of PHP libraries like jumbojett/openid-connect for OAuth support.)
|
||||||
|
|
||||||
- **Folder Permissions:** Ensure the server can write to the following directories (create them if they don’t exist):
|
- **Folder Permissions:** Ensure the server can write to the following directories (create them if they don’t exist):
|
||||||
|
|
||||||
@@ -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 must enable WebDAV in admin panel to access it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux (GVFS/GIO)
|
||||||
|
gio mount dav://demo@your-host/webdav.php/
|
||||||
|
|
||||||
|
# macOS (Finder → Go → Connect to Server…)
|
||||||
|
dav://demo@your-host/webdav.php/
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (File Explorer)
|
||||||
|
|
||||||
|
- Open **File Explorer** → Right-click **This PC** → **Map network drive…**
|
||||||
|
- Choose a drive letter (e.g., `Z:`).
|
||||||
|
- In **Folder**, enter:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://your-host/webdav.php/
|
||||||
|
```
|
||||||
|
|
||||||
|
- Check **Connect using different credentials**, and enter your FileRise username and password.
|
||||||
|
- Click **Finish**. The drive will now appear under **This PC**.
|
||||||
|
|
||||||
|
> **Important:**
|
||||||
|
> Windows requires HTTPS (SSL) for WebDAV connections by default.
|
||||||
|
> If your server uses plain HTTP, you must adjust a registry setting:
|
||||||
|
>
|
||||||
|
> 1. Open **Registry Editor** (`regedit.exe`).
|
||||||
|
> 2. Navigate to:
|
||||||
|
>
|
||||||
|
> ```text
|
||||||
|
> HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> 3. Find or create a `DWORD` value named **BasicAuthLevel**.
|
||||||
|
> 4. Set its value to `2`.
|
||||||
|
> 5. Restart the **WebClient** service or reboot your computer.
|
||||||
|
|
||||||
|
📖 For a full guide (including SSL setup, HTTP workaround, and troubleshooting), see the [WebDAV Usage Wiki](https://github.com/error311/FileRise/wiki/WebDAV).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## FAQ / Troubleshooting
|
## FAQ / Troubleshooting
|
||||||
|
|
||||||
- **“Upload failed” or large files not uploading:** Make sure `TOTAL_UPLOAD_SIZE` in config and PHP’s `post_max_size` / `upload_max_filesize` are all set high enough. For extremely large files, you might also need to increase max_execution_time in PHP or rely on the resumable upload feature in smaller chunks.
|
- **“Upload failed” or large files not uploading:** Make sure `TOTAL_UPLOAD_SIZE` in config and PHP’s `post_max_size` / `upload_max_filesize` are all set high enough. For extremely large files, you might also need to increase max_execution_time in PHP or rely on the resumable upload feature in smaller chunks.
|
||||||
@@ -185,18 +232,25 @@ Areas where you can help: translations, bug fixes, UI improvements, or building
|
|||||||
- **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7)
|
- **[phpseclib/phpseclib](https://github.com/phpseclib/phpseclib)** (v~3.0.7)
|
||||||
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
|
- **[robthree/twofactorauth](https://github.com/RobThree/TwoFactorAuth)** (v^3.0)
|
||||||
- **[endroid/qr-code](https://github.com/endroid/qr-code)** (v^5.0)
|
- **[endroid/qr-code](https://github.com/endroid/qr-code)** (v^5.0)
|
||||||
|
- **[sabre/dav](https://github.com/sabre-io/dav)** (^4.4)
|
||||||
|
|
||||||
### Client-Side Libraries
|
### Client-Side Libraries
|
||||||
|
|
||||||
- **Google Fonts** – [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
|
- **Google Fonts** – [Roboto](https://fonts.google.com/specimen/Roboto) and **Material Icons** ([Google Material Icons](https://fonts.google.com/icons))
|
||||||
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
|
- **[Bootstrap](https://getbootstrap.com/)** (v4.5.2)
|
||||||
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) – For code editing functionality.
|
- **[CodeMirror](https://codemirror.net/)** (v5.65.5) – For code editing functionality.
|
||||||
- **[Resumable.js](http://www.resumablejs.com/)** (v1.1.0) – For file uploads.
|
- **[Resumable.js](https://github.com/23/resumable.js/)** (v1.1.0) – For file uploads.
|
||||||
- **[DOMPurify](https://github.com/cure53/DOMPurify)** (v2.4.0) – For sanitizing HTML.
|
- **[DOMPurify](https://github.com/cure53/DOMPurify)** (v2.4.0) – For sanitizing HTML.
|
||||||
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
- **[Fuse.js](https://fusejs.io/)** (v6.6.2) – For indexed, fuzzy searching.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- Based on [uploader](https://github.com/sensboston/uploader) by @sensboston.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is open-source under the MIT License. That means you’re free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!
|
This project is open-source under the MIT License. That means you’re free to use, modify, and distribute **FileRise**, with attribution. We hope you find it useful and contribute back!
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"jumbojett/openid-connect-php": "^1.0.0",
|
"jumbojett/openid-connect-php": "^1.0.0",
|
||||||
"phpseclib/phpseclib": "~3.0.7",
|
"phpseclib/phpseclib": "~3.0.7",
|
||||||
"robthree/twofactorauth": "^3.0",
|
"robthree/twofactorauth": "^3.0",
|
||||||
"endroid/qr-code": "^5.0"
|
"endroid/qr-code": "^5.0",
|
||||||
|
"sabre/dav": "^4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
497
composer.lock
generated
497
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "6b70aec0c1830ebb2b8f9bb625b04a22",
|
"content-hash": "3a9b8d9fcfdaaa865ba03eab392e88fd",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -451,6 +451,56 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-12-14T21:12:59+00:00"
|
"time": "2024-12-14T21:12:59+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/log",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/log.git",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Log\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for logging libraries",
|
||||||
|
"homepage": "https://github.com/php-fig/log",
|
||||||
|
"keywords": [
|
||||||
|
"log",
|
||||||
|
"psr",
|
||||||
|
"psr-3"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||||
|
},
|
||||||
|
"time": "2024-09-11T13:17:53+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "robthree/twofactorauth",
|
"name": "robthree/twofactorauth",
|
||||||
"version": "v3.0.2",
|
"version": "v3.0.2",
|
||||||
@@ -531,6 +581,451 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-10-24T15:14:25+00:00"
|
"time": "2024-10-24T15:14:25+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/dav",
|
||||||
|
"version": "4.7.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/dav.git",
|
||||||
|
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
|
||||||
|
"reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-date": "*",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-pcre": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"ext-spl": "*",
|
||||||
|
"lib-libxml": ">=2.7.0",
|
||||||
|
"php": "^7.1.0 || ^8.0",
|
||||||
|
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"sabre/event": "^5.0",
|
||||||
|
"sabre/http": "^5.0.5",
|
||||||
|
"sabre/uri": "^2.0",
|
||||||
|
"sabre/vobject": "^4.2.1",
|
||||||
|
"sabre/xml": "^2.0.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^2.19",
|
||||||
|
"monolog/monolog": "^1.27 || ^2.0",
|
||||||
|
"phpstan/phpstan": "^0.12 || ^1.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.0",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-imap": "*",
|
||||||
|
"ext-pdo": "*"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/sabredav",
|
||||||
|
"bin/naturalselection"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "WebDAV Framework for PHP",
|
||||||
|
"homepage": "http://sabre.io/",
|
||||||
|
"keywords": [
|
||||||
|
"CalDAV",
|
||||||
|
"CardDAV",
|
||||||
|
"WebDAV",
|
||||||
|
"framework",
|
||||||
|
"iCalendar"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/dav/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-dav"
|
||||||
|
},
|
||||||
|
"time": "2024-10-29T11:46:02+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/event",
|
||||||
|
"version": "5.1.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/event.git",
|
||||||
|
"reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2",
|
||||||
|
"reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "~2.17.1||^3.63",
|
||||||
|
"phpstan/phpstan": "^0.12",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/coroutine.php",
|
||||||
|
"lib/Loop/functions.php",
|
||||||
|
"lib/Promise/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\Event\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "sabre/event is a library for lightweight event-based programming",
|
||||||
|
"homepage": "http://sabre.io/event/",
|
||||||
|
"keywords": [
|
||||||
|
"EventEmitter",
|
||||||
|
"async",
|
||||||
|
"coroutine",
|
||||||
|
"eventloop",
|
||||||
|
"events",
|
||||||
|
"hooks",
|
||||||
|
"plugin",
|
||||||
|
"promise",
|
||||||
|
"reactor",
|
||||||
|
"signal"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/event/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-event"
|
||||||
|
},
|
||||||
|
"time": "2024-08-27T11:23:05+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/http",
|
||||||
|
"version": "5.1.12",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/http.git",
|
||||||
|
"reference": "dedff73f3995578bc942fa4c8484190cac14f139"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/http/zipball/dedff73f3995578bc942fa4c8484190cac14f139",
|
||||||
|
"reference": "dedff73f3995578bc942fa4c8484190cac14f139",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"sabre/event": ">=4.0 <6.0",
|
||||||
|
"sabre/uri": "^2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "~2.17.1||^3.63",
|
||||||
|
"phpstan/phpstan": "^0.12",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-curl": " to make http requests with the Client class"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\HTTP\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
|
||||||
|
"homepage": "https://github.com/fruux/sabre-http",
|
||||||
|
"keywords": [
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/http/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-http"
|
||||||
|
},
|
||||||
|
"time": "2024-08-27T16:07:41+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/uri",
|
||||||
|
"version": "2.3.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/uri.git",
|
||||||
|
"reference": "b76524c22de90d80ca73143680a8e77b1266c291"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291",
|
||||||
|
"reference": "b76524c22de90d80ca73143680a8e77b1266c291",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.63",
|
||||||
|
"phpstan/extension-installer": "^1.4",
|
||||||
|
"phpstan/phpstan": "^1.12",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.4",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.6",
|
||||||
|
"phpunit/phpunit": "^9.6"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\Uri\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Functions for making sense out of URIs.",
|
||||||
|
"homepage": "http://sabre.io/uri/",
|
||||||
|
"keywords": [
|
||||||
|
"rfc3986",
|
||||||
|
"uri",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/uri/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-uri"
|
||||||
|
},
|
||||||
|
"time": "2024-08-27T12:18:16+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/vobject",
|
||||||
|
"version": "4.5.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/vobject.git",
|
||||||
|
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
|
||||||
|
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"sabre/xml": "^2.1 || ^3.0 || ^4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "~2.17.1",
|
||||||
|
"phpstan/phpstan": "^0.12 || ^1.12 || ^2.0",
|
||||||
|
"phpunit/php-invoker": "^2.0 || ^3.1",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"hoa/bench": "If you would like to run the benchmark scripts"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/vobject",
|
||||||
|
"bin/generate_vcards"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\VObject\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dominik Tobschall",
|
||||||
|
"email": "dominik@fruux.com",
|
||||||
|
"homepage": "http://tobschall.de/",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ivan Enderlin",
|
||||||
|
"email": "ivan.enderlin@hoa-project.net",
|
||||||
|
"homepage": "http://mnt.io/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
|
||||||
|
"homepage": "http://sabre.io/vobject/",
|
||||||
|
"keywords": [
|
||||||
|
"availability",
|
||||||
|
"freebusy",
|
||||||
|
"iCalendar",
|
||||||
|
"ical",
|
||||||
|
"ics",
|
||||||
|
"jCal",
|
||||||
|
"jCard",
|
||||||
|
"recurrence",
|
||||||
|
"rfc2425",
|
||||||
|
"rfc2426",
|
||||||
|
"rfc2739",
|
||||||
|
"rfc4770",
|
||||||
|
"rfc5545",
|
||||||
|
"rfc5546",
|
||||||
|
"rfc6321",
|
||||||
|
"rfc6350",
|
||||||
|
"rfc6351",
|
||||||
|
"rfc6474",
|
||||||
|
"rfc6638",
|
||||||
|
"rfc6715",
|
||||||
|
"rfc6868",
|
||||||
|
"vCalendar",
|
||||||
|
"vCard",
|
||||||
|
"vcf",
|
||||||
|
"xCal",
|
||||||
|
"xCard"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/vobject/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-vobject"
|
||||||
|
},
|
||||||
|
"time": "2025-04-17T09:22:48+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabre/xml",
|
||||||
|
"version": "2.2.11",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/sabre-io/xml.git",
|
||||||
|
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
|
||||||
|
"reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-xmlreader": "*",
|
||||||
|
"ext-xmlwriter": "*",
|
||||||
|
"lib-libxml": ">=2.6.20",
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"sabre/uri": ">=1.0,<3.0.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "~2.17.1||3.63.2",
|
||||||
|
"phpstan/phpstan": "^0.12",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/Deserializer/functions.php",
|
||||||
|
"lib/Serializer/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sabre\\Xml\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Evert Pot",
|
||||||
|
"email": "me@evertpot.com",
|
||||||
|
"homepage": "http://evertpot.com/",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Markus Staab",
|
||||||
|
"email": "markus.staab@redaxo.de",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "sabre/xml is an XML library that you may not hate.",
|
||||||
|
"homepage": "https://sabre.io/xml/",
|
||||||
|
"keywords": [
|
||||||
|
"XMLReader",
|
||||||
|
"XMLWriter",
|
||||||
|
"dom",
|
||||||
|
"xml"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||||
|
"issues": "https://github.com/sabre-io/xml/issues",
|
||||||
|
"source": "https://github.com/fruux/sabre-xml"
|
||||||
|
},
|
||||||
|
"time": "2024-09-06T07:37:46+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ upload_tmp_dir=/tmp
|
|||||||
session.gc_maxlifetime=1440
|
session.gc_maxlifetime=1440
|
||||||
session.gc_probability=1
|
session.gc_probability=1
|
||||||
session.gc_divisor=100
|
session.gc_divisor=100
|
||||||
|
session.save_path = "/var/www/sessions"
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
; Error Handling / Logging
|
; Error Handling / Logging
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ DirectoryIndex index.html
|
|||||||
Require all denied
|
Require all denied
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
|
<FilesMatch "^(api\.html|openapi\.json)$">
|
||||||
|
Require valid-user
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Enforce HTTPS (optional)
|
# Enforce HTTPS (optional)
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
@@ -44,6 +44,55 @@ function showToast(msgKey) {
|
|||||||
}
|
}
|
||||||
window.showToast = showToast;
|
window.showToast = showToast;
|
||||||
|
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @param {string} url
|
||||||
|
* @param {object} options
|
||||||
|
* @returns {Promise<Response>}
|
||||||
|
*/
|
||||||
|
export async function fetchWithCsrf(url, options = {}) {
|
||||||
|
// 1) Merge in credentials + header
|
||||||
|
options = {
|
||||||
|
credentials: 'include',
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
options.headers = {
|
||||||
|
...(options.headers || {}),
|
||||||
|
'X-CSRF-Token': window.csrfToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2) First attempt
|
||||||
|
let res = await originalFetch(url, options);
|
||||||
|
|
||||||
|
// 3) If we got a 403, try to refresh token & retry
|
||||||
|
if (res.status === 403) {
|
||||||
|
// 3a) See if the server gave us a new token header
|
||||||
|
let newToken = res.headers.get('X-CSRF-Token');
|
||||||
|
// 3b) Otherwise fall back to the /api/auth/token endpoint
|
||||||
|
if (!newToken) {
|
||||||
|
const tokRes = await originalFetch('/api/auth/token.php', { credentials: 'include' });
|
||||||
|
if (tokRes.ok) {
|
||||||
|
const body = await tokRes.json();
|
||||||
|
newToken = body.csrf_token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newToken) {
|
||||||
|
// 3c) Update global + meta
|
||||||
|
window.csrfToken = newToken;
|
||||||
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||||
|
if (meta) meta.content = newToken;
|
||||||
|
|
||||||
|
// 3d) Retry the original request with the new token
|
||||||
|
options.headers['X-CSRF-Token'] = newToken;
|
||||||
|
res = await originalFetch(url, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Return the real Response—no body peeking here!
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows
|
// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows
|
||||||
function openTOTPLoginModal() {
|
function openTOTPLoginModal() {
|
||||||
originalOpenTOTPLoginModal();
|
originalOpenTOTPLoginModal();
|
||||||
@@ -228,6 +277,7 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
}
|
}
|
||||||
window.setupMode = false;
|
window.setupMode = false;
|
||||||
if (data.authenticated) {
|
if (data.authenticated) {
|
||||||
|
localStorage.setItem('isAdmin', data.isAdmin ? 'true' : 'false');
|
||||||
localStorage.setItem("folderOnly", data.folderOnly);
|
localStorage.setItem("folderOnly", data.folderOnly);
|
||||||
localStorage.setItem("readOnly", data.readOnly);
|
localStorage.setItem("readOnly", data.readOnly);
|
||||||
localStorage.setItem("disableUpload", data.disableUpload);
|
localStorage.setItem("disableUpload", data.disableUpload);
|
||||||
@@ -235,6 +285,10 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
if (typeof data.totp_enabled !== "undefined") {
|
if (typeof data.totp_enabled !== "undefined") {
|
||||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||||
}
|
}
|
||||||
|
if (data.csrf_token) {
|
||||||
|
window.csrfToken = data.csrf_token;
|
||||||
|
document.querySelector('meta[name="csrf-token"]').content = data.csrf_token;
|
||||||
|
}
|
||||||
updateAuthenticatedUI(data);
|
updateAuthenticatedUI(data);
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
@@ -276,11 +330,11 @@ async function submitLogin(data) {
|
|||||||
try {
|
try {
|
||||||
const perm = await sendRequest("/api/getUserPermissions.php", "GET");
|
const perm = await sendRequest("/api/getUserPermissions.php", "GET");
|
||||||
if (perm && typeof perm === "object") {
|
if (perm && typeof perm === "object") {
|
||||||
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
||||||
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
||||||
localStorage.setItem("disableUpload",perm.disableUpload? "true" : "false");
|
localStorage.setItem("disableUpload", perm.disableUpload ? "true" : "false");
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
return window.location.reload();
|
return window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,10 +459,10 @@ function initAuth() {
|
|||||||
}
|
}
|
||||||
let url = "/api/addUser.php";
|
let url = "/api/addUser.php";
|
||||||
if (window.setupMode) url += "?setup=1";
|
if (window.setupMode) url += "?setup=1";
|
||||||
fetch(url, {
|
fetchWithCsrf(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -438,10 +492,10 @@ function initAuth() {
|
|||||||
}
|
}
|
||||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
fetch("/api/removeUser.php", {
|
fetchWithCsrf("/api/removeUser.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username: usernameToRemove })
|
body: JSON.stringify({ username: usernameToRemove })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -477,10 +531,10 @@ function initAuth() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = { oldPassword, newPassword, confirmPassword };
|
const data = { oldPassword, newPassword, confirmPassword };
|
||||||
fetch("/api/changePassword.php", {
|
fetchWithCsrf("/api/changePassword.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { sendRequest } from './networkUtils.js';
|
|||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||||
import { loadAdminConfigFunc } from './auth.js';
|
import { loadAdminConfigFunc } from './auth.js';
|
||||||
|
|
||||||
const version = "v1.2.1"; // Update this version string as needed
|
const version = "v1.2.5"; // Update this version string as needed
|
||||||
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||||
|
|
||||||
let lastLoginData = null;
|
let lastLoginData = null;
|
||||||
@@ -230,14 +230,36 @@ export function openUserPanel() {
|
|||||||
|
|
||||||
<!-- New API Docs link -->
|
<!-- New API Docs link -->
|
||||||
<div style="margin-bottom: 15px;">
|
<div style="margin-bottom: 15px;">
|
||||||
<a href="api.html" target="_blank" class="btn btn-secondary">
|
<button type="button" id="openApiModalBtn" class="btn btn-secondary">
|
||||||
${t("api_docs") || "API Docs"}
|
${t("api_docs") || "API Docs"}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(userPanelModal);
|
document.body.appendChild(userPanelModal);
|
||||||
|
|
||||||
|
const apiModal = document.createElement("div");
|
||||||
|
apiModal.id = "apiModal";
|
||||||
|
apiModal.style.cssText = `
|
||||||
|
position: fixed; top:0; left:0; width:100vw; height:100vh;
|
||||||
|
background: rgba(0,0,0,0.8); z-index: 4000; display:none;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
`;
|
||||||
|
apiModal.innerHTML = `
|
||||||
|
<div style="position:relative; width:90vw; height:90vh; background:#fff; border-radius:8px; overflow:hidden;">
|
||||||
|
<div class="editor-close-btn" id="closeApiModal">×</div>
|
||||||
|
<iframe src="api.html" style="width:100%;height:100%;border:none;"></iframe>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(apiModal);
|
||||||
|
|
||||||
|
document.getElementById("openApiModalBtn").addEventListener("click", () => {
|
||||||
|
apiModal.style.display = "flex";
|
||||||
|
});
|
||||||
|
document.getElementById("closeApiModal").addEventListener("click", () => {
|
||||||
|
apiModal.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
// Handlers…
|
// Handlers…
|
||||||
document.getElementById("closeUserPanel").addEventListener("click", () => {
|
document.getElementById("closeUserPanel").addEventListener("click", () => {
|
||||||
userPanelModal.style.display = "none";
|
userPanelModal.style.display = "none";
|
||||||
@@ -246,6 +268,7 @@ export function openUserPanel() {
|
|||||||
document.getElementById("changePasswordModal").style.display = "block";
|
document.getElementById("changePasswordModal").style.display = "block";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// TOTP checkbox
|
// TOTP checkbox
|
||||||
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
const totpCheckbox = document.getElementById("userTOTPEnabled");
|
||||||
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
|
||||||
@@ -597,6 +620,7 @@ export function openAdminPanel() {
|
|||||||
}
|
}
|
||||||
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||||
|
|
||||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||||
const modalContentStyles = `
|
const modalContentStyles = `
|
||||||
@@ -611,6 +635,7 @@ export function openAdminPanel() {
|
|||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let adminModal = document.getElementById("adminPanelModal");
|
let adminModal = document.getElementById("adminPanelModal");
|
||||||
|
|
||||||
if (!adminModal) {
|
if (!adminModal) {
|
||||||
@@ -663,6 +688,28 @@ export function openAdminPanel() {
|
|||||||
<label for="disableOIDCLogin">${t("disable_oidc_login")}</label>
|
<label for="disableOIDCLogin">${t("disable_oidc_login")}</label>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- New WebDAV setting -->
|
||||||
|
<fieldset style="margin-bottom: 15px;">
|
||||||
|
<legend>WebDAV Access</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="enableWebDAV" />
|
||||||
|
<label for="enableWebDAV">Enable WebDAV</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<!-- End WebDAV setting -->
|
||||||
|
|
||||||
|
<!-- New Shared Max Upload Size setting -->
|
||||||
|
<fieldset style="margin-bottom: 15px;">
|
||||||
|
<legend>Shared Max Upload Size (bytes)</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="number" id="sharedMaxUploadSize" class="form-control"
|
||||||
|
placeholder="e.g. 52428800" />
|
||||||
|
<small>Enter maximum bytes allowed for shared-folder uploads</small>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<!-- End Shared Max Upload Size setting -->
|
||||||
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
<fieldset style="margin-bottom: 15px;">
|
||||||
<legend>${t("oidc_configuration")}</legend>
|
<legend>${t("oidc_configuration")}</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -698,33 +745,34 @@ export function openAdminPanel() {
|
|||||||
`;
|
`;
|
||||||
document.body.appendChild(adminModal);
|
document.body.appendChild(adminModal);
|
||||||
|
|
||||||
// Bind closing events that will use our enhanced close function.
|
// Bind closing
|
||||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||||
adminModal.addEventListener("click", (e) => {
|
adminModal.addEventListener("click", e => { if (e.target === adminModal) closeAdminPanel(); });
|
||||||
if (e.target === adminModal) closeAdminPanel();
|
|
||||||
});
|
|
||||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||||
|
|
||||||
// Bind other buttons.
|
// Bind other buttons
|
||||||
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
document.getElementById("adminOpenAddUser").addEventListener("click", () => {
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById("newUsername").focus();
|
document.getElementById("newUsername").focus();
|
||||||
});
|
});
|
||||||
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
document.getElementById("adminOpenRemoveUser").addEventListener("click", () => {
|
||||||
if (typeof window.loadUserList === "function") {
|
if (typeof window.loadUserList === "function") window.loadUserList();
|
||||||
window.loadUserList();
|
|
||||||
}
|
|
||||||
toggleVisibility("removeUserModal", true);
|
toggleVisibility("removeUserModal", true);
|
||||||
});
|
});
|
||||||
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
document.getElementById("adminOpenUserPermissions").addEventListener("click", () => {
|
||||||
openUserPermissionsModal();
|
openUserPermissionsModal();
|
||||||
});
|
});
|
||||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
|
||||||
|
|
||||||
|
// Save handler
|
||||||
|
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
||||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
const enableWebDAVCheckbox = document.getElementById("enableWebDAV");
|
||||||
|
const sharedMaxUploadSizeInput = document.getElementById("sharedMaxUploadSize");
|
||||||
|
|
||||||
|
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
||||||
|
.filter(cb => cb.checked).length;
|
||||||
if (totalDisabled === 3) {
|
if (totalDisabled === 3) {
|
||||||
showToast(t("at_least_one_login_method"));
|
showToast(t("at_least_one_login_method"));
|
||||||
disableOIDCLoginCheckbox.checked = false;
|
disableOIDCLoginCheckbox.checked = false;
|
||||||
@@ -738,8 +786,8 @@ export function openAdminPanel() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
|
|
||||||
|
|
||||||
|
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
|
||||||
const newOIDCConfig = {
|
const newOIDCConfig = {
|
||||||
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||||
clientId: document.getElementById("oidcClientId").value.trim(),
|
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||||
@@ -749,13 +797,18 @@ export function openAdminPanel() {
|
|||||||
const disableFormLogin = disableFormLoginCheckbox.checked;
|
const disableFormLogin = disableFormLoginCheckbox.checked;
|
||||||
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
||||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
||||||
|
const enableWebDAV = enableWebDAVCheckbox.checked;
|
||||||
|
const sharedMaxUploadSize = parseInt(sharedMaxUploadSizeInput.value, 10) || 0;
|
||||||
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
||||||
|
|
||||||
sendRequest("/api/admin/updateConfig.php", "POST", {
|
sendRequest("/api/admin/updateConfig.php", "POST", {
|
||||||
header_title: newHeaderTitle,
|
header_title: newHeaderTitle,
|
||||||
oidc: newOIDCConfig,
|
oidc: newOIDCConfig,
|
||||||
disableFormLogin,
|
disableFormLogin,
|
||||||
disableBasicAuth,
|
disableBasicAuth,
|
||||||
disableOIDCLogin,
|
disableOIDCLogin,
|
||||||
|
enableWebDAV,
|
||||||
|
sharedMaxUploadSize,
|
||||||
globalOtpauthUrl
|
globalOtpauthUrl
|
||||||
}, { "X-CSRF-Token": window.csrfToken })
|
}, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -764,26 +817,32 @@ export function openAdminPanel() {
|
|||||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||||
|
localStorage.setItem("enableWebDAV", enableWebDAV);
|
||||||
|
localStorage.setItem("sharedMaxUploadSize", sharedMaxUploadSize);
|
||||||
if (typeof window.updateLoginOptionsUI === "function") {
|
if (typeof window.updateLoginOptionsUI === "function") {
|
||||||
window.updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
window.updateLoginOptionsUI({
|
||||||
|
disableFormLogin,
|
||||||
|
disableBasicAuth,
|
||||||
|
disableOIDCLogin
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Update the captured initial state since the changes have now been saved.
|
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
closeAdminPanel();
|
closeAdminPanel();
|
||||||
loadAdminConfigFunc();
|
loadAdminConfigFunc();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
|
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => { });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enforce login option constraints.
|
// Enforce login option constraints.
|
||||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||||
function enforceLoginOptionConstraint(changedCheckbox) {
|
function enforceLoginOptionConstraint(changedCheckbox) {
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox].filter(cb => cb.checked).length;
|
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
||||||
|
.filter(cb => cb.checked).length;
|
||||||
if (changedCheckbox.checked && totalDisabled === 3) {
|
if (changedCheckbox.checked && totalDisabled === 3) {
|
||||||
showToast(t("at_least_one_login_method"));
|
showToast(t("at_least_one_login_method"));
|
||||||
changedCheckbox.checked = false;
|
changedCheckbox.checked = false;
|
||||||
@@ -793,13 +852,17 @@ export function openAdminPanel() {
|
|||||||
disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||||
disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
||||||
|
|
||||||
|
// Initial checkbox and input states
|
||||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||||
|
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||||
|
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||||
|
|
||||||
// Capture initial state after the modal loads.
|
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
// Update existing modal and show
|
||||||
adminModal.style.backgroundColor = overlayBackground;
|
adminModal.style.backgroundColor = overlayBackground;
|
||||||
const modalContent = adminModal.querySelector(".modal-content");
|
const modalContent = adminModal.querySelector(".modal-content");
|
||||||
if (modalContent) {
|
if (modalContent) {
|
||||||
@@ -815,6 +878,8 @@ export function openAdminPanel() {
|
|||||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||||
|
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||||
|
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||||
adminModal.style.display = "flex";
|
adminModal.style.display = "flex";
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
}
|
}
|
||||||
@@ -837,6 +902,8 @@ export function openAdminPanel() {
|
|||||||
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
||||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
||||||
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
||||||
|
document.getElementById("enableWebDAV").checked = localStorage.getItem("enableWebDAV") === "true";
|
||||||
|
document.getElementById("sharedMaxUploadSize").value = localStorage.getItem("sharedMaxUploadSize") || "";
|
||||||
adminModal.style.display = "flex";
|
adminModal.style.display = "flex";
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -634,7 +634,7 @@ function updateSliderConstraints() {
|
|||||||
|
|
||||||
// Set maximum based on screen size.
|
// Set maximum based on screen size.
|
||||||
if (width < 600) { // small devices (phones)
|
if (width < 600) { // small devices (phones)
|
||||||
max = 2;
|
max = 1;
|
||||||
} else if (width < 1024) { // medium devices
|
} else if (width < 1024) { // medium devices
|
||||||
max = 3;
|
max = 3;
|
||||||
} else if (width < 1440) { // between medium and large devices
|
} else if (width < 1440) { // between medium and large devices
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { loadFileList } from './fileListView.js';
|
|||||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
import { openFolderShareModal } from './folderShareModal.js';
|
import { openFolderShareModal } from './folderShareModal.js';
|
||||||
|
import { fetchWithCsrf } from './auth.js';
|
||||||
|
import { loadCsrfToken } from './main.js';
|
||||||
|
|
||||||
/* ----------------------
|
/* ----------------------
|
||||||
Helper Functions (Data/State)
|
Helper Functions (Data/State)
|
||||||
@@ -102,24 +104,26 @@ export function setupBreadcrumbDelegation() {
|
|||||||
|
|
||||||
// Click handler via delegation
|
// Click handler via delegation
|
||||||
function breadcrumbClickHandler(e) {
|
function breadcrumbClickHandler(e) {
|
||||||
|
// find the nearest .breadcrumb-link
|
||||||
const link = e.target.closest(".breadcrumb-link");
|
const link = e.target.closest(".breadcrumb-link");
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const folder = link.getAttribute("data-folder");
|
const folder = link.dataset.folder;
|
||||||
window.currentFolder = folder;
|
window.currentFolder = folder;
|
||||||
localStorage.setItem("lastOpenedFolder", folder);
|
localStorage.setItem("lastOpenedFolder", folder);
|
||||||
|
|
||||||
// Update the container with sanitized breadcrumbs.
|
// rebuild the title safely
|
||||||
const container = document.getElementById("fileListTitle");
|
updateBreadcrumbTitle(folder);
|
||||||
const sanitizedBreadcrumb = DOMPurify.sanitize(renderBreadcrumb(folder));
|
|
||||||
container.innerHTML = t("files_in") + " (" + sanitizedBreadcrumb + ")";
|
|
||||||
|
|
||||||
expandTreePath(folder);
|
expandTreePath(folder);
|
||||||
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
document.querySelectorAll(".folder-option").forEach(el =>
|
||||||
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
el.classList.remove("selected")
|
||||||
if (targetOption) targetOption.classList.add("selected");
|
);
|
||||||
|
const target = document.querySelector(`.folder-option[data-folder="${folder}"]`);
|
||||||
|
if (target) target.classList.add("selected");
|
||||||
|
|
||||||
loadFileList(folder);
|
loadFileList(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,11 +337,43 @@ function folderDropHandler(event) {
|
|||||||
/* ----------------------
|
/* ----------------------
|
||||||
Main Folder Tree Rendering and Event Binding
|
Main Folder Tree Rendering and Event Binding
|
||||||
----------------------*/
|
----------------------*/
|
||||||
|
// --- Helpers for safe breadcrumb rendering ---
|
||||||
|
function renderBreadcrumbFragment(folderPath) {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const parts = folderPath.split("/");
|
||||||
|
let acc = "";
|
||||||
|
|
||||||
|
parts.forEach((part, idx) => {
|
||||||
|
acc = idx === 0 ? part : acc + "/" + part;
|
||||||
|
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.classList.add("breadcrumb-link");
|
||||||
|
span.dataset.folder = acc;
|
||||||
|
span.textContent = part;
|
||||||
|
frag.appendChild(span);
|
||||||
|
|
||||||
|
if (idx < parts.length - 1) {
|
||||||
|
frag.appendChild(document.createTextNode(" / "));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBreadcrumbTitle(folder) {
|
||||||
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
|
titleEl.textContent = "";
|
||||||
|
titleEl.appendChild(document.createTextNode(t("files_in") + " ("));
|
||||||
|
titleEl.appendChild(renderBreadcrumbFragment(folder));
|
||||||
|
titleEl.appendChild(document.createTextNode(")"));
|
||||||
|
setupBreadcrumbDelegation();
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadFolderTree(selectedFolder) {
|
export async function loadFolderTree(selectedFolder) {
|
||||||
try {
|
try {
|
||||||
// Check if the user has folder-only permission.
|
// Check if the user has folder-only permission.
|
||||||
await checkUserFolderPermission();
|
await checkUserFolderPermission();
|
||||||
|
|
||||||
// Determine effective root folder.
|
// Determine effective root folder.
|
||||||
const username = localStorage.getItem("username") || "root";
|
const username = localStorage.getItem("username") || "root";
|
||||||
let effectiveRoot = "root";
|
let effectiveRoot = "root";
|
||||||
@@ -351,14 +387,14 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
} else {
|
} else {
|
||||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build fetch URL.
|
// Build fetch URL.
|
||||||
let fetchUrl = '/api/folder/getFolderList.php';
|
let fetchUrl = '/api/folder/getFolderList.php';
|
||||||
if (window.userFolderOnly) {
|
if (window.userFolderOnly) {
|
||||||
fetchUrl += '?restricted=1';
|
fetchUrl += '?restricted=1';
|
||||||
}
|
}
|
||||||
console.log("Fetching folder list from:", fetchUrl);
|
console.log("Fetching folder list from:", fetchUrl);
|
||||||
|
|
||||||
// Fetch folder list from the server.
|
// Fetch folder list from the server.
|
||||||
const response = await fetch(fetchUrl);
|
const response = await fetch(fetchUrl);
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
@@ -375,10 +411,10 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
} else if (Array.isArray(folderData)) {
|
} else if (Array.isArray(folderData)) {
|
||||||
folders = folderData;
|
folders = folderData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any global "root" entry.
|
// Remove any global "root" entry.
|
||||||
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
||||||
|
|
||||||
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
||||||
if (window.userFolderOnly && effectiveRoot !== "root") {
|
if (window.userFolderOnly && effectiveRoot !== "root") {
|
||||||
folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/"));
|
folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/"));
|
||||||
@@ -386,16 +422,16 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
localStorage.setItem("lastOpenedFolder", effectiveRoot);
|
localStorage.setItem("lastOpenedFolder", effectiveRoot);
|
||||||
window.currentFolder = effectiveRoot;
|
window.currentFolder = effectiveRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||||
|
|
||||||
// Render the folder tree.
|
// Render the folder tree.
|
||||||
const container = document.getElementById("folderTreeContainer");
|
const container = document.getElementById("folderTreeContainer");
|
||||||
if (!container) {
|
if (!container) {
|
||||||
console.error("Folder tree container not found.");
|
console.error("Folder tree container not found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = `<div id="rootRow" class="root-row">
|
let html = `<div id="rootRow" class="root-row">
|
||||||
<span class="folder-toggle" data-folder="${effectiveRoot}">[<span class="custom-dash">-</span>]</span>
|
<span class="folder-toggle" data-folder="${effectiveRoot}">[<span class="custom-dash">-</span>]</span>
|
||||||
<span class="folder-option root-folder-option" data-folder="${effectiveRoot}">${effectiveLabel}</span>
|
<span class="folder-option root-folder-option" data-folder="${effectiveRoot}">${effectiveLabel}</span>
|
||||||
@@ -405,35 +441,35 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
html += renderFolderTree(tree, "", "block");
|
html += renderFolderTree(tree, "", "block");
|
||||||
}
|
}
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
// Attach drag/drop event listeners.
|
// Attach drag/drop event listeners.
|
||||||
container.querySelectorAll(".folder-option").forEach(el => {
|
container.querySelectorAll(".folder-option").forEach(el => {
|
||||||
el.addEventListener("dragover", folderDragOverHandler);
|
el.addEventListener("dragover", folderDragOverHandler);
|
||||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||||
el.addEventListener("drop", folderDropHandler);
|
el.addEventListener("drop", folderDropHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedFolder) {
|
if (selectedFolder) {
|
||||||
window.currentFolder = selectedFolder;
|
window.currentFolder = selectedFolder;
|
||||||
}
|
}
|
||||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||||
|
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
// Initial breadcrumb update
|
||||||
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")";
|
updateBreadcrumbTitle(window.currentFolder);
|
||||||
setupBreadcrumbDelegation();
|
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
|
|
||||||
const folderState = loadFolderTreeState();
|
const folderState = loadFolderTreeState();
|
||||||
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
||||||
expandTreePath(window.currentFolder);
|
expandTreePath(window.currentFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||||
if (selectedEl) {
|
if (selectedEl) {
|
||||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||||
selectedEl.classList.add("selected");
|
selectedEl.classList.add("selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Folder-option click: update selection, breadcrumbs, and file list
|
||||||
container.querySelectorAll(".folder-option").forEach(el => {
|
container.querySelectorAll(".folder-option").forEach(el => {
|
||||||
el.addEventListener("click", function (e) {
|
el.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -442,13 +478,14 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
const selected = this.getAttribute("data-folder");
|
const selected = this.getAttribute("data-folder");
|
||||||
window.currentFolder = selected;
|
window.currentFolder = selected;
|
||||||
localStorage.setItem("lastOpenedFolder", selected);
|
localStorage.setItem("lastOpenedFolder", selected);
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
|
||||||
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(selected) + ")";
|
// Safe breadcrumb update
|
||||||
setupBreadcrumbDelegation();
|
updateBreadcrumbTitle(selected);
|
||||||
loadFileList(selected);
|
loadFileList(selected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Root toggle handler
|
||||||
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
||||||
if (rootToggle) {
|
if (rootToggle) {
|
||||||
rootToggle.addEventListener("click", function (e) {
|
rootToggle.addEventListener("click", function (e) {
|
||||||
@@ -471,7 +508,8 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Other folder-toggle handlers
|
||||||
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
||||||
toggle.addEventListener("click", function (e) {
|
toggle.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -494,12 +532,13 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading folder tree:", error);
|
console.error("Error loading folder tree:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// For backward compatibility.
|
// For backward compatibility.
|
||||||
export function loadFolderList(selectedFolder) {
|
export function loadFolderList(selectedFolder) {
|
||||||
loadFolderTree(selectedFolder);
|
loadFolderTree(selectedFolder);
|
||||||
@@ -627,45 +666,53 @@ document.getElementById("cancelCreateFolder").addEventListener("click", function
|
|||||||
document.getElementById("newFolderName").value = "";
|
document.getElementById("newFolderName").value = "";
|
||||||
});
|
});
|
||||||
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
||||||
document.getElementById("submitCreateFolder").addEventListener("click", function () {
|
document.getElementById("submitCreateFolder").addEventListener("click", async () => {
|
||||||
const folderInput = document.getElementById("newFolderName").value.trim();
|
const folderInput = document.getElementById("newFolderName").value.trim();
|
||||||
if (!folderInput) {
|
if (!folderInput) return showToast("Please enter a folder name.");
|
||||||
showToast("Please enter a folder name.");
|
|
||||||
return;
|
const selectedFolder = window.currentFolder || "root";
|
||||||
|
const parent = selectedFolder === "root" ? "" : selectedFolder;
|
||||||
|
|
||||||
|
// 1) Guarantee fresh CSRF
|
||||||
|
try {
|
||||||
|
await loadCsrfToken();
|
||||||
|
} catch {
|
||||||
|
return showToast("Could not refresh CSRF token. Please reload.");
|
||||||
}
|
}
|
||||||
let selectedFolder = window.currentFolder || "root";
|
|
||||||
let fullFolderName = folderInput;
|
// 2) Call with fetchWithCsrf
|
||||||
if (selectedFolder && selectedFolder !== "root") {
|
fetchWithCsrf("/api/folder/createFolder.php", {
|
||||||
fullFolderName = selectedFolder + "/" + folderInput;
|
|
||||||
}
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
||||||
fetch("/api/folder/createFolder.php", {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify({ folderName: folderInput, parent })
|
||||||
"X-CSRF-Token": csrfToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
folderName: folderInput,
|
|
||||||
parent: selectedFolder === "root" ? "" : selectedFolder
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(async res => {
|
||||||
.then(data => {
|
if (!res.ok) {
|
||||||
if (data.success) {
|
// pull out a JSON error, or fallback to status text
|
||||||
showToast("Folder created successfully!");
|
let err;
|
||||||
window.currentFolder = fullFolderName;
|
try {
|
||||||
localStorage.setItem("lastOpenedFolder", fullFolderName);
|
const j = await res.json();
|
||||||
loadFolderList(fullFolderName);
|
err = j.error || j.message || res.statusText;
|
||||||
} else {
|
} catch {
|
||||||
showToast("Error: " + (data.error || "Could not create folder"));
|
err = res.statusText;
|
||||||
|
}
|
||||||
|
throw new Error(err);
|
||||||
}
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
showToast("Folder created!");
|
||||||
|
const full = parent ? `${parent}/${folderInput}` : folderInput;
|
||||||
|
window.currentFolder = full;
|
||||||
|
localStorage.setItem("lastOpenedFolder", full);
|
||||||
|
loadFolderList(full);
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
showToast("Error creating folder: " + e.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
document.getElementById("createFolderModal").style.display = "none";
|
document.getElementById("createFolderModal").style.display = "none";
|
||||||
document.getElementById("newFolderName").value = "";
|
document.getElementById("newFolderName").value = "";
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error("Error creating folder:", error);
|
|
||||||
document.getElementById("createFolderModal").style.display = "none";
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { sendRequest } from './networkUtils.js';
|
import { sendRequest } from './networkUtils.js';
|
||||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
||||||
import { loadFolderTree } from './folderManager.js';
|
|
||||||
import { initUpload } from './upload.js';
|
import { initUpload } from './upload.js';
|
||||||
import { initAuth, checkAuthentication, loadAdminConfigFunc } from './auth.js';
|
import { initAuth, fetchWithCsrf, checkAuthentication, loadAdminConfigFunc } from './auth.js';
|
||||||
|
const _originalFetch = window.fetch;
|
||||||
|
window.fetch = fetchWithCsrf;
|
||||||
|
import { loadFolderTree } from './folderManager.js';
|
||||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||||
@@ -12,36 +14,37 @@ import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload }
|
|||||||
import { editFile, saveFile } from './fileEditor.js';
|
import { editFile, saveFile } from './fileEditor.js';
|
||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||||
|
|
||||||
// Remove the retry logic version and just use loadCsrfToken directly:
|
|
||||||
function loadCsrfToken() {
|
export function loadCsrfToken() {
|
||||||
return fetch('/api/auth/token.php', { credentials: 'include' })
|
return fetchWithCsrf('/api/auth/token.php', {
|
||||||
.then(response => {
|
method: 'GET'
|
||||||
if (!response.ok) {
|
})
|
||||||
throw new Error("Token fetch failed with status: " + response.status);
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Token fetch failed with status ${res.status}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(({ csrf_token, share_url }) => {
|
||||||
window.csrfToken = data.csrf_token;
|
// Update global and <meta>
|
||||||
window.SHARE_URL = data.share_url;
|
window.csrfToken = csrf_token;
|
||||||
|
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
if (!meta) {
|
||||||
if (!metaCSRF) {
|
meta = document.createElement('meta');
|
||||||
metaCSRF = document.createElement('meta');
|
meta.name = 'csrf-token';
|
||||||
metaCSRF.name = 'csrf-token';
|
document.head.appendChild(meta);
|
||||||
document.head.appendChild(metaCSRF);
|
|
||||||
}
|
}
|
||||||
metaCSRF.setAttribute('content', data.csrf_token);
|
meta.content = csrf_token;
|
||||||
|
|
||||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||||||
if (!metaShare) {
|
if (!shareMeta) {
|
||||||
metaShare = document.createElement('meta');
|
shareMeta = document.createElement('meta');
|
||||||
metaShare.name = 'share-url';
|
shareMeta.name = 'share-url';
|
||||||
document.head.appendChild(metaShare);
|
document.head.appendChild(shareMeta);
|
||||||
}
|
}
|
||||||
metaShare.setAttribute('content', data.share_url);
|
shareMeta.content = share_url;
|
||||||
|
|
||||||
return data;
|
return { csrf_token, share_url };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -412,7 +412,12 @@ function initResumableUpload() {
|
|||||||
forceChunkSize: true,
|
forceChunkSize: true,
|
||||||
testChunks: false,
|
testChunks: false,
|
||||||
throttleProgressCallbacks: 1,
|
throttleProgressCallbacks: 1,
|
||||||
headers: { "X-CSRF-Token": window.csrfToken }
|
withCredentials: true,
|
||||||
|
headers: { 'X-CSRF-Token': window.csrfToken },
|
||||||
|
query: {
|
||||||
|
folder: window.currentFolder || "root",
|
||||||
|
upload_token: window.csrfToken // still as a fallback
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileInput = document.getElementById("file");
|
const fileInput = document.getElementById("file");
|
||||||
@@ -496,26 +501,40 @@ function initResumableUpload() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
resumableInstance.on("fileSuccess", function(file, message) {
|
resumableInstance.on("fileSuccess", function(file, message) {
|
||||||
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
|
// Try to parse JSON response
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(message);
|
||||||
|
} catch (e) {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Soft‐fail CSRF? then update token & retry this file
|
||||||
|
if (data && data.csrf_expired) {
|
||||||
|
// Update global and Resumable headers
|
||||||
|
window.csrfToken = data.csrf_token;
|
||||||
|
resumableInstance.opts.headers['X-CSRF-Token'] = data.csrf_token;
|
||||||
|
resumableInstance.opts.query.upload_token = data.csrf_token;
|
||||||
|
// Retry this chunk/file
|
||||||
|
file.retry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Otherwise treat as real success:
|
||||||
|
const li = document.querySelector(
|
||||||
|
`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`
|
||||||
|
);
|
||||||
if (li && li.progressBar) {
|
if (li && li.progressBar) {
|
||||||
li.progressBar.style.width = "100%";
|
li.progressBar.style.width = "100%";
|
||||||
li.progressBar.innerText = "Done";
|
li.progressBar.innerText = "Done";
|
||||||
// Hide pause/resume and remove buttons for successful files.
|
// remove action buttons
|
||||||
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
||||||
if (pauseResumeBtn) {
|
if (pauseResumeBtn) pauseResumeBtn.style.display = "none";
|
||||||
pauseResumeBtn.style.display = "none";
|
|
||||||
}
|
|
||||||
const removeBtn = li.querySelector(".remove-file-btn");
|
const removeBtn = li.querySelector(".remove-file-btn");
|
||||||
if (removeBtn) {
|
if (removeBtn) removeBtn.style.display = "none";
|
||||||
removeBtn.style.display = "none";
|
setTimeout(() => li.remove(), 5000);
|
||||||
}
|
|
||||||
// Schedule removal of the file entry after 5 seconds.
|
|
||||||
setTimeout(() => {
|
|
||||||
li.remove();
|
|
||||||
window.selectedFiles = window.selectedFiles.filter(f => f.uniqueIdentifier !== file.uniqueIdentifier);
|
|
||||||
updateFileInfoCount();
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -618,8 +637,25 @@ function submitFiles(allFiles) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
jsonResponse = null;
|
jsonResponse = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Soft-fail CSRF: retry this upload ───────────────────────
|
||||||
|
if (jsonResponse && jsonResponse.csrf_expired) {
|
||||||
|
console.warn("CSRF expired during upload, retrying chunk", file.uploadIndex);
|
||||||
|
// 1) update global token + header
|
||||||
|
window.csrfToken = jsonResponse.csrf_token;
|
||||||
|
xhr.open("POST", "/api/upload/upload.php", true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||||
|
// 2) re-send the same formData
|
||||||
|
xhr.send(formData);
|
||||||
|
return; // skip the "finishedCount++" and error/success logic for now
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Normal success/error handling ────────────────────────────
|
||||||
const li = progressElements[file.uploadIndex];
|
const li = progressElements[file.uploadIndex];
|
||||||
|
|
||||||
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
||||||
|
// real success
|
||||||
if (li) {
|
if (li) {
|
||||||
li.progressBar.style.width = "100%";
|
li.progressBar.style.width = "100%";
|
||||||
li.progressBar.innerText = "Done";
|
li.progressBar.innerText = "Done";
|
||||||
@@ -627,11 +663,14 @@ function submitFiles(allFiles) {
|
|||||||
}
|
}
|
||||||
uploadResults[file.uploadIndex] = true;
|
uploadResults[file.uploadIndex] = true;
|
||||||
} else {
|
} else {
|
||||||
|
// real failure
|
||||||
if (li) {
|
if (li) {
|
||||||
li.progressBar.innerText = "Error";
|
li.progressBar.innerText = "Error";
|
||||||
}
|
}
|
||||||
allSucceeded = false;
|
allSucceeded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Only now count this chunk as finished ───────────────────
|
||||||
finishedCount++;
|
finishedCount++;
|
||||||
if (finishedCount === allFiles.length) {
|
if (finishedCount === allFiles.length) {
|
||||||
refreshFileList(allFiles, uploadResults, progressElements);
|
refreshFileList(allFiles, uploadResults, progressElements);
|
||||||
@@ -665,6 +704,7 @@ function submitFiles(allFiles) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
xhr.open("POST", "/api/upload/upload.php", true);
|
xhr.open("POST", "/api/upload/upload.php", true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
});
|
});
|
||||||
|
|||||||
74
public/webdav.php
Normal file
74
public/webdav.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
// public/webdav.php
|
||||||
|
|
||||||
|
// ─── 0) Forward Basic auth into PHP_AUTH_* for every HTTP verb ─────────────
|
||||||
|
if (
|
||||||
|
empty($_SERVER['PHP_AUTH_USER'])
|
||||||
|
&& !empty($_SERVER['HTTP_AUTHORIZATION'])
|
||||||
|
&& preg_match('#Basic\s+(.*)$#i', $_SERVER['HTTP_AUTHORIZATION'], $m)
|
||||||
|
) {
|
||||||
|
[$u, $p] = explode(':', base64_decode($m[1]), 2) + ['', ''];
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = $u;
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = $p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 1) Bootstrap & load models ─────────────────────────────────────────────
|
||||||
|
require_once __DIR__ . '/../config/config.php'; // UPLOAD_DIR, META_DIR, DATE_TIME_FORMAT
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php'; // Composer & SabreDAV
|
||||||
|
require_once __DIR__ . '/../src/models/AuthModel.php'; // AuthModel::authenticate(), getUserRole(), loadFolderPermission()
|
||||||
|
require_once __DIR__ . '/../src/models/AdminModel.php'; // AdminModel::getConfig()
|
||||||
|
|
||||||
|
// ─── 1.1) Global WebDAV feature toggle ──────────────────────────────────────
|
||||||
|
$adminConfig = AdminModel::getConfig();
|
||||||
|
$enableWebDAV = isset($adminConfig['enableWebDAV']) && $adminConfig['enableWebDAV'];
|
||||||
|
if (!$enableWebDAV) {
|
||||||
|
header('HTTP/1.1 403 Forbidden');
|
||||||
|
echo 'WebDAV access is currently disabled by administrator.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 2) Load WebDAV directory implementation ──────────────────────────
|
||||||
|
require_once __DIR__ . '/../src/webdav/FileRiseDirectory.php';
|
||||||
|
use Sabre\DAV\Server;
|
||||||
|
use Sabre\DAV\Auth\Backend\BasicCallBack;
|
||||||
|
use Sabre\DAV\Auth\Plugin as AuthPlugin;
|
||||||
|
use Sabre\DAV\Locks\Plugin as LocksPlugin;
|
||||||
|
use Sabre\DAV\Locks\Backend\File as LocksFileBackend;
|
||||||
|
use FileRise\WebDAV\FileRiseDirectory;
|
||||||
|
|
||||||
|
// ─── 3) HTTP‑Basic backend ─────────────────────────────────────────────────
|
||||||
|
$authBackend = new BasicCallBack(function(string $user, string $pass) {
|
||||||
|
return \AuthModel::authenticate($user, $pass) !== false;
|
||||||
|
});
|
||||||
|
$authPlugin = new AuthPlugin($authBackend, 'FileRise');
|
||||||
|
|
||||||
|
// ─── 4) Determine user scope ────────────────────────────────────────────────
|
||||||
|
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
|
||||||
|
$isAdmin = (\AuthModel::getUserRole($user) === '1');
|
||||||
|
$folderOnly = (bool)\AuthModel::loadFolderPermission($user);
|
||||||
|
|
||||||
|
if ($isAdmin || !$folderOnly) {
|
||||||
|
// Admins (or users without folder-only restriction) see the full /uploads
|
||||||
|
$rootPath = rtrim(UPLOAD_DIR, '/\\');
|
||||||
|
} else {
|
||||||
|
// Folder‑only users see only /uploads/{username}
|
||||||
|
$rootPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $user;
|
||||||
|
if (!is_dir($rootPath)) {
|
||||||
|
mkdir($rootPath, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 5) Spin up SabreDAV ────────────────────────────────────────────────────
|
||||||
|
$server = new Server([
|
||||||
|
new FileRiseDirectory($rootPath, $user, $folderOnly),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$server->addPlugin($authPlugin);
|
||||||
|
$server->addPlugin(
|
||||||
|
new LocksPlugin(
|
||||||
|
new LocksFileBackend(sys_get_temp_dir() . '/sabre-locksdb')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$server->setBaseUri('/webdav.php/');
|
||||||
|
$server->exec();
|
||||||
@@ -35,7 +35,9 @@ class AdminController
|
|||||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||||
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
||||||
* ),
|
* ),
|
||||||
* @OA\Property(property="globalOtpauthUrl", type="string", example="")
|
* @OA\Property(property="globalOtpauthUrl", type="string", example=""),
|
||||||
|
* @OA\Property(property="enableWebDAV", type="boolean", example=false),
|
||||||
|
* @OA\Property(property="sharedMaxUploadSize", type="integer", example=52428800)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
@@ -88,7 +90,9 @@ class AdminController
|
|||||||
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
* @OA\Property(property="disableBasicAuth", type="boolean", example=false),
|
||||||
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
* @OA\Property(property="disableOIDCLogin", type="boolean", example=false)
|
||||||
* ),
|
* ),
|
||||||
* @OA\Property(property="globalOtpauthUrl", type="string", example="")
|
* @OA\Property(property="globalOtpauthUrl", type="string", example=""),
|
||||||
|
* @OA\Property(property="enableWebDAV", type="boolean", example=false),
|
||||||
|
* @OA\Property(property="sharedMaxUploadSize", type="integer", example=52428800)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
@@ -149,7 +153,7 @@ class AdminController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare configuration array.
|
// Prepare existing settings
|
||||||
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
||||||
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
||||||
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
||||||
@@ -183,20 +187,38 @@ class AdminController
|
|||||||
}
|
}
|
||||||
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
|
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
|
||||||
|
|
||||||
|
// ── NEW: enableWebDAV flag ──────────────────────────────────────
|
||||||
|
$enableWebDAV = false;
|
||||||
|
if (array_key_exists('enableWebDAV', $data)) {
|
||||||
|
$enableWebDAV = filter_var($data['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
|
||||||
|
} elseif (isset($data['features']['enableWebDAV'])) {
|
||||||
|
$enableWebDAV = filter_var($data['features']['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NEW: sharedMaxUploadSize ──────────────────────────────────────
|
||||||
|
$sharedMaxUploadSize = null;
|
||||||
|
if (array_key_exists('sharedMaxUploadSize', $data)) {
|
||||||
|
$sharedMaxUploadSize = filter_var($data['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
|
||||||
|
} elseif (isset($data['features']['sharedMaxUploadSize'])) {
|
||||||
|
$sharedMaxUploadSize = filter_var($data['features']['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
|
||||||
|
}
|
||||||
|
|
||||||
$configUpdate = [
|
$configUpdate = [
|
||||||
'header_title' => $headerTitle,
|
'header_title' => $headerTitle,
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
'providerUrl' => $oidcProviderUrl,
|
'providerUrl' => $oidcProviderUrl,
|
||||||
'clientId' => $oidcClientId,
|
'clientId' => $oidcClientId,
|
||||||
'clientSecret' => $oidcClientSecret,
|
'clientSecret' => $oidcClientSecret,
|
||||||
'redirectUri' => $oidcRedirectUri,
|
'redirectUri' => $oidcRedirectUri,
|
||||||
],
|
],
|
||||||
'loginOptions' => [
|
'loginOptions' => [
|
||||||
'disableFormLogin' => $disableFormLogin,
|
'disableFormLogin' => $disableFormLogin,
|
||||||
'disableBasicAuth' => $disableBasicAuth,
|
'disableBasicAuth' => $disableBasicAuth,
|
||||||
'disableOIDCLogin' => $disableOIDCLogin,
|
'disableOIDCLogin' => $disableOIDCLogin,
|
||||||
],
|
],
|
||||||
'globalOtpauthUrl' => $globalOtpauthUrl
|
'globalOtpauthUrl' => $globalOtpauthUrl,
|
||||||
|
'enableWebDAV' => $enableWebDAV,
|
||||||
|
'sharedMaxUploadSize' => $sharedMaxUploadSize // ← NEW
|
||||||
];
|
];
|
||||||
|
|
||||||
// Delegate to the model.
|
// Delegate to the model.
|
||||||
@@ -207,4 +229,4 @@ class AdminController
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,28 +238,28 @@ class AuthController
|
|||||||
$token = bin2hex(random_bytes(32));
|
$token = bin2hex(random_bytes(32));
|
||||||
$expiry = time() + 30 * 24 * 60 * 60;
|
$expiry = time() + 30 * 24 * 60 * 60;
|
||||||
$all = [];
|
$all = [];
|
||||||
|
|
||||||
if (file_exists($tokFile)) {
|
if (file_exists($tokFile)) {
|
||||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||||
$all = json_decode($dec, true) ?: [];
|
$all = json_decode($dec, true) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$all[$token] = [
|
$all[$token] = [
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'expiry' => $expiry,
|
'expiry' => $expiry,
|
||||||
'isAdmin' => $_SESSION['isAdmin']
|
'isAdmin' => $_SESSION['isAdmin']
|
||||||
];
|
];
|
||||||
|
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
$tokFile,
|
$tokFile,
|
||||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||||
LOCK_EX
|
LOCK_EX
|
||||||
);
|
);
|
||||||
|
|
||||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
|
|
||||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||||
|
|
||||||
setcookie(
|
setcookie(
|
||||||
session_name(),
|
session_name(),
|
||||||
session_id(),
|
session_id(),
|
||||||
@@ -269,7 +269,7 @@ class AuthController
|
|||||||
$secure,
|
$secure,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,40 +341,86 @@ class AuthController
|
|||||||
|
|
||||||
public function checkAuth(): void
|
public function checkAuth(): void
|
||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
// 1) Remember-me re-login
|
||||||
|
if (empty($_SESSION['authenticated']) && !empty($_COOKIE['remember_me_token'])) {
|
||||||
|
$payload = AuthModel::validateRememberToken($_COOKIE['remember_me_token']);
|
||||||
|
if ($payload) {
|
||||||
|
$old = $_SESSION['csrf_token'] ?? bin2hex(random_bytes(32));
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['csrf_token'] = $old;
|
||||||
|
$_SESSION['authenticated'] = true;
|
||||||
|
$_SESSION['username'] = $payload['username'];
|
||||||
|
$_SESSION['isAdmin'] = !empty($payload['isAdmin']);
|
||||||
|
$_SESSION['folderOnly'] = $payload['folderOnly'] ?? false;
|
||||||
|
$_SESSION['readOnly'] = $payload['readOnly'] ?? false;
|
||||||
|
$_SESSION['disableUpload'] = $payload['disableUpload'] ?? false;
|
||||||
|
// regenerate CSRF if you use one
|
||||||
|
|
||||||
|
|
||||||
|
// TOTP enabled? (same logic as below)
|
||||||
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
$totp = false;
|
||||||
|
if (file_exists($usersFile)) {
|
||||||
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
|
$parts = explode(':', trim($line));
|
||||||
|
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
||||||
|
$totp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'authenticated' => true,
|
||||||
|
'csrf_token' => $_SESSION['csrf_token'],
|
||||||
|
'isAdmin' => $_SESSION['isAdmin'],
|
||||||
|
'totp_enabled' => $totp,
|
||||||
|
'username' => $_SESSION['username'],
|
||||||
|
'folderOnly' => $_SESSION['folderOnly'],
|
||||||
|
'readOnly' => $_SESSION['readOnly'],
|
||||||
|
'disableUpload' => $_SESSION['disableUpload']
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
// setup mode?
|
// 2) Setup mode?
|
||||||
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
||||||
error_log("checkAuth: setup mode");
|
error_log("checkAuth: setup mode");
|
||||||
echo json_encode(['setup' => true]);
|
echo json_encode(['setup' => true]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3) Session-based auth
|
||||||
if (empty($_SESSION['authenticated'])) {
|
if (empty($_SESSION['authenticated'])) {
|
||||||
echo json_encode(['authenticated' => false]);
|
echo json_encode(['authenticated' => false]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOTP enabled?
|
// 4) TOTP enabled?
|
||||||
$totp = false;
|
$totp = false;
|
||||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
$parts = explode(':', trim($line));
|
$parts = explode(':', trim($line));
|
||||||
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
if ($parts[0] === ($_SESSION['username'] ?? '') && !empty($parts[3])) {
|
||||||
$totp = true;
|
$totp = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$isAdmin = ((int)AuthModel::getUserRole($_SESSION['username']) === 1);
|
// 5) Final response
|
||||||
$resp = [
|
$resp = [
|
||||||
'authenticated' => true,
|
'authenticated' => true,
|
||||||
'isAdmin' => $isAdmin,
|
'isAdmin' => !empty($_SESSION['isAdmin']),
|
||||||
'totp_enabled' => $totp,
|
'totp_enabled' => $totp,
|
||||||
'username' => $_SESSION['username'],
|
'username' => $_SESSION['username'],
|
||||||
'folderOnly' => $_SESSION['folderOnly'] ?? false,
|
'folderOnly' => $_SESSION['folderOnly'] ?? false,
|
||||||
'readOnly' => $_SESSION['readOnly'] ?? false,
|
'readOnly' => $_SESSION['readOnly'] ?? false,
|
||||||
'disableUpload' => $_SESSION['disableUpload'] ?? false
|
'disableUpload' => $_SESSION['disableUpload'] ?? false
|
||||||
];
|
];
|
||||||
|
|
||||||
echo json_encode($resp);
|
echo json_encode($resp);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -403,10 +449,19 @@ class AuthController
|
|||||||
*/
|
*/
|
||||||
public function getToken(): void
|
public function getToken(): void
|
||||||
{
|
{
|
||||||
|
// 1) Ensure session and CSRF token exist
|
||||||
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Emit headers
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
header('X-CSRF-Token: ' . $_SESSION['csrf_token']);
|
||||||
|
|
||||||
|
// 3) Return JSON payload
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"csrf_token" => $_SESSION['csrf_token'],
|
'csrf_token' => $_SESSION['csrf_token'],
|
||||||
"share_url" => SHARE_URL
|
'share_url' => SHARE_URL
|
||||||
]);
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -450,56 +450,57 @@ class FileController {
|
|||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// --- CSRF Protection ---
|
// --- CSRF Protection ---
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
$receivedToken = $headersArr['x-csrf-token'] ?? '';
|
||||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user is authenticated.
|
// --- Authentication Check ---
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is allowed to save files (not read-only).
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
// --- Read‑only check ---
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if ($username && !empty($userPermissions['readOnly'])) {
|
||||||
echo json_encode(["error" => "Read-only users are not allowed to save files."]);
|
echo json_encode(["error" => "Read-only users are not allowed to save files."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get JSON input.
|
// --- Input parsing ---
|
||||||
$data = json_decode(file_get_contents("php://input"), true);
|
$data = json_decode(file_get_contents("php://input"), true);
|
||||||
|
if (empty($data) || !isset($data["fileName"], $data["content"])) {
|
||||||
if (!$data) {
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "No data received"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($data["fileName"]) || !isset($data["content"])) {
|
|
||||||
echo json_encode(["error" => "Invalid request data", "received" => $data]);
|
echo json_encode(["error" => "Invalid request data", "received" => $data]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fileName = basename($data["fileName"]);
|
$fileName = basename($data["fileName"]);
|
||||||
// Determine the folder. Default to "root" if not provided.
|
$folder = isset($data["folder"]) ? trim($data["folder"]) : "root";
|
||||||
$folder = isset($data["folder"]) ? trim($data["folder"]) : "root";
|
|
||||||
|
|
||||||
// Validate folder if not root.
|
// --- Folder validation ---
|
||||||
if (strtolower($folder) !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if (strtolower($folder) !== "root" && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
echo json_encode(["error" => "Invalid folder name"]);
|
echo json_encode(["error" => "Invalid folder name"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$folder = trim($folder, "/\\ ");
|
$folder = trim($folder, "/\\ ");
|
||||||
|
|
||||||
// Delegate to the model.
|
// --- Delegate to model, passing the uploader ---
|
||||||
$result = FileModel::saveFile($folder, $fileName, $data["content"]);
|
// Make sure FileModel::saveFile signature is:
|
||||||
|
// saveFile(string $folder, string $fileName, $content, ?string $uploader = null)
|
||||||
|
$result = FileModel::saveFile(
|
||||||
|
$folder,
|
||||||
|
$fileName,
|
||||||
|
$data["content"],
|
||||||
|
$username // ← pass the real uploader here
|
||||||
|
);
|
||||||
|
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -401,6 +401,20 @@ class FolderController
|
|||||||
*
|
*
|
||||||
* @return void Outputs HTML content.
|
* @return void Outputs HTML content.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function formatBytes($bytes)
|
||||||
|
{
|
||||||
|
if ($bytes < 1024) {
|
||||||
|
return $bytes . " B";
|
||||||
|
} elseif ($bytes < 1024 * 1024) {
|
||||||
|
return round($bytes / 1024, 2) . " KB";
|
||||||
|
} elseif ($bytes < 1024 * 1024 * 1024) {
|
||||||
|
return round($bytes / (1024 * 1024), 2) . " MB";
|
||||||
|
} else {
|
||||||
|
return round($bytes / (1024 * 1024 * 1024), 2) . " GB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function shareFolder(): void
|
public function shareFolder(): void
|
||||||
{
|
{
|
||||||
// Retrieve GET parameters.
|
// Retrieve GET parameters.
|
||||||
@@ -495,12 +509,14 @@ class FolderController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract data for the HTML view.
|
// Load admin config so we can pull the sharedMaxUploadSize
|
||||||
$folderName = $data['folder'];
|
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||||
$files = $data['files'];
|
$adminConfig = AdminModel::getConfig();
|
||||||
$currentPage = $data['currentPage'];
|
$sharedMaxUploadSize = isset($adminConfig['sharedMaxUploadSize']) && is_numeric($adminConfig['sharedMaxUploadSize'])
|
||||||
$totalPages = $data['totalPages'];
|
? (int)$adminConfig['sharedMaxUploadSize']
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// For human‐readable formatting
|
||||||
function formatBytes($bytes)
|
function formatBytes($bytes)
|
||||||
{
|
{
|
||||||
if ($bytes < 1024) {
|
if ($bytes < 1024) {
|
||||||
@@ -514,6 +530,12 @@ class FolderController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract data for the HTML view.
|
||||||
|
$folderName = $data['folder'];
|
||||||
|
$files = $data['files'];
|
||||||
|
$currentPage = $data['currentPage'];
|
||||||
|
$totalPages = $data['totalPages'];
|
||||||
|
|
||||||
// Build the HTML view.
|
// Build the HTML view.
|
||||||
header("Content-Type: text/html; charset=utf-8");
|
header("Content-Type: text/html; charset=utf-8");
|
||||||
?>
|
?>
|
||||||
@@ -717,7 +739,11 @@ class FolderController
|
|||||||
<!-- Upload Container (if uploads are allowed by the share record) -->
|
<!-- Upload Container (if uploads are allowed by the share record) -->
|
||||||
<?php if (isset($data['record']['allowUpload']) && $data['record']['allowUpload'] == 1): ?>
|
<?php if (isset($data['record']['allowUpload']) && $data['record']['allowUpload'] == 1): ?>
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<h3>Upload File (50mb max size)</h3>
|
<h3>Upload File
|
||||||
|
<?php if ($sharedMaxUploadSize !== null): ?>
|
||||||
|
(<?php echo formatBytes($sharedMaxUploadSize); ?> max size)
|
||||||
|
<?php endif; ?>
|
||||||
|
</h3>
|
||||||
<form action="/api/folder/uploadToSharedFolder.php" method="post" enctype="multipart/form-data">
|
<form action="/api/folder/uploadToSharedFolder.php" method="post" enctype="multipart/form-data">
|
||||||
<!-- Pass the share token so the upload endpoint can verify -->
|
<!-- Pass the share token so the upload endpoint can verify -->
|
||||||
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
|
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
|||||||
@@ -72,34 +72,56 @@ class UploadController {
|
|||||||
*/
|
*/
|
||||||
public function handleUpload(): void {
|
public function handleUpload(): void {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// CSRF Protection.
|
//
|
||||||
|
// 1) CSRF – pull from header or POST fields
|
||||||
|
//
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$receivedToken = $headersArr['x-csrf-token'] ?? '';
|
$received = '';
|
||||||
if (!isset($_SESSION['csrf_token']) || trim($receivedToken) !== $_SESSION['csrf_token']) {
|
if (!empty($headersArr['x-csrf-token'])) {
|
||||||
http_response_code(403);
|
$received = trim($headersArr['x-csrf-token']);
|
||||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
} elseif (!empty($_POST['csrf_token'])) {
|
||||||
|
$received = trim($_POST['csrf_token']);
|
||||||
|
} elseif (!empty($_POST['upload_token'])) {
|
||||||
|
$received = trim($_POST['upload_token']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1a) If it doesn’t match, soft-fail: send new token and let client retry
|
||||||
|
if (!isset($_SESSION['csrf_token']) || $received !== $_SESSION['csrf_token']) {
|
||||||
|
// regenerate
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
// tell client “please retry with this new token”
|
||||||
|
http_response_code(200);
|
||||||
|
echo json_encode([
|
||||||
|
'csrf_expired' => true,
|
||||||
|
'csrf_token' => $_SESSION['csrf_token']
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
// Ensure user is authenticated.
|
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
//
|
||||||
|
// 2) Auth checks
|
||||||
|
//
|
||||||
|
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
// Check user permissions.
|
$userPerms = loadUserPermissions($_SESSION['username']);
|
||||||
$username = $_SESSION['username'] ?? '';
|
if (!empty($userPerms['disableUpload'])) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
if ($username && !empty($userPermissions['disableUpload'])) {
|
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(["error" => "Upload disabled for this user."]);
|
echo json_encode(["error" => "Upload disabled for this user."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to the model.
|
//
|
||||||
|
// 3) Delegate the actual file handling
|
||||||
|
//
|
||||||
$result = UploadModel::handleUpload($_POST, $_FILES);
|
$result = UploadModel::handleUpload($_POST, $_FILES);
|
||||||
|
|
||||||
// For chunked uploads, output JSON (e.g., "chunk uploaded" status).
|
//
|
||||||
|
// 4) Respond
|
||||||
|
//
|
||||||
if (isset($result['error'])) {
|
if (isset($result['error'])) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
@@ -109,8 +131,8 @@ class UploadController {
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, for full upload success, set a flash message and redirect.
|
// full‐upload redirect
|
||||||
$_SESSION['upload_message'] = "File uploaded successfully.";
|
$_SESSION['upload_message'] = "File uploaded successfully.";
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,63 +87,83 @@ class UserController
|
|||||||
|
|
||||||
public function addUser()
|
public function addUser()
|
||||||
{
|
{
|
||||||
|
// 1) Ensure JSON output and session
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
// 1a) Initialize CSRF token if missing
|
||||||
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if we're in setup mode.
|
// 2) Determine setup mode (first-ever admin creation)
|
||||||
// Setup mode means the "setup" query parameter is passed
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
// and users.txt is missing, empty, or contains only whitespace.
|
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
||||||
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
$setupMode = false;
|
||||||
if ($isSetup && (!file_exists($usersFile) || filesize($usersFile) == 0 || trim(file_get_contents($usersFile)) === '')) {
|
if (
|
||||||
// Allow initial admin creation without session or CSRF checks.
|
$isSetup && (! file_exists($usersFile)
|
||||||
|
|| filesize($usersFile) === 0
|
||||||
|
|| trim(file_get_contents($usersFile)) === ''
|
||||||
|
)
|
||||||
|
) {
|
||||||
$setupMode = true;
|
$setupMode = true;
|
||||||
} else {
|
} else {
|
||||||
$setupMode = false;
|
// 3) In non-setup, enforce CSRF + auth checks
|
||||||
// In non-setup mode, perform CSRF token and authentication checks.
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
$receivedToken = trim($headersArr['x-csrf-token'] ?? '');
|
||||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
|
||||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
// 3a) Soft-fail CSRF: on mismatch, regenerate and return new token
|
||||||
http_response_code(403);
|
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
header('X-CSRF-Token: ' . $_SESSION['csrf_token']);
|
||||||
|
echo json_encode([
|
||||||
|
'csrf_expired' => true,
|
||||||
|
'csrf_token' => $_SESSION['csrf_token']
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3b) Must be logged in as admin
|
||||||
if (
|
if (
|
||||||
!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
empty($_SESSION['authenticated'])
|
||||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true
|
|| $_SESSION['authenticated'] !== true
|
||||||
|
|| empty($_SESSION['isAdmin'])
|
||||||
|
|| $_SESSION['isAdmin'] !== true
|
||||||
) {
|
) {
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the JSON input data.
|
// 4) Parse input
|
||||||
$data = json_decode(file_get_contents("php://input"), true);
|
$data = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||||
$newUsername = trim($data["username"] ?? "");
|
$newUsername = trim($data['username'] ?? '');
|
||||||
$newPassword = trim($data["password"] ?? "");
|
$newPassword = trim($data['password'] ?? '');
|
||||||
|
|
||||||
// In setup mode, force the new user to be an admin.
|
// 5) Determine admin flag
|
||||||
if ($setupMode) {
|
if ($setupMode) {
|
||||||
$isAdmin = "1";
|
$isAdmin = '1';
|
||||||
} else {
|
} else {
|
||||||
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0";
|
$isAdmin = !empty($data['isAdmin']) ? '1' : '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that a username and password are provided.
|
// 6) Validate fields
|
||||||
if (!$newUsername || !$newPassword) {
|
if ($newUsername === '' || $newPassword === '') {
|
||||||
echo json_encode(["error" => "Username and password required"]);
|
echo json_encode(["error" => "Username and password required"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate username format.
|
|
||||||
if (!preg_match(REGEX_USER, $newUsername)) {
|
if (!preg_match(REGEX_USER, $newUsername)) {
|
||||||
echo json_encode(["error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
echo json_encode([
|
||||||
|
"error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate the business logic to the model.
|
// 7) Delegate to model
|
||||||
$result = userModel::addUser($newUsername, $newPassword, $isAdmin, $setupMode);
|
$result = userModel::addUser($newUsername, $newPassword, $isAdmin, $setupMode);
|
||||||
|
|
||||||
|
// 8) Return model result
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -852,7 +872,7 @@ class UserController
|
|||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
||||||
|
|
||||||
// Rate‑limit
|
// Rate-limit
|
||||||
if (!isset($_SESSION['totp_failures'])) {
|
if (!isset($_SESSION['totp_failures'])) {
|
||||||
$_SESSION['totp_failures'] = 0;
|
$_SESSION['totp_failures'] = 0;
|
||||||
}
|
}
|
||||||
@@ -863,7 +883,7 @@ class UserController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must be authenticated OR pending login
|
// Must be authenticated OR pending login
|
||||||
if (!((!empty($_SESSION['authenticated'])) || isset($_SESSION['pending_login_user']))) {
|
if (empty($_SESSION['authenticated']) && !isset($_SESSION['pending_login_user'])) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
||||||
exit;
|
exit;
|
||||||
@@ -878,7 +898,7 @@ class UserController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and validate input
|
// Parse & validate input
|
||||||
$inputData = json_decode(file_get_contents("php://input"), true);
|
$inputData = json_decode(file_get_contents("php://input"), true);
|
||||||
$code = trim($inputData['totp_code'] ?? '');
|
$code = trim($inputData['totp_code'] ?? '');
|
||||||
if (!preg_match('/^\d{6}$/', $code)) {
|
if (!preg_match('/^\d{6}$/', $code)) {
|
||||||
@@ -893,11 +913,11 @@ class UserController
|
|||||||
'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
|
'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pending‑login flow (first password step passed)
|
// === Pending-login flow (we just came from auth and need to finish login) ===
|
||||||
if (isset($_SESSION['pending_login_user'])) {
|
if (isset($_SESSION['pending_login_user'])) {
|
||||||
$username = $_SESSION['pending_login_user'];
|
$username = $_SESSION['pending_login_user'];
|
||||||
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
||||||
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
||||||
|
|
||||||
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
||||||
$_SESSION['totp_failures']++;
|
$_SESSION['totp_failures']++;
|
||||||
@@ -906,53 +926,45 @@ class UserController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Issue “remember me” token if requested ===
|
// Issue “remember me” token if requested
|
||||||
if ($rememberMe) {
|
if ($rememberMe) {
|
||||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
$token = bin2hex(random_bytes(32));
|
$token = bin2hex(random_bytes(32));
|
||||||
$expiry = time() + 30 * 24 * 60 * 60;
|
$expiry = time() + 30 * 24 * 60 * 60;
|
||||||
$all = [];
|
$all = [];
|
||||||
|
|
||||||
if (file_exists($tokFile)) {
|
if (file_exists($tokFile)) {
|
||||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||||
$all = json_decode($dec, true) ?: [];
|
$all = json_decode($dec, true) ?: [];
|
||||||
}
|
}
|
||||||
$all[$token] = [
|
$all[$token] = [
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'expiry' => $expiry,
|
'expiry' => $expiry,
|
||||||
'isAdmin' => $_SESSION['isAdmin']
|
'isAdmin' => ((int)userModel::getUserRole($username) === 1),
|
||||||
|
'folderOnly' => loadUserPermissions($username)['folderOnly'] ?? false,
|
||||||
|
'readOnly' => loadUserPermissions($username)['readOnly'] ?? false,
|
||||||
|
'disableUpload'=> loadUserPermissions($username)['disableUpload']?? false
|
||||||
];
|
];
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
$tokFile,
|
$tokFile,
|
||||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||||
LOCK_EX
|
LOCK_EX
|
||||||
);
|
);
|
||||||
|
|
||||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
|
|
||||||
// Persistent cookie
|
|
||||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||||
|
setcookie(session_name(), session_id(), $expiry, '/', '', $secure, true);
|
||||||
// Re‑issue PHP session cookie
|
|
||||||
setcookie(
|
|
||||||
session_name(),
|
|
||||||
session_id(),
|
|
||||||
$expiry,
|
|
||||||
'/',
|
|
||||||
'',
|
|
||||||
$secure,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize login
|
// === Finalize login into session exactly as finalizeLogin() would ===
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION['authenticated'] = true;
|
$_SESSION['authenticated'] = true;
|
||||||
$_SESSION['username'] = $username;
|
$_SESSION['username'] = $username;
|
||||||
$_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1");
|
$_SESSION['isAdmin'] = ((int)userModel::getUserRole($username) === 1);
|
||||||
$_SESSION['folderOnly'] = loadUserPermissions($username);
|
$perms = loadUserPermissions($username);
|
||||||
|
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
|
||||||
|
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
|
||||||
|
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
|
||||||
|
|
||||||
// Clean up
|
// Clean up pending markers
|
||||||
unset(
|
unset(
|
||||||
$_SESSION['pending_login_user'],
|
$_SESSION['pending_login_user'],
|
||||||
$_SESSION['pending_login_secret'],
|
$_SESSION['pending_login_secret'],
|
||||||
@@ -960,34 +972,43 @@ class UserController
|
|||||||
$_SESSION['totp_failures']
|
$_SESSION['totp_failures']
|
||||||
);
|
);
|
||||||
|
|
||||||
echo json_encode(['status' => 'ok', 'message' => 'Login successful']);
|
// Send back full login payload
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'ok',
|
||||||
|
'success' => 'Login successful',
|
||||||
|
'isAdmin' => $_SESSION['isAdmin'],
|
||||||
|
'folderOnly' => $_SESSION['folderOnly'],
|
||||||
|
'readOnly' => $_SESSION['readOnly'],
|
||||||
|
'disableUpload' => $_SESSION['disableUpload'],
|
||||||
|
'username' => $_SESSION['username']
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup/verification flow (not pending)
|
// Setup/verification flow (not pending)
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
if (!$username) {
|
if (!$username) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$totpSecret = userModel::getTOTPSecret($username);
|
$totpSecret = userModel::getTOTPSecret($username);
|
||||||
if (!$totpSecret) {
|
if (!$totpSecret) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||||
$_SESSION['totp_failures']++;
|
$_SESSION['totp_failures']++;
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successful setup/verification
|
// Successful setup/verification
|
||||||
unset($_SESSION['totp_failures']);
|
unset($_SESSION['totp_failures']);
|
||||||
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,23 @@ require_once PROJECT_ROOT . '/config/config.php';
|
|||||||
|
|
||||||
class AdminModel
|
class AdminModel
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Parse a shorthand size value (e.g. "5G", "500M", "123K") into bytes.
|
||||||
|
*
|
||||||
|
* @param string $val
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function parseSize(string $val): int
|
||||||
|
{
|
||||||
|
$unit = strtolower(substr($val, -1));
|
||||||
|
$num = (int) rtrim($val, 'bkmgtpezyBKMGTPESY');
|
||||||
|
switch ($unit) {
|
||||||
|
case 'g': return $num * 1024 ** 3;
|
||||||
|
case 'm': return $num * 1024 ** 2;
|
||||||
|
case 'k': return $num * 1024;
|
||||||
|
default: return $num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the admin configuration file.
|
* Updates the admin configuration file.
|
||||||
@@ -24,6 +41,28 @@ class AdminModel
|
|||||||
return ["error" => "Incomplete OIDC configuration."];
|
return ["error" => "Incomplete OIDC configuration."];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure enableWebDAV flag is boolean (default to false if missing)
|
||||||
|
$configUpdate['enableWebDAV'] = isset($configUpdate['enableWebDAV'])
|
||||||
|
? (bool)$configUpdate['enableWebDAV']
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// Validate sharedMaxUploadSize if provided
|
||||||
|
if (isset($configUpdate['sharedMaxUploadSize'])) {
|
||||||
|
$sms = filter_var(
|
||||||
|
$configUpdate['sharedMaxUploadSize'],
|
||||||
|
FILTER_VALIDATE_INT,
|
||||||
|
["options" => ["min_range" => 1]]
|
||||||
|
);
|
||||||
|
if ($sms === false) {
|
||||||
|
return ["error" => "Invalid sharedMaxUploadSize."];
|
||||||
|
}
|
||||||
|
$totalBytes = self::parseSize(TOTAL_UPLOAD_SIZE);
|
||||||
|
if ($sms > $totalBytes) {
|
||||||
|
return ["error" => "sharedMaxUploadSize must be ≤ TOTAL_UPLOAD_SIZE."];
|
||||||
|
}
|
||||||
|
$configUpdate['sharedMaxUploadSize'] = $sms;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert configuration to JSON.
|
// Convert configuration to JSON.
|
||||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||||
if ($plainTextConfig === false) {
|
if ($plainTextConfig === false) {
|
||||||
@@ -59,7 +98,8 @@ class AdminModel
|
|||||||
*
|
*
|
||||||
* @return array The configuration array, or defaults if not found.
|
* @return array The configuration array, or defaults if not found.
|
||||||
*/
|
*/
|
||||||
public static function getConfig(): array {
|
public static function getConfig(): array
|
||||||
|
{
|
||||||
$configFile = USERS_DIR . 'adminConfig.json';
|
$configFile = USERS_DIR . 'adminConfig.json';
|
||||||
if (file_exists($configFile)) {
|
if (file_exists($configFile)) {
|
||||||
$encryptedContent = file_get_contents($configFile);
|
$encryptedContent = file_get_contents($configFile);
|
||||||
@@ -72,10 +112,9 @@ class AdminModel
|
|||||||
if (!is_array($config)) {
|
if (!is_array($config)) {
|
||||||
$config = [];
|
$config = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize login options.
|
// Normalize login options if missing
|
||||||
if (!isset($config['loginOptions'])) {
|
if (!isset($config['loginOptions'])) {
|
||||||
// Create loginOptions array from top-level keys if missing.
|
|
||||||
$config['loginOptions'] = [
|
$config['loginOptions'] = [
|
||||||
'disableFormLogin' => isset($config['disableFormLogin']) ? (bool)$config['disableFormLogin'] : false,
|
'disableFormLogin' => isset($config['disableFormLogin']) ? (bool)$config['disableFormLogin'] : false,
|
||||||
'disableBasicAuth' => isset($config['disableBasicAuth']) ? (bool)$config['disableBasicAuth'] : false,
|
'disableBasicAuth' => isset($config['disableBasicAuth']) ? (bool)$config['disableBasicAuth'] : false,
|
||||||
@@ -88,31 +127,43 @@ class AdminModel
|
|||||||
$config['loginOptions']['disableBasicAuth'] = (bool)$config['loginOptions']['disableBasicAuth'];
|
$config['loginOptions']['disableBasicAuth'] = (bool)$config['loginOptions']['disableBasicAuth'];
|
||||||
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default values for other keys
|
||||||
if (!isset($config['globalOtpauthUrl'])) {
|
if (!isset($config['globalOtpauthUrl'])) {
|
||||||
$config['globalOtpauthUrl'] = "";
|
$config['globalOtpauthUrl'] = "";
|
||||||
}
|
}
|
||||||
if (!isset($config['header_title']) || empty($config['header_title'])) {
|
if (!isset($config['header_title']) || empty($config['header_title'])) {
|
||||||
$config['header_title'] = "FileRise";
|
$config['header_title'] = "FileRise";
|
||||||
}
|
}
|
||||||
|
if (!isset($config['enableWebDAV'])) {
|
||||||
|
$config['enableWebDAV'] = false;
|
||||||
|
}
|
||||||
|
// Default sharedMaxUploadSize to 50MB or TOTAL_UPLOAD_SIZE if smaller
|
||||||
|
if (!isset($config['sharedMaxUploadSize'])) {
|
||||||
|
$defaultSms = min(50 * 1024 * 1024, self::parseSize(TOTAL_UPLOAD_SIZE));
|
||||||
|
$config['sharedMaxUploadSize'] = $defaultSms;
|
||||||
|
}
|
||||||
|
|
||||||
return $config;
|
return $config;
|
||||||
} else {
|
} else {
|
||||||
// Return defaults.
|
// Return defaults.
|
||||||
return [
|
return [
|
||||||
'header_title' => "FileRise",
|
'header_title' => "FileRise",
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
'providerUrl' => 'https://your-oidc-provider.com',
|
'providerUrl' => 'https://your-oidc-provider.com',
|
||||||
'clientId' => 'YOUR_CLIENT_ID',
|
'clientId' => 'YOUR_CLIENT_ID',
|
||||||
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
||||||
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
||||||
],
|
],
|
||||||
'loginOptions' => [
|
'loginOptions' => [
|
||||||
'disableFormLogin' => false,
|
'disableFormLogin' => false,
|
||||||
'disableBasicAuth' => false,
|
'disableBasicAuth' => false,
|
||||||
'disableOIDCLogin' => false
|
'disableOIDCLogin' => false
|
||||||
],
|
],
|
||||||
'globalOtpauthUrl' => ""
|
'globalOtpauthUrl' => "",
|
||||||
|
'enableWebDAV' => false,
|
||||||
|
'sharedMaxUploadSize' => min(50 * 1024 * 1024, self::parseSize(TOTAL_UPLOAD_SIZE))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
require_once PROJECT_ROOT . '/config/config.php';
|
require_once PROJECT_ROOT . '/config/config.php';
|
||||||
|
|
||||||
class AuthModel {
|
class AuthModel
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the user's role from the users file.
|
* Retrieves the user's role from the users file.
|
||||||
@@ -11,7 +12,8 @@ class AuthModel {
|
|||||||
* @param string $username
|
* @param string $username
|
||||||
* @return string|null The role string (e.g. "1" for admin) or null if not found.
|
* @return string|null The role string (e.g. "1" for admin) or null if not found.
|
||||||
*/
|
*/
|
||||||
public static function getUserRole(string $username): ?string {
|
public static function getUserRole(string $username): ?string
|
||||||
|
{
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (file_exists($usersFile)) {
|
if (file_exists($usersFile)) {
|
||||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
@@ -23,7 +25,7 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticates the user using form-based credentials.
|
* Authenticates the user using form-based credentials.
|
||||||
*
|
*
|
||||||
@@ -31,7 +33,8 @@ class AuthModel {
|
|||||||
* @param string $password
|
* @param string $password
|
||||||
* @return array|false Returns an associative array with user data (role, totp_secret) on success or false on failure.
|
* @return array|false Returns an associative array with user data (role, totp_secret) on success or false on failure.
|
||||||
*/
|
*/
|
||||||
public static function authenticate(string $username, string $password) {
|
public static function authenticate(string $username, string $password)
|
||||||
|
{
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -51,14 +54,15 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads failed login attempts from a file.
|
* Loads failed login attempts from a file.
|
||||||
*
|
*
|
||||||
* @param string $file
|
* @param string $file
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function loadFailedAttempts(string $file): array {
|
public static function loadFailedAttempts(string $file): array
|
||||||
|
{
|
||||||
if (file_exists($file)) {
|
if (file_exists($file)) {
|
||||||
$data = json_decode(file_get_contents($file), true);
|
$data = json_decode(file_get_contents($file), true);
|
||||||
if (is_array($data)) {
|
if (is_array($data)) {
|
||||||
@@ -67,7 +71,7 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves failed login attempts into a file.
|
* Saves failed login attempts into a file.
|
||||||
*
|
*
|
||||||
@@ -75,17 +79,19 @@ class AuthModel {
|
|||||||
* @param array $data
|
* @param array $data
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function saveFailedAttempts(string $file, array $data): void {
|
public static function saveFailedAttempts(string $file, array $data): void
|
||||||
|
{
|
||||||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
|
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a user's TOTP secret from the users file.
|
* Retrieves a user's TOTP secret from the users file.
|
||||||
*
|
*
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @return string|null Returns the decrypted TOTP secret or null if not set.
|
* @return string|null Returns the decrypted TOTP secret or null if not set.
|
||||||
*/
|
*/
|
||||||
public static function getUserTOTPSecret(string $username): ?string {
|
public static function getUserTOTPSecret(string $username): ?string
|
||||||
|
{
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -98,14 +104,15 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the folder-only permission for a given user.
|
* Loads the folder-only permission for a given user.
|
||||||
*
|
*
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function loadFolderPermission(string $username): bool {
|
public static function loadFolderPermission(string $username): bool
|
||||||
|
{
|
||||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||||
if (file_exists($permissionsFile)) {
|
if (file_exists($permissionsFile)) {
|
||||||
$content = file_get_contents($permissionsFile);
|
$content = file_get_contents($permissionsFile);
|
||||||
@@ -121,4 +128,31 @@ class AuthModel {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Validate a remember-me token and return its stored payload.
|
||||||
|
*
|
||||||
|
* @param string $token
|
||||||
|
* @return array|null Returns ['username'=>…, 'expiry'=>…, 'isAdmin'=>…] or null if invalid/expired.
|
||||||
|
*/
|
||||||
|
public static function validateRememberToken(string $token): ?array
|
||||||
|
{
|
||||||
|
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
|
if (! file_exists($tokFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt and decode the full token store
|
||||||
|
$encrypted = file_get_contents($tokFile);
|
||||||
|
$json = decryptData($encrypted, $GLOBALS['encryptionKey']);
|
||||||
|
$all = json_decode($json, true) ?: [];
|
||||||
|
|
||||||
|
// Lookup and expiry check
|
||||||
|
if (empty($all[$token]) || !isset($all[$token]['expiry']) || $all[$token]['expiry'] < time()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid token—return its payload
|
||||||
|
return $all[$token];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -383,88 +383,95 @@ class FileModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Saves file content to disk and updates folder metadata.
|
* Save a file’s contents *and* record its metadata, including who uploaded it.
|
||||||
*
|
*
|
||||||
* @param string $folder The target folder where the file is to be saved (e.g. "root" or a subfolder).
|
* @param string $folder Folder key (e.g. "root" or "invoices/2025")
|
||||||
* @param string $fileName The name of the file.
|
* @param string $fileName Basename of the file
|
||||||
* @param string $content The file content.
|
* @param resource|string $content File contents (stream or string)
|
||||||
* @return array Returns an associative array with either a "success" key or an "error" key.
|
* @param string|null $uploader Username of uploader (if null, falls back to session)
|
||||||
*/
|
* @return array ["success"=>"…"] or ["error"=>"…"]
|
||||||
public static function saveFile($folder, $fileName, $content) {
|
*/
|
||||||
// Sanitize and determine the folder name.
|
public static function saveFile(string $folder, string $fileName, $content, ?string $uploader = null): array {
|
||||||
$folder = trim($folder) ?: 'root';
|
// Sanitize inputs
|
||||||
$fileName = basename(trim($fileName));
|
$folder = trim($folder) ?: 'root';
|
||||||
|
$fileName = basename(trim($fileName));
|
||||||
|
|
||||||
// Validate folder: if not "root", must match REGEX_FOLDER_NAME.
|
// Validate folder name
|
||||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
return ["error" => "Invalid folder name"];
|
return ["error" => "Invalid folder name"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine target directory
|
||||||
|
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||||
|
$targetDir = strtolower($folder) === 'root'
|
||||||
|
? $baseDir . DIRECTORY_SEPARATOR
|
||||||
|
: $baseDir . DIRECTORY_SEPARATOR . trim($folder, "/\\ ") . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (strpos(realpath($targetDir), realpath($baseDir)) !== 0) {
|
||||||
|
return ["error" => "Invalid folder path"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true)) {
|
||||||
|
return ["error" => "Failed to create destination folder"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $targetDir . $fileName;
|
||||||
|
|
||||||
|
// ——— STREAM TO DISK ———
|
||||||
|
if (is_resource($content)) {
|
||||||
|
$out = fopen($filePath, 'wb');
|
||||||
|
if ($out === false) {
|
||||||
|
return ["error" => "Unable to open file for writing"];
|
||||||
}
|
}
|
||||||
|
stream_copy_to_stream($content, $out);
|
||||||
// Determine base upload directory.
|
fclose($out);
|
||||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
} else {
|
||||||
if (strtolower($folder) === 'root' || $folder === "") {
|
if (file_put_contents($filePath, (string)$content) === false) {
|
||||||
$targetDir = $baseDir . DIRECTORY_SEPARATOR;
|
|
||||||
} else {
|
|
||||||
$targetDir = $baseDir . DIRECTORY_SEPARATOR . trim($folder, "/\\ ") . DIRECTORY_SEPARATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// (Optional security check to ensure targetDir is within baseDir.)
|
|
||||||
if (strpos(realpath($targetDir), realpath($baseDir)) !== 0) {
|
|
||||||
return ["error" => "Invalid folder path"];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create target directory if it doesn't exist.
|
|
||||||
if (!is_dir($targetDir)) {
|
|
||||||
if (!mkdir($targetDir, 0775, true)) {
|
|
||||||
return ["error" => "Failed to create destination folder"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$filePath = $targetDir . $fileName;
|
|
||||||
// Attempt to save the file.
|
|
||||||
if (file_put_contents($filePath, $content) === false) {
|
|
||||||
return ["error" => "Error saving file"];
|
return ["error" => "Error saving file"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update metadata.
|
|
||||||
// Build metadata file path for the folder.
|
|
||||||
$metadataKey = (strtolower($folder) === "root" || $folder === "") ? "root" : $folder;
|
|
||||||
$metadataFileName = str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json';
|
|
||||||
$metadataFilePath = META_DIR . $metadataFileName;
|
|
||||||
|
|
||||||
if (file_exists($metadataFilePath)) {
|
|
||||||
$metadata = json_decode(file_get_contents($metadataFilePath), true);
|
|
||||||
} else {
|
|
||||||
$metadata = [];
|
|
||||||
}
|
|
||||||
if (!is_array($metadata)) {
|
|
||||||
$metadata = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentTime = date(DATE_TIME_FORMAT);
|
|
||||||
$uploader = $_SESSION['username'] ?? "Unknown";
|
|
||||||
|
|
||||||
// Update metadata for the file. If already exists, update its "modified" timestamp.
|
|
||||||
if (isset($metadata[$fileName])) {
|
|
||||||
$metadata[$fileName]['modified'] = $currentTime;
|
|
||||||
$metadata[$fileName]['uploader'] = $uploader; // optional: update uploader if desired.
|
|
||||||
} else {
|
|
||||||
$metadata[$fileName] = [
|
|
||||||
"uploaded" => $currentTime,
|
|
||||||
"modified" => $currentTime,
|
|
||||||
"uploader" => $uploader
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write updated metadata.
|
|
||||||
if (file_put_contents($metadataFilePath, json_encode($metadata, JSON_PRETTY_PRINT)) === false) {
|
|
||||||
return ["error" => "Failed to update metadata"];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ["success" => "File saved successfully"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ——— UPDATE METADATA ———
|
||||||
|
$metadataKey = strtolower($folder) === "root" ? "root" : $folder;
|
||||||
|
$metadataFileName = str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json';
|
||||||
|
$metadataFilePath = META_DIR . $metadataFileName;
|
||||||
|
|
||||||
|
// Load existing metadata
|
||||||
|
$metadata = [];
|
||||||
|
if (file_exists($metadataFilePath)) {
|
||||||
|
$existing = @json_decode(file_get_contents($metadataFilePath), true);
|
||||||
|
if (is_array($existing)) {
|
||||||
|
$metadata = $existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentTime = date(DATE_TIME_FORMAT);
|
||||||
|
// Use passed-in uploader, or fall back to session
|
||||||
|
if ($uploader === null) {
|
||||||
|
$uploader = $_SESSION['username'] ?? "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($metadata[$fileName])) {
|
||||||
|
$metadata[$fileName]['modified'] = $currentTime;
|
||||||
|
$metadata[$fileName]['uploader'] = $uploader;
|
||||||
|
} else {
|
||||||
|
$metadata[$fileName] = [
|
||||||
|
"uploaded" => $currentTime,
|
||||||
|
"modified" => $currentTime,
|
||||||
|
"uploader" => $uploader
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($metadataFilePath, json_encode($metadata, JSON_PRETTY_PRINT)) === false) {
|
||||||
|
return ["error" => "Failed to update metadata"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ["success" => "File saved successfully"];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and retrieves information needed to download a file.
|
* Validates and retrieves information needed to download a file.
|
||||||
*
|
*
|
||||||
|
|||||||
16
src/webdav/CurrentUser.php
Normal file
16
src/webdav/CurrentUser.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
// src/webdav/CurrentUser.php
|
||||||
|
namespace FileRise\WebDAV;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton holder for the current WebDAV username.
|
||||||
|
*/
|
||||||
|
class CurrentUser {
|
||||||
|
private static string $user = 'Unknown';
|
||||||
|
public static function set(string $u): void {
|
||||||
|
self::$user = $u;
|
||||||
|
}
|
||||||
|
public static function get(): string {
|
||||||
|
return self::$user;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/webdav/FileRiseDirectory.php
Normal file
110
src/webdav/FileRiseDirectory.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
namespace FileRise\WebDAV;
|
||||||
|
|
||||||
|
// Bootstrap constants and models
|
||||||
|
require_once __DIR__ . '/../../config/config.php'; // UPLOAD_DIR, META_DIR, DATE_TIME_FORMAT
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php'; // SabreDAV
|
||||||
|
require_once __DIR__ . '/../../src/models/FolderModel.php';
|
||||||
|
require_once __DIR__ . '/../../src/models/FileModel.php';
|
||||||
|
require_once __DIR__ . '/FileRiseFile.php';
|
||||||
|
|
||||||
|
use Sabre\DAV\ICollection;
|
||||||
|
use Sabre\DAV\INode;
|
||||||
|
use Sabre\DAV\Exception\NotFound;
|
||||||
|
use Sabre\DAV\Exception\Forbidden;
|
||||||
|
use FileRise\WebDAV\FileRiseFile;
|
||||||
|
use FolderModel;
|
||||||
|
use FileModel;
|
||||||
|
|
||||||
|
class FileRiseDirectory implements ICollection, INode {
|
||||||
|
private string $path;
|
||||||
|
private string $user;
|
||||||
|
private bool $folderOnly;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path Absolute filesystem path (no trailing slash)
|
||||||
|
* @param string $user Authenticated username
|
||||||
|
* @param bool $folderOnly If true, non‑admins only see $path/{user}
|
||||||
|
*/
|
||||||
|
public function __construct(string $path, string $user, bool $folderOnly) {
|
||||||
|
$this->path = rtrim($path, '/\\');
|
||||||
|
$this->user = $user;
|
||||||
|
$this->folderOnly = $folderOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── INode ───────────────────────────────────────────
|
||||||
|
|
||||||
|
public function getName(): string {
|
||||||
|
return basename($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastModified(): int {
|
||||||
|
return filemtime($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(): void {
|
||||||
|
throw new Forbidden('Cannot delete this node');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName($name): void {
|
||||||
|
throw new Forbidden('Renaming not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ICollection ────────────────────────────────────
|
||||||
|
|
||||||
|
public function getChildren(): array {
|
||||||
|
$nodes = [];
|
||||||
|
foreach (new \DirectoryIterator($this->path) as $item) {
|
||||||
|
if ($item->isDot()) continue;
|
||||||
|
$full = $item->getPathname();
|
||||||
|
if ($item->isDir()) {
|
||||||
|
$nodes[] = new self($full, $this->user, $this->folderOnly);
|
||||||
|
} else {
|
||||||
|
$nodes[] = new FileRiseFile($full, $this->user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Apply folder‑only at the top level
|
||||||
|
if (
|
||||||
|
$this->folderOnly
|
||||||
|
&& realpath($this->path) === realpath(rtrim(UPLOAD_DIR,'/\\'))
|
||||||
|
) {
|
||||||
|
$nodes = array_filter($nodes, fn(INode $n)=> $n->getName() === $this->user);
|
||||||
|
}
|
||||||
|
return array_values($nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function childExists($name): bool {
|
||||||
|
return file_exists($this->path . DIRECTORY_SEPARATOR . $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChild($name): INode {
|
||||||
|
$full = $this->path . DIRECTORY_SEPARATOR . $name;
|
||||||
|
if (!file_exists($full)) throw new NotFound("Not found: $name");
|
||||||
|
return is_dir($full)
|
||||||
|
? new self($full, $this->user, $this->folderOnly)
|
||||||
|
: new FileRiseFile($full, $this->user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createFile($name, $data = null): INode {
|
||||||
|
$full = $this->path . DIRECTORY_SEPARATOR . $name;
|
||||||
|
$content = is_resource($data) ? stream_get_contents($data) : (string)$data;
|
||||||
|
|
||||||
|
// Compute folder‑key relative to UPLOAD_DIR
|
||||||
|
$rel = substr($full, strlen(rtrim(UPLOAD_DIR,'/\\'))+1);
|
||||||
|
$parts = explode('/', str_replace('\\','/',$rel));
|
||||||
|
$filename = array_pop($parts);
|
||||||
|
$folder = empty($parts) ? 'root' : implode('/', $parts);
|
||||||
|
|
||||||
|
FileModel::saveFile($folder, $filename, $content, $this->user);
|
||||||
|
return new FileRiseFile($full, $this->user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDirectory($name): INode {
|
||||||
|
$full = $this->path . DIRECTORY_SEPARATOR . $name;
|
||||||
|
$rel = substr($full, strlen(rtrim(UPLOAD_DIR,'/\\'))+1);
|
||||||
|
$parent = dirname(str_replace('\\','/',$rel));
|
||||||
|
if ($parent === '.' || $parent === '/') $parent = '';
|
||||||
|
FolderModel::createFolder($name, $parent, $this->user);
|
||||||
|
return new self($full, $this->user, $this->folderOnly);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/webdav/FileRiseFile.php
Normal file
115
src/webdav/FileRiseFile.php
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
// src/webdav/FileRiseFile.php
|
||||||
|
|
||||||
|
namespace FileRise\WebDAV;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../../src/models/FileModel.php';
|
||||||
|
|
||||||
|
use Sabre\DAV\IFile;
|
||||||
|
use Sabre\DAV\INode;
|
||||||
|
use Sabre\DAV\Exception\Forbidden;
|
||||||
|
use FileModel;
|
||||||
|
|
||||||
|
class FileRiseFile implements IFile, INode {
|
||||||
|
private string $path;
|
||||||
|
|
||||||
|
public function __construct(string $path) {
|
||||||
|
$this->path = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── INode ───────────────────────────────────────────
|
||||||
|
|
||||||
|
public function getName(): string {
|
||||||
|
return basename($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastModified(): int {
|
||||||
|
return filemtime($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(): void {
|
||||||
|
$base = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||||
|
$rel = substr($this->path, strlen($base));
|
||||||
|
$parts = explode(DIRECTORY_SEPARATOR, $rel);
|
||||||
|
$file = array_pop($parts);
|
||||||
|
$folder = empty($parts) ? 'root' : $parts[0];
|
||||||
|
FileModel::deleteFiles($folder, [$file]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName($newName): void {
|
||||||
|
throw new Forbidden('Renaming files not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── IFile ───────────────────────────────────────────
|
||||||
|
|
||||||
|
public function get() {
|
||||||
|
return fopen($this->path, 'rb');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put($data): ?string {
|
||||||
|
// 1) Save incoming data
|
||||||
|
file_put_contents(
|
||||||
|
$this->path,
|
||||||
|
is_resource($data) ? stream_get_contents($data) : (string)$data
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2) Update metadata with CurrentUser
|
||||||
|
$this->updateMetadata();
|
||||||
|
|
||||||
|
// 3) Flush to client fast
|
||||||
|
if (function_exists('fastcgi_finish_request')) {
|
||||||
|
fastcgi_finish_request();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // no ETag
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSize(): int {
|
||||||
|
return filesize($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getETag(): string {
|
||||||
|
return '"' . md5($this->getLastModified() . $this->getSize()) . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContentType(): ?string {
|
||||||
|
return mime_content_type($this->path) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Metadata helper ───────────────────────────────────
|
||||||
|
|
||||||
|
private function updateMetadata(): void {
|
||||||
|
$base = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||||
|
$rel = substr($this->path, strlen($base));
|
||||||
|
$parts = explode(DIRECTORY_SEPARATOR, $rel);
|
||||||
|
$fileName = array_pop($parts);
|
||||||
|
$folder = empty($parts) ? 'root' : $parts[0];
|
||||||
|
|
||||||
|
$metaFile = META_DIR
|
||||||
|
. ($folder === 'root'
|
||||||
|
? 'root_metadata.json'
|
||||||
|
: str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json');
|
||||||
|
|
||||||
|
$metadata = [];
|
||||||
|
if (file_exists($metaFile)) {
|
||||||
|
$decoded = json_decode(file_get_contents($metaFile), true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$metadata = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date(DATE_TIME_FORMAT);
|
||||||
|
$uploaded = $metadata[$fileName]['uploaded'] ?? $now;
|
||||||
|
$uploader = CurrentUser::get();
|
||||||
|
|
||||||
|
$metadata[$fileName] = [
|
||||||
|
'uploaded' => $uploaded,
|
||||||
|
'modified' => $now,
|
||||||
|
'uploader' => $uploader,
|
||||||
|
];
|
||||||
|
|
||||||
|
file_put_contents($metaFile, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
}
|
||||||
194
start.sh
194
start.sh
@@ -1,162 +1,112 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
echo "🚀 Running start.sh..."
|
echo "🚀 Running start.sh..."
|
||||||
|
|
||||||
# Warn if default persistent tokens key is in use
|
# 1) Token‐key warning
|
||||||
if [ "$PERSISTENT_TOKENS_KEY" = "default_please_change_this_key" ]; then
|
if [ "${PERSISTENT_TOKENS_KEY}" = "default_please_change_this_key" ]; then
|
||||||
echo "⚠️ WARNING: Using default persistent tokens key. Please override PERSISTENT_TOKENS_KEY for production."
|
echo "⚠️ WARNING: Using default persistent tokens key—override for production."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update config.php based on environment variables
|
# 2) Update config.php based on environment variables
|
||||||
CONFIG_FILE="/var/www/config/config.php"
|
CONFIG_FILE="/var/www/config/config.php"
|
||||||
if [ -f "$CONFIG_FILE" ]; then
|
if [ -f "${CONFIG_FILE}" ]; then
|
||||||
echo "🔄 Updating config.php based on environment variables..."
|
echo "🔄 Updating config.php from env vars..."
|
||||||
if [ -n "$TIMEZONE" ]; then
|
[ -n "${TIMEZONE:-}" ] && sed -i "s|define('TIMEZONE',[[:space:]]*'[^']*');|define('TIMEZONE', '${TIMEZONE}');|" "${CONFIG_FILE}"
|
||||||
echo " Setting TIMEZONE to $TIMEZONE"
|
[ -n "${DATE_TIME_FORMAT:-}" ] && sed -i "s|define('DATE_TIME_FORMAT',[[:space:]]*'[^']*');|define('DATE_TIME_FORMAT', '${DATE_TIME_FORMAT}');|" "${CONFIG_FILE}"
|
||||||
sed -i "s|define('TIMEZONE',[[:space:]]*'[^']*');|define('TIMEZONE', '$TIMEZONE');|" "$CONFIG_FILE"
|
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||||
fi
|
sed -i "s|define('TOTAL_UPLOAD_SIZE',[[:space:]]*'[^']*');|define('TOTAL_UPLOAD_SIZE', '${TOTAL_UPLOAD_SIZE}');|" "${CONFIG_FILE}"
|
||||||
if [ -n "$DATE_TIME_FORMAT" ]; then
|
|
||||||
echo "🔄 Setting DATE_TIME_FORMAT to $DATE_TIME_FORMAT"
|
|
||||||
sed -i "s|define('DATE_TIME_FORMAT',[[:space:]]*'[^']*');|define('DATE_TIME_FORMAT', '$DATE_TIME_FORMAT');|" "$CONFIG_FILE"
|
|
||||||
fi
|
|
||||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
|
||||||
echo "🔄 Setting TOTAL_UPLOAD_SIZE to $TOTAL_UPLOAD_SIZE"
|
|
||||||
sed -i "s|define('TOTAL_UPLOAD_SIZE',[[:space:]]*'[^']*');|define('TOTAL_UPLOAD_SIZE', '$TOTAL_UPLOAD_SIZE');|" "$CONFIG_FILE"
|
|
||||||
fi
|
|
||||||
if [ -n "$SECURE" ]; then
|
|
||||||
echo "🔄 Setting SECURE to $SECURE"
|
|
||||||
sed -i "s|\$envSecure = getenv('SECURE');|\$envSecure = '$SECURE';|" "$CONFIG_FILE"
|
|
||||||
fi
|
|
||||||
if [ -n "$SHARE_URL" ]; then
|
|
||||||
echo "🔄 Setting SHARE_URL to $SHARE_URL"
|
|
||||||
sed -i "s|define('SHARE_URL',[[:space:]]*'[^']*');|define('SHARE_URL', '$SHARE_URL');|" "$CONFIG_FILE"
|
|
||||||
fi
|
fi
|
||||||
|
[ -n "${SECURE:-}" ] && sed -i "s|\$envSecure = getenv('SECURE');|\$envSecure = '${SECURE}';|" "${CONFIG_FILE}"
|
||||||
|
[ -n "${SHARE_URL:-}" ] && sed -i "s|define('SHARE_URL',[[:space:]]*'[^']*');|define('SHARE_URL', '${SHARE_URL}');|" "${CONFIG_FILE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure the PHP configuration directory exists
|
# 2.1) Prepare metadata/log for Apache logs
|
||||||
|
mkdir -p /var/www/metadata/log
|
||||||
|
chown www-data:www-data /var/www/metadata/log
|
||||||
|
chmod 775 /var/www/metadata/log
|
||||||
|
|
||||||
|
mkdir -p /var/www/sessions
|
||||||
|
chown www-data:www-data /var/www/sessions
|
||||||
|
chmod 700 /var/www/sessions
|
||||||
|
|
||||||
|
# 2.2) Prepare other dynamic dirs
|
||||||
|
for d in uploads users metadata; do
|
||||||
|
tgt="/var/www/${d}"
|
||||||
|
mkdir -p "${tgt}"
|
||||||
|
chown www-data:www-data "${tgt}"
|
||||||
|
chmod 775 "${tgt}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3) Ensure PHP config dir & set upload limits
|
||||||
mkdir -p /etc/php/8.3/apache2/conf.d
|
mkdir -p /etc/php/8.3/apache2/conf.d
|
||||||
|
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||||
# Update PHP upload limits at runtime if TOTAL_UPLOAD_SIZE is set.
|
echo "🔄 Setting PHP upload limits to ${TOTAL_UPLOAD_SIZE}"
|
||||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
cat > /etc/php/8.3/apache2/conf.d/99-custom.ini <<EOF
|
||||||
echo "🔄 Updating PHP upload limits with TOTAL_UPLOAD_SIZE=$TOTAL_UPLOAD_SIZE"
|
upload_max_filesize = ${TOTAL_UPLOAD_SIZE}
|
||||||
echo "upload_max_filesize = $TOTAL_UPLOAD_SIZE" > /etc/php/8.3/apache2/conf.d/99-custom.ini
|
post_max_size = ${TOTAL_UPLOAD_SIZE}
|
||||||
echo "post_max_size = $TOTAL_UPLOAD_SIZE" >> /etc/php/8.3/apache2/conf.d/99-custom.ini
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update Apache LimitRequestBody based on TOTAL_UPLOAD_SIZE if set.
|
# 4) Adjust Apache LimitRequestBody
|
||||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||||
size_str=$(echo "$TOTAL_UPLOAD_SIZE" | tr '[:upper:]' '[:lower:]')
|
# convert to bytes
|
||||||
factor=1
|
size_str=$(echo "${TOTAL_UPLOAD_SIZE}" | tr '[:upper:]' '[:lower:]')
|
||||||
case "${size_str: -1}" in
|
case "${size_str: -1}" in
|
||||||
g)
|
g) factor=$((1024*1024*1024)); num=${size_str%g} ;;
|
||||||
factor=$((1024*1024*1024))
|
m) factor=$((1024*1024)); num=${size_str%m} ;;
|
||||||
size_num=${size_str%g}
|
k) factor=1024; num=${size_str%k} ;;
|
||||||
;;
|
*) factor=1; num=${size_str} ;;
|
||||||
m)
|
|
||||||
factor=$((1024*1024))
|
|
||||||
size_num=${size_str%m}
|
|
||||||
;;
|
|
||||||
k)
|
|
||||||
factor=1024
|
|
||||||
size_num=${size_str%k}
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
size_num=$size_str
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
LIMIT_REQUEST_BODY=$((size_num * factor))
|
LIMIT_REQUEST_BODY=$(( num * factor ))
|
||||||
echo "🔄 Setting Apache LimitRequestBody to $LIMIT_REQUEST_BODY bytes (from TOTAL_UPLOAD_SIZE=$TOTAL_UPLOAD_SIZE)"
|
echo "🔄 Setting Apache LimitRequestBody to ${LIMIT_REQUEST_BODY} bytes"
|
||||||
cat <<EOF > /etc/apache2/conf-enabled/limit_request_body.conf
|
cat > /etc/apache2/conf-enabled/limit_request_body.conf <<EOF
|
||||||
<Directory "/var/www/public">
|
<Directory "/var/www/public">
|
||||||
LimitRequestBody $LIMIT_REQUEST_BODY
|
LimitRequestBody ${LIMIT_REQUEST_BODY}
|
||||||
</Directory>
|
</Directory>
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set Apache Timeout (default is 300 seconds)
|
# 5) Configure Apache timeout (600s)
|
||||||
echo "🔄 Setting Apache Timeout to 600 seconds"
|
cat > /etc/apache2/conf-enabled/timeout.conf <<EOF
|
||||||
cat <<EOF > /etc/apache2/conf-enabled/timeout.conf
|
|
||||||
Timeout 600
|
Timeout 600
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "🔥 Final Apache Timeout configuration:"
|
# 6) Override ports if provided
|
||||||
cat /etc/apache2/conf-enabled/timeout.conf
|
if [ -n "${HTTP_PORT:-}" ]; then
|
||||||
|
sed -i "s/^Listen 80$/Listen ${HTTP_PORT}/" /etc/apache2/ports.conf
|
||||||
# Update Apache ports if environment variables are provided
|
sed -i "s/<VirtualHost \*:80>/<VirtualHost *:${HTTP_PORT}>/" /etc/apache2/sites-available/000-default.conf
|
||||||
if [ -n "$HTTP_PORT" ]; then
|
fi
|
||||||
echo "🔄 Setting Apache HTTP port to $HTTP_PORT"
|
if [ -n "${HTTPS_PORT:-}" ]; then
|
||||||
sed -i "s/^Listen 80$/Listen $HTTP_PORT/" /etc/apache2/ports.conf
|
sed -i "s/^Listen 443$/Listen ${HTTPS_PORT}/" /etc/apache2/ports.conf
|
||||||
sed -i "s/<VirtualHost \*:80>/<VirtualHost *:$HTTP_PORT>/" /etc/apache2/sites-available/000-default.conf
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$HTTPS_PORT" ]; then
|
# 7) Set ServerName
|
||||||
echo "🔄 Setting Apache HTTPS port to $HTTPS_PORT"
|
if [ -n "${SERVER_NAME:-}" ]; then
|
||||||
sed -i "s/^Listen 443$/Listen $HTTPS_PORT/" /etc/apache2/ports.conf
|
echo "ServerName ${SERVER_NAME}" >> /etc/apache2/apache2.conf
|
||||||
fi
|
|
||||||
|
|
||||||
# Update Apache ServerName if environment variable is provided
|
|
||||||
if [ -n "$SERVER_NAME" ]; then
|
|
||||||
echo "🔄 Setting Apache ServerName to $SERVER_NAME"
|
|
||||||
echo "ServerName $SERVER_NAME" >> /etc/apache2/apache2.conf
|
|
||||||
else
|
else
|
||||||
echo "🔄 Setting Apache ServerName to default: FileRise"
|
|
||||||
echo "ServerName FileRise" >> /etc/apache2/apache2.conf
|
echo "ServerName FileRise" >> /etc/apache2/apache2.conf
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Final /etc/apache2/ports.conf content:"
|
# 8) Prepare dynamic data directories with least privilege
|
||||||
cat /etc/apache2/ports.conf
|
for d in uploads users metadata; do
|
||||||
|
tgt="/var/www/${d}"
|
||||||
|
mkdir -p "${tgt}"
|
||||||
|
chown www-data:www-data "${tgt}"
|
||||||
|
chmod 775 "${tgt}"
|
||||||
|
done
|
||||||
|
|
||||||
echo "📁 Web app is served from /var/www/public."
|
# 9) Initialize persistent files if absent
|
||||||
|
|
||||||
# Ensure the uploads folder exists in /var/www
|
|
||||||
mkdir -p /var/www/uploads
|
|
||||||
echo "🔑 Fixing permissions for /var/www/uploads..."
|
|
||||||
chown -R ${PUID:-99}:${PGID:-100} /var/www/uploads
|
|
||||||
chmod -R 775 /var/www/uploads
|
|
||||||
|
|
||||||
# Ensure the users folder exists in /var/www
|
|
||||||
mkdir -p /var/www/users
|
|
||||||
echo "🔑 Fixing permissions for /var/www/users..."
|
|
||||||
chown -R ${PUID:-99}:${PGID:-100} /var/www/users
|
|
||||||
chmod -R 775 /var/www/users
|
|
||||||
|
|
||||||
# Ensure the metadata folder exists in /var/www
|
|
||||||
mkdir -p /var/www/metadata
|
|
||||||
echo "🔑 Fixing permissions for /var/www/metadata..."
|
|
||||||
chown -R ${PUID:-99}:${PGID:-100} /var/www/metadata
|
|
||||||
chmod -R 775 /var/www/metadata
|
|
||||||
|
|
||||||
# Create users.txt only if it doesn't already exist (preserving persistent data)
|
|
||||||
if [ ! -f /var/www/users/users.txt ]; then
|
if [ ! -f /var/www/users/users.txt ]; then
|
||||||
echo "ℹ️ users.txt not found in persistent storage; creating new file..."
|
|
||||||
echo "" > /var/www/users/users.txt
|
echo "" > /var/www/users/users.txt
|
||||||
chown ${PUID:-99}:${PGID:-100} /var/www/users/users.txt
|
chown www-data:www-data /var/www/users/users.txt
|
||||||
chmod 664 /var/www/users/users.txt
|
chmod 664 /var/www/users/users.txt
|
||||||
else
|
|
||||||
echo "ℹ️ users.txt already exists; preserving persistent data."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create createdTags.json only if it doesn't already exist (preserving persistent data)
|
|
||||||
if [ ! -f /var/www/metadata/createdTags.json ]; then
|
if [ ! -f /var/www/metadata/createdTags.json ]; then
|
||||||
echo "ℹ️ createdTags.json not found in persistent storage; creating new file..."
|
|
||||||
echo "[]" > /var/www/metadata/createdTags.json
|
echo "[]" > /var/www/metadata/createdTags.json
|
||||||
chown ${PUID:-99}:${PGID:-100} /var/www/metadata/createdTags.json
|
chown www-data:www-data /var/www/metadata/createdTags.json
|
||||||
chmod 664 /var/www/metadata/createdTags.json
|
chmod 664 /var/www/metadata/createdTags.json
|
||||||
else
|
|
||||||
echo "ℹ️ createdTags.json already exists; preserving persistent data."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Optionally, fix permissions for the rest of /var/www
|
|
||||||
echo "🔑 Fixing permissions for /var/www..."
|
|
||||||
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 Apache configuration (limit_request_body.conf):"
|
|
||||||
cat /etc/apache2/conf-enabled/limit_request_body.conf
|
|
||||||
|
|
||||||
echo "🔥 Starting Apache..."
|
echo "🔥 Starting Apache..."
|
||||||
exec apachectl -D FOREGROUND
|
exec apachectl -D FOREGROUND
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<IfModule mod_php7.c>
|
|
||||||
php_flag engine off
|
|
||||||
</IfModule>
|
|
||||||
<IfModule mod_php.c>
|
|
||||||
php_flag engine off
|
|
||||||
</IfModule>
|
|
||||||
Options -Indexes
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<Files "users.txt">
|
|
||||||
Require all denied
|
|
||||||
</Files>
|
|
||||||
Reference in New Issue
Block a user