Compare commits
21 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 |
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
|
||||||
2
.gitattributes
vendored
2
.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
|
||||||
|
|||||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,5 +1,101 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
## Changes 4/21/2025 v1.2.2
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
129
Dockerfile
129
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,88 +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 \
|
apache2 php php-json php-curl php-zip php-mbstring php-gd php-xml \
|
||||||
php \
|
ca-certificates curl git openssl && \
|
||||||
php-json \
|
apt-get clean && rm -rf /var/lib/apt/lists/* # slim down image
|
||||||
php-curl \
|
|
||||||
php-zip \
|
|
||||||
php-mbstring \
|
|
||||||
php-gd \
|
|
||||||
php-xml \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
openssl && \
|
|
||||||
apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Fix www-data UID/GID
|
# Remap www-data to the PUID/PGID provided 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
|
||||||
|
|||||||
20
README.md
20
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,7 +20,7 @@ Upload, organize, and share files through a sleek web interface. **FileRise** is
|
|||||||
|
|
||||||
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) – convenient for sending individual files without exposing the whole app.
|
- 🗃️ **Folder Sharing & File Sharing:** Easily share entire folders via secure, expiring public links. Folder shares can be password-protected, and shared folders support file uploads from outside users with a separate, secure upload mechanism. Folder listings are paginated (10 items per page) with navigation controls, and file sizes are displayed in MB for clarity. Share files with others using one-time or expiring public links (with password protection if desired) – convenient for sending individual files without exposing the whole app.
|
||||||
|
|
||||||
- 🔌 **WebDAV Support:** Mount FileRise as a network drive or connect via any WebDAV client. Supports standard file operations (upload/download/rename/delete) and direct `curl`/CLI access for scripting and automation. FolderOnly users are restricted to their personal folder, while admins and unrestricted users have full access. Compatible with Cyberduck, WinSCP, native OS drive mounts, and more.
|
- 🔌 **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.
|
||||||
|
|
||||||
@@ -115,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):
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ Now navigate to the FileRise URL in your browser. On first load, you’ll be pro
|
|||||||
|
|
||||||
## Quick‑start: Mount via WebDAV
|
## Quick‑start: Mount via WebDAV
|
||||||
|
|
||||||
Once FileRise is running, you can mount it like any other network drive:
|
Once FileRise is running, you must enable WebDAV in admin panel to access it.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux (GVFS/GIO)
|
# Linux (GVFS/GIO)
|
||||||
@@ -232,19 +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)
|
- **[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!
|
||||||
|
|||||||
@@ -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.2"; // 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 {
|
||||||
|
|||||||
@@ -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,6 +337,38 @@ 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.
|
||||||
@@ -418,9 +454,8 @@ export async function loadFolderTree(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();
|
||||||
@@ -434,6 +469,7 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
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) {
|
||||||
@@ -472,6 +509,7 @@ 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();
|
||||||
@@ -500,6 +538,7 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
// public/webdav.php
|
// public/webdav.php
|
||||||
|
|
||||||
|
// ─── 0) Forward Basic auth into PHP_AUTH_* for every HTTP verb ─────────────
|
||||||
if (
|
if (
|
||||||
empty($_SERVER['PHP_AUTH_USER'])
|
empty($_SERVER['PHP_AUTH_USER'])
|
||||||
&& !empty($_SERVER['HTTP_AUTHORIZATION'])
|
&& !empty($_SERVER['HTTP_AUTHORIZATION'])
|
||||||
@@ -11,46 +12,58 @@ if (
|
|||||||
$_SERVER['PHP_AUTH_PW'] = $p;
|
$_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__ . '/../config/config.php'; // UPLOAD_DIR, META_DIR, DATE_TIME_FORMAT
|
||||||
require_once __DIR__ . '/../vendor/autoload.php'; // Composer & SabreDAV
|
require_once __DIR__ . '/../vendor/autoload.php'; // Composer & SabreDAV
|
||||||
require_once __DIR__ . '/../src/models/AuthModel.php'; // AuthModel::authenticate(), getUserRole(), loadFolderPermission()
|
require_once __DIR__ . '/../src/models/AuthModel.php'; // AuthModel::authenticate(), getUserRole(), loadFolderPermission()
|
||||||
|
require_once __DIR__ . '/../src/models/AdminModel.php'; // AdminModel::getConfig()
|
||||||
|
|
||||||
// ─── 3) Load your WebDAV directory implementation ──────────────────────────
|
// ─── 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';
|
require_once __DIR__ . '/../src/webdav/FileRiseDirectory.php';
|
||||||
use Sabre\DAV\Server;
|
use Sabre\DAV\Server;
|
||||||
use Sabre\DAV\Auth\Backend\BasicCallBack;
|
use Sabre\DAV\Auth\Backend\BasicCallBack;
|
||||||
use Sabre\DAV\Auth\Plugin as AuthPlugin;
|
use Sabre\DAV\Auth\Plugin as AuthPlugin;
|
||||||
use Sabre\DAV\Browser\Plugin as BrowserPlugin;
|
|
||||||
use Sabre\DAV\Locks\Plugin as LocksPlugin;
|
use Sabre\DAV\Locks\Plugin as LocksPlugin;
|
||||||
use Sabre\DAV\Locks\Backend\File as LocksFileBackend;
|
use Sabre\DAV\Locks\Backend\File as LocksFileBackend;
|
||||||
use FileRise\WebDAV\FileRiseDirectory;
|
use FileRise\WebDAV\FileRiseDirectory;
|
||||||
|
|
||||||
|
// ─── 3) HTTP‑Basic backend ─────────────────────────────────────────────────
|
||||||
$authBackend = new BasicCallBack(function(string $user, string $pass) {
|
$authBackend = new BasicCallBack(function(string $user, string $pass) {
|
||||||
return \AuthModel::authenticate($user, $pass) !== false;
|
return \AuthModel::authenticate($user, $pass) !== false;
|
||||||
});
|
});
|
||||||
$authPlugin = new AuthPlugin($authBackend, 'FileRise');
|
$authPlugin = new AuthPlugin($authBackend, 'FileRise');
|
||||||
|
|
||||||
|
// ─── 4) Determine user scope ────────────────────────────────────────────────
|
||||||
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
|
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
|
||||||
$isAdmin = (\AuthModel::getUserRole($user) === '1');
|
$isAdmin = (\AuthModel::getUserRole($user) === '1');
|
||||||
$folderOnly = (bool)\AuthModel::loadFolderPermission($user);
|
$folderOnly = (bool)\AuthModel::loadFolderPermission($user);
|
||||||
|
|
||||||
if ($isAdmin || !$folderOnly) {
|
if ($isAdmin || !$folderOnly) {
|
||||||
// admins or unrestricted users see the full /uploads
|
// Admins (or users without folder-only restriction) see the full /uploads
|
||||||
$rootPath = rtrim(UPLOAD_DIR, '/\\');
|
$rootPath = rtrim(UPLOAD_DIR, '/\\');
|
||||||
} else {
|
} else {
|
||||||
// folder‑only users see only /uploads/{username}
|
// Folder‑only users see only /uploads/{username}
|
||||||
$rootPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $user;
|
$rootPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $user;
|
||||||
if (!is_dir($rootPath)) {
|
if (!is_dir($rootPath)) {
|
||||||
mkdir($rootPath, 0755, true);
|
mkdir($rootPath, 0755, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 5) Spin up SabreDAV ────────────────────────────────────────────────────
|
||||||
$server = new Server([
|
$server = new Server([
|
||||||
new FileRiseDirectory($rootPath, $user, $folderOnly),
|
new FileRiseDirectory($rootPath, $user, $folderOnly),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$server->addPlugin($authPlugin);
|
$server->addPlugin($authPlugin);
|
||||||
//$server->addPlugin(new BrowserPlugin()); // optional HTML browser UI
|
|
||||||
$server->addPlugin(
|
$server->addPlugin(
|
||||||
new LocksPlugin(
|
new LocksPlugin(
|
||||||
new LocksFileBackend(sys_get_temp_dir() . '/sabre-locksdb')
|
new LocksFileBackend(sys_get_temp_dir() . '/sabre-locksdb')
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'); ?>">
|
||||||
|
|||||||
@@ -73,33 +73,55 @@ 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);
|
||||||
@@ -110,7 +132,7 @@ class UploadController {
|
|||||||
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);
|
||||||
@@ -73,9 +113,8 @@ class AdminModel
|
|||||||
$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,
|
||||||
@@ -89,29 +128,41 @@ class AdminModel
|
|||||||
$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) {
|
||||||
@@ -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;
|
||||||
@@ -58,7 +61,8 @@ class AuthModel {
|
|||||||
* @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)) {
|
||||||
@@ -75,7 +79,8 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +90,8 @@ class AuthModel {
|
|||||||
* @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;
|
||||||
@@ -105,7 +111,8 @@ class AuthModel {
|
|||||||
* @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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
192
start.sh
192
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