Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06b3f28df0 | ||
|
|
89f124250c | ||
|
|
66f13fd6a7 | ||
|
|
a81d9cb940 | ||
|
|
13b8871200 | ||
|
|
2792c05c1c | ||
|
|
6ccfc88acb | ||
|
|
7f1d59b33a | ||
|
|
e4e8b108d2 |
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
||||
# dockerignore
|
||||
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
.github/**
|
||||
Dockerfile*
|
||||
resources/
|
||||
node_modules/
|
||||
*.log
|
||||
tmp/
|
||||
.env
|
||||
.vscode/
|
||||
.DS_Store
|
||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,2 +1,4 @@
|
||||
public/api.html linguist-documentation
|
||||
public/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:
|
||||
- 'CHANGELOG.md'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
## 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`
|
||||
@@ -21,6 +51,16 @@
|
||||
- `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
|
||||
|
||||
---
|
||||
|
||||
|
||||
86
Dockerfile
86
Dockerfile
@@ -6,12 +6,9 @@
|
||||
FROM ubuntu:24.04 AS appsource
|
||||
RUN apt-get update && \
|
||||
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
|
||||
|
||||
# **Copy the FileRise source** (where your composer.json lives)
|
||||
COPY . /var/www
|
||||
|
||||
#############################
|
||||
@@ -19,94 +16,73 @@ COPY . /var/www
|
||||
#############################
|
||||
FROM composer:2 AS composer
|
||||
WORKDIR /app
|
||||
|
||||
# **Copy composer files from the source** and install
|
||||
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
|
||||
#############################
|
||||
FROM ubuntu:24.04
|
||||
|
||||
LABEL by=error311
|
||||
|
||||
# Set basic environment variables (these can be overridden via the Unraid template)
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
HOME=/root \
|
||||
LC_ALL=C.UTF-8 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LANGUAGE=en_US.UTF-8 \
|
||||
TERM=xterm \
|
||||
UPLOAD_MAX_FILESIZE=5G \
|
||||
POST_MAX_SIZE=5G \
|
||||
TOTAL_UPLOAD_SIZE=5G \
|
||||
LC_ALL=C.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 TERM=xterm \
|
||||
UPLOAD_MAX_FILESIZE=5G POST_MAX_SIZE=5G TOTAL_UPLOAD_SIZE=5G \
|
||||
PERSISTENT_TOKENS_KEY=default_please_change_this_key \
|
||||
PUID=99 \
|
||||
PGID=100
|
||||
PUID=99 PGID=100
|
||||
|
||||
# Install Apache, PHP, and required extensions
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
apache2 \
|
||||
php \
|
||||
php-json \
|
||||
php-curl \
|
||||
php-zip \
|
||||
php-mbstring \
|
||||
php-gd \
|
||||
php-xml \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
openssl && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
apache2 php php-json php-curl php-zip php-mbstring php-gd php-xml \
|
||||
ca-certificates curl git openssl && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* # slim down image
|
||||
|
||||
# Remap www-data to the PUID/PGID provided
|
||||
# Remap www-data to the PUID/PGID provided for safe bind mounts
|
||||
RUN set -eux; \
|
||||
# only change the UID if it’s not already correct
|
||||
if [ "$(id -u www-data)" != "${PUID}" ]; then \
|
||||
usermod -u "${PUID}" www-data; \
|
||||
fi; \
|
||||
# attempt to change the GID, but ignore “already exists” errors
|
||||
if [ "$(id -g www-data)" != "${PGID}" ]; then \
|
||||
groupmod -g "${PGID}" www-data 2>/dev/null || true; \
|
||||
fi; \
|
||||
# finally set www-data’s primary group to PGID (will succeed if the group exists)
|
||||
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 2>/dev/null || true; fi; \
|
||||
usermod -g "${PGID}" www-data
|
||||
|
||||
# Copy application tuning and code
|
||||
# Copy config, code, and vendor
|
||||
COPY custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
|
||||
COPY --from=appsource /var/www /var/www
|
||||
COPY --from=composer /app/vendor /var/www/vendor
|
||||
COPY --from=composer /app/vendor /var/www/vendor
|
||||
|
||||
# Ensure the webroot is owned by the remapped www-data user
|
||||
RUN chown -R www-data:www-data /var/www && chmod -R 775 /var/www
|
||||
# Secure permissions: code read-only, only data dirs writable
|
||||
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.
|
||||
RUN cd /var/www/public && ln -s ../uploads uploads
|
||||
|
||||
# Configure Apache
|
||||
# Apache site configuration
|
||||
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
||||
<VirtualHost *:80>
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/public
|
||||
Alias /uploads/ /var/www/uploads/
|
||||
<Directory "/var/www/uploads/">
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
<Directory "/var/www/public">
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
DirectoryIndex index.php index.html
|
||||
DirectoryIndex index.html
|
||||
</Directory>
|
||||
ErrorLog /var/log/apache2/error.log
|
||||
CustomLog /var/log/apache2/access.log combined
|
||||
ErrorLog /var/www/metadata/log/error.log
|
||||
CustomLog /var/www/metadata/log/access.log combined
|
||||
</VirtualHost>
|
||||
EOF
|
||||
|
||||
# Enable the rewrite and headers modules
|
||||
# Enable required modules
|
||||
RUN a2enmod rewrite headers
|
||||
|
||||
# Expose ports and set up the startup script
|
||||
EXPOSE 80 443
|
||||
COPY start.sh /usr/local/bin/start.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
1
LICENSE
1
LICENSE
@@ -1,5 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 SeNS
|
||||
Copyright (c) 2025 FileRise
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
10
README.md
10
README.md
@@ -117,7 +117,7 @@ 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).
|
||||
|
||||
- **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):
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
# Linux (GVFS/GIO)
|
||||
@@ -245,6 +245,12 @@ Areas where you can help: translations, bug fixes, UI improvements, or building
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Based on [uploader](https://github.com/sensboston/uploader) by @sensboston.
|
||||
|
||||
---
|
||||
|
||||
## 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!
|
||||
|
||||
@@ -41,6 +41,7 @@ upload_tmp_dir=/tmp
|
||||
session.gc_maxlifetime=1440
|
||||
session.gc_probability=1
|
||||
session.gc_divisor=100
|
||||
session.save_path = "/var/www/sessions"
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Error Handling / Logging
|
||||
|
||||
@@ -44,6 +44,55 @@ function showToast(msgKey) {
|
||||
}
|
||||
window.showToast = showToast;
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
|
||||
/*
|
||||
* @param {string} url
|
||||
* @param {object} options
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
export async function fetchWithCsrf(url, options = {}) {
|
||||
options = { credentials: 'include', headers: {}, ...options };
|
||||
options.headers['X-CSRF-Token'] = window.csrfToken;
|
||||
|
||||
// 1) First attempt using the original fetch
|
||||
let res = await originalFetch(url, options);
|
||||
|
||||
// 2) Soft‐failure JSON check (200 + {csrf_expired})
|
||||
if (res.ok && res.headers.get('content-type')?.includes('application/json')) {
|
||||
const clone = res.clone();
|
||||
const data = await clone.json();
|
||||
if (data.csrf_expired) {
|
||||
const newToken = data.csrf_token;
|
||||
window.csrfToken = newToken;
|
||||
document.querySelector('meta[name="csrf-token"]').content = newToken;
|
||||
options.headers['X-CSRF-Token'] = newToken;
|
||||
return originalFetch(url, options);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) HTTP 403 fallback
|
||||
if (res.status === 403) {
|
||||
let newToken = res.headers.get('X-CSRF-Token');
|
||||
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) {
|
||||
window.csrfToken = newToken;
|
||||
document.querySelector('meta[name="csrf-token"]').content = newToken;
|
||||
options.headers['X-CSRF-Token'] = newToken;
|
||||
res = await originalFetch(url, options);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows
|
||||
function openTOTPLoginModal() {
|
||||
originalOpenTOTPLoginModal();
|
||||
@@ -228,6 +277,7 @@ function checkAuthentication(showLoginToast = true) {
|
||||
}
|
||||
window.setupMode = false;
|
||||
if (data.authenticated) {
|
||||
localStorage.setItem('isAdmin', data.isAdmin ? 'true' : 'false');
|
||||
localStorage.setItem("folderOnly", data.folderOnly);
|
||||
localStorage.setItem("readOnly", data.readOnly);
|
||||
localStorage.setItem("disableUpload", data.disableUpload);
|
||||
@@ -235,6 +285,10 @@ function checkAuthentication(showLoginToast = true) {
|
||||
if (typeof data.totp_enabled !== "undefined") {
|
||||
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);
|
||||
return data;
|
||||
} else {
|
||||
@@ -276,11 +330,11 @@ async function submitLogin(data) {
|
||||
try {
|
||||
const perm = await sendRequest("/api/getUserPermissions.php", "GET");
|
||||
if (perm && typeof perm === "object") {
|
||||
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
||||
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
||||
localStorage.setItem("disableUpload",perm.disableUpload? "true" : "false");
|
||||
localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
|
||||
localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
|
||||
localStorage.setItem("disableUpload", perm.disableUpload ? "true" : "false");
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
return window.location.reload();
|
||||
}
|
||||
|
||||
@@ -405,10 +459,10 @@ function initAuth() {
|
||||
}
|
||||
let url = "/api/addUser.php";
|
||||
if (window.setupMode) url += "?setup=1";
|
||||
fetch(url, {
|
||||
fetchWithCsrf(url, {
|
||||
method: "POST",
|
||||
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 })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -438,10 +492,10 @@ function initAuth() {
|
||||
}
|
||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||
if (!confirmed) return;
|
||||
fetch("/api/removeUser.php", {
|
||||
fetchWithCsrf("/api/removeUser.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: usernameToRemove })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -477,10 +531,10 @@ function initAuth() {
|
||||
return;
|
||||
}
|
||||
const data = { oldPassword, newPassword, confirmPassword };
|
||||
fetch("/api/changePassword.php", {
|
||||
fetchWithCsrf("/api/changePassword.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -3,7 +3,7 @@ import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
import { loadAdminConfigFunc } from './auth.js';
|
||||
|
||||
const version = "v1.2.3"; // Update this version string as needed
|
||||
const version = "v1.2.4"; // Update this version string as needed
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
|
||||
let lastLoginData = null;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { loadFileList } from './fileListView.js';
|
||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { openFolderShareModal } from './folderShareModal.js';
|
||||
import { fetchWithCsrf } from './auth.js';
|
||||
import { loadCsrfToken } from './main.js';
|
||||
|
||||
/* ----------------------
|
||||
Helper Functions (Data/State)
|
||||
@@ -337,7 +339,7 @@ export async function loadFolderTree(selectedFolder) {
|
||||
try {
|
||||
// Check if the user has folder-only permission.
|
||||
await checkUserFolderPermission();
|
||||
|
||||
|
||||
// Determine effective root folder.
|
||||
const username = localStorage.getItem("username") || "root";
|
||||
let effectiveRoot = "root";
|
||||
@@ -351,14 +353,14 @@ export async function loadFolderTree(selectedFolder) {
|
||||
} else {
|
||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||
}
|
||||
|
||||
|
||||
// Build fetch URL.
|
||||
let fetchUrl = '/api/folder/getFolderList.php';
|
||||
if (window.userFolderOnly) {
|
||||
fetchUrl += '?restricted=1';
|
||||
}
|
||||
console.log("Fetching folder list from:", fetchUrl);
|
||||
|
||||
|
||||
// Fetch folder list from the server.
|
||||
const response = await fetch(fetchUrl);
|
||||
if (response.status === 401) {
|
||||
@@ -375,10 +377,10 @@ export async function loadFolderTree(selectedFolder) {
|
||||
} else if (Array.isArray(folderData)) {
|
||||
folders = folderData;
|
||||
}
|
||||
|
||||
|
||||
// Remove any global "root" entry.
|
||||
folders = folders.filter(folder => folder.toLowerCase() !== "root");
|
||||
|
||||
|
||||
// If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself).
|
||||
if (window.userFolderOnly && effectiveRoot !== "root") {
|
||||
folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/"));
|
||||
@@ -386,16 +388,16 @@ export async function loadFolderTree(selectedFolder) {
|
||||
localStorage.setItem("lastOpenedFolder", effectiveRoot);
|
||||
window.currentFolder = effectiveRoot;
|
||||
}
|
||||
|
||||
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
|
||||
// Render the folder tree.
|
||||
const container = document.getElementById("folderTreeContainer");
|
||||
if (!container) {
|
||||
console.error("Folder tree container not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let html = `<div id="rootRow" class="root-row">
|
||||
<span class="folder-toggle" data-folder="${effectiveRoot}">[<span class="custom-dash">-</span>]</span>
|
||||
<span class="folder-option root-folder-option" data-folder="${effectiveRoot}">${effectiveLabel}</span>
|
||||
@@ -405,35 +407,35 @@ export async function loadFolderTree(selectedFolder) {
|
||||
html += renderFolderTree(tree, "", "block");
|
||||
}
|
||||
container.innerHTML = html;
|
||||
|
||||
|
||||
// Attach drag/drop event listeners.
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("dragover", folderDragOverHandler);
|
||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||
el.addEventListener("drop", folderDropHandler);
|
||||
});
|
||||
|
||||
|
||||
if (selectedFolder) {
|
||||
window.currentFolder = selectedFolder;
|
||||
}
|
||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||
|
||||
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")";
|
||||
setupBreadcrumbDelegation();
|
||||
loadFileList(window.currentFolder);
|
||||
|
||||
|
||||
const folderState = loadFolderTreeState();
|
||||
if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") {
|
||||
expandTreePath(window.currentFolder);
|
||||
}
|
||||
|
||||
|
||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||
if (selectedEl) {
|
||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
selectedEl.classList.add("selected");
|
||||
}
|
||||
|
||||
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
el.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -448,7 +450,7 @@ export async function loadFolderTree(selectedFolder) {
|
||||
loadFileList(selected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const rootToggle = container.querySelector("#rootRow .folder-toggle");
|
||||
if (rootToggle) {
|
||||
rootToggle.addEventListener("click", function (e) {
|
||||
@@ -471,7 +473,7 @@ export async function loadFolderTree(selectedFolder) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
container.querySelectorAll(".folder-toggle").forEach(toggle => {
|
||||
toggle.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -494,7 +496,7 @@ export async function loadFolderTree(selectedFolder) {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading folder tree:", error);
|
||||
}
|
||||
@@ -627,45 +629,53 @@ document.getElementById("cancelCreateFolder").addEventListener("click", function
|
||||
document.getElementById("newFolderName").value = "";
|
||||
});
|
||||
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
||||
document.getElementById("submitCreateFolder").addEventListener("click", function () {
|
||||
document.getElementById("submitCreateFolder").addEventListener("click", async () => {
|
||||
const folderInput = document.getElementById("newFolderName").value.trim();
|
||||
if (!folderInput) {
|
||||
showToast("Please enter a folder name.");
|
||||
return;
|
||||
if (!folderInput) return showToast("Please enter a folder name.");
|
||||
|
||||
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;
|
||||
if (selectedFolder && selectedFolder !== "root") {
|
||||
fullFolderName = selectedFolder + "/" + folderInput;
|
||||
}
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
fetch("/api/folder/createFolder.php", {
|
||||
|
||||
// 2) Call with fetchWithCsrf
|
||||
fetchWithCsrf("/api/folder/createFolder.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folderName: folderInput,
|
||||
parent: selectedFolder === "root" ? "" : selectedFolder
|
||||
})
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folderName: folderInput, parent })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast("Folder created successfully!");
|
||||
window.currentFolder = fullFolderName;
|
||||
localStorage.setItem("lastOpenedFolder", fullFolderName);
|
||||
loadFolderList(fullFolderName);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not create folder"));
|
||||
.then(async res => {
|
||||
if (!res.ok) {
|
||||
// pull out a JSON error, or fallback to status text
|
||||
let err;
|
||||
try {
|
||||
const j = await res.json();
|
||||
err = j.error || j.message || res.statusText;
|
||||
} catch {
|
||||
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("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 { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
||||
import { loadFolderTree } from './folderManager.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 { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||
@@ -13,35 +15,53 @@ import { editFile, saveFile } from './fileEditor.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
|
||||
// Remove the retry logic version and just use loadCsrfToken directly:
|
||||
function loadCsrfToken() {
|
||||
return fetch('/api/auth/token.php', { credentials: 'include' })
|
||||
/**
|
||||
* Fetches the current CSRF token (and share URL), updates window globals
|
||||
* and <meta> tags, and returns the data.
|
||||
*
|
||||
* @returns {Promise<{csrf_token: string, share_url: string}>}
|
||||
*/
|
||||
export function loadCsrfToken() {
|
||||
return fetch('/api/auth/token.php', {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Token fetch failed with status: " + response.status);
|
||||
throw new Error(`Token fetch failed with status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
// Prefer header if set, otherwise fall back to body
|
||||
const headerToken = response.headers.get('X-CSRF-Token');
|
||||
return response.json()
|
||||
.then(body => ({
|
||||
csrf_token: headerToken || body.csrf_token,
|
||||
share_url: body.share_url
|
||||
}));
|
||||
})
|
||||
.then(data => {
|
||||
window.csrfToken = data.csrf_token;
|
||||
window.SHARE_URL = data.share_url;
|
||||
|
||||
let metaCSRF = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!metaCSRF) {
|
||||
metaCSRF = document.createElement('meta');
|
||||
metaCSRF.name = 'csrf-token';
|
||||
document.head.appendChild(metaCSRF);
|
||||
}
|
||||
metaCSRF.setAttribute('content', data.csrf_token);
|
||||
.then(({ csrf_token, share_url }) => {
|
||||
// Update globals
|
||||
window.csrfToken = csrf_token;
|
||||
window.SHARE_URL = share_url;
|
||||
|
||||
let metaShare = document.querySelector('meta[name="share-url"]');
|
||||
if (!metaShare) {
|
||||
metaShare = document.createElement('meta');
|
||||
metaShare.name = 'share-url';
|
||||
document.head.appendChild(metaShare);
|
||||
// Sync <meta name="csrf-token">
|
||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
metaShare.setAttribute('content', data.share_url);
|
||||
meta.content = csrf_token;
|
||||
|
||||
return data;
|
||||
// Sync <meta name="share-url">
|
||||
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||||
if (!shareMeta) {
|
||||
shareMeta = document.createElement('meta');
|
||||
shareMeta.name = 'share-url';
|
||||
document.head.appendChild(shareMeta);
|
||||
}
|
||||
shareMeta.content = share_url;
|
||||
|
||||
return { csrf_token, share_url };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -412,7 +412,12 @@ function initResumableUpload() {
|
||||
forceChunkSize: true,
|
||||
testChunks: false,
|
||||
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");
|
||||
@@ -496,26 +501,40 @@ function initResumableUpload() {
|
||||
});
|
||||
|
||||
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) {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerText = "Done";
|
||||
// Hide pause/resume and remove buttons for successful files.
|
||||
// remove action buttons
|
||||
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
||||
if (pauseResumeBtn) {
|
||||
pauseResumeBtn.style.display = "none";
|
||||
}
|
||||
if (pauseResumeBtn) pauseResumeBtn.style.display = "none";
|
||||
const removeBtn = li.querySelector(".remove-file-btn");
|
||||
if (removeBtn) {
|
||||
removeBtn.style.display = "none";
|
||||
}
|
||||
// Schedule removal of the file entry after 5 seconds.
|
||||
setTimeout(() => {
|
||||
li.remove();
|
||||
window.selectedFiles = window.selectedFiles.filter(f => f.uniqueIdentifier !== file.uniqueIdentifier);
|
||||
updateFileInfoCount();
|
||||
}, 5000);
|
||||
if (removeBtn) removeBtn.style.display = "none";
|
||||
setTimeout(() => li.remove(), 5000);
|
||||
}
|
||||
|
||||
loadFileList(window.currentFolder);
|
||||
});
|
||||
|
||||
@@ -618,8 +637,25 @@ function submitFiles(allFiles) {
|
||||
} catch (e) {
|
||||
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];
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
||||
// real success
|
||||
if (li) {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerText = "Done";
|
||||
@@ -627,11 +663,14 @@ function submitFiles(allFiles) {
|
||||
}
|
||||
uploadResults[file.uploadIndex] = true;
|
||||
} else {
|
||||
// real failure
|
||||
if (li) {
|
||||
li.progressBar.innerText = "Error";
|
||||
}
|
||||
allSucceeded = false;
|
||||
}
|
||||
|
||||
// ─── Only now count this chunk as finished ───────────────────
|
||||
finishedCount++;
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList(allFiles, uploadResults, progressElements);
|
||||
@@ -665,6 +704,7 @@ function submitFiles(allFiles) {
|
||||
});
|
||||
|
||||
xhr.open("POST", "/api/upload/upload.php", true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||
xhr.send(formData);
|
||||
});
|
||||
|
||||
@@ -238,28 +238,28 @@ class AuthController
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + 30 * 24 * 60 * 60;
|
||||
$all = [];
|
||||
|
||||
|
||||
if (file_exists($tokFile)) {
|
||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||
$all = json_decode($dec, true) ?: [];
|
||||
}
|
||||
|
||||
|
||||
$all[$token] = [
|
||||
'username' => $username,
|
||||
'expiry' => $expiry,
|
||||
'isAdmin' => $_SESSION['isAdmin']
|
||||
];
|
||||
|
||||
|
||||
file_put_contents(
|
||||
$tokFile,
|
||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||
LOCK_EX
|
||||
);
|
||||
|
||||
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
|
||||
|
||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||
|
||||
|
||||
setcookie(
|
||||
session_name(),
|
||||
session_id(),
|
||||
@@ -269,7 +269,7 @@ class AuthController
|
||||
$secure,
|
||||
true
|
||||
);
|
||||
|
||||
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
|
||||
@@ -341,40 +341,86 @@ class AuthController
|
||||
|
||||
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;
|
||||
|
||||
// setup mode?
|
||||
// 2) Setup mode?
|
||||
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
||||
error_log("checkAuth: setup mode");
|
||||
echo json_encode(['setup' => true]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// 3) Session-based auth
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
echo json_encode(['authenticated' => false]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// TOTP enabled?
|
||||
// 4) TOTP enabled?
|
||||
$totp = false;
|
||||
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])) {
|
||||
if ($parts[0] === ($_SESSION['username'] ?? '') && !empty($parts[3])) {
|
||||
$totp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$isAdmin = ((int)AuthModel::getUserRole($_SESSION['username']) === 1);
|
||||
// 5) Final response
|
||||
$resp = [
|
||||
'authenticated' => true,
|
||||
'isAdmin' => $isAdmin,
|
||||
'totp_enabled' => $totp,
|
||||
'username' => $_SESSION['username'],
|
||||
'folderOnly' => $_SESSION['folderOnly'] ?? false,
|
||||
'readOnly' => $_SESSION['readOnly'] ?? false,
|
||||
'isAdmin' => !empty($_SESSION['isAdmin']),
|
||||
'totp_enabled' => $totp,
|
||||
'username' => $_SESSION['username'],
|
||||
'folderOnly' => $_SESSION['folderOnly'] ?? false,
|
||||
'readOnly' => $_SESSION['readOnly'] ?? false,
|
||||
'disableUpload' => $_SESSION['disableUpload'] ?? false
|
||||
];
|
||||
|
||||
echo json_encode($resp);
|
||||
exit();
|
||||
}
|
||||
@@ -403,10 +449,19 @@ class AuthController
|
||||
*/
|
||||
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('X-CSRF-Token: ' . $_SESSION['csrf_token']);
|
||||
|
||||
// 3) Return JSON payload
|
||||
echo json_encode([
|
||||
"csrf_token" => $_SESSION['csrf_token'],
|
||||
"share_url" => SHARE_URL
|
||||
'csrf_token' => $_SESSION['csrf_token'],
|
||||
'share_url' => SHARE_URL
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -72,34 +72,56 @@ class UploadController {
|
||||
*/
|
||||
public function handleUpload(): void {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// CSRF Protection.
|
||||
|
||||
//
|
||||
// 1) CSRF – pull from header or POST fields
|
||||
//
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = $headersArr['x-csrf-token'] ?? '';
|
||||
if (!isset($_SESSION['csrf_token']) || trim($receivedToken) !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
$received = '';
|
||||
if (!empty($headersArr['x-csrf-token'])) {
|
||||
$received = trim($headersArr['x-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;
|
||||
}
|
||||
// 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);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
// Check user permissions.
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$userPermissions = loadUserPermissions($username);
|
||||
if ($username && !empty($userPermissions['disableUpload'])) {
|
||||
$userPerms = loadUserPermissions($_SESSION['username']);
|
||||
if (!empty($userPerms['disableUpload'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Upload disabled for this user."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delegate to the model.
|
||||
|
||||
//
|
||||
// 3) Delegate the actual file handling
|
||||
//
|
||||
$result = UploadModel::handleUpload($_POST, $_FILES);
|
||||
|
||||
// For chunked uploads, output JSON (e.g., "chunk uploaded" status).
|
||||
|
||||
//
|
||||
// 4) Respond
|
||||
//
|
||||
if (isset($result['error'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode($result);
|
||||
@@ -109,8 +131,8 @@ class UploadController {
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Otherwise, for full upload success, set a flash message and redirect.
|
||||
|
||||
// full‐upload redirect
|
||||
$_SESSION['upload_message'] = "File uploaded successfully.";
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -87,63 +87,83 @@ class UserController
|
||||
|
||||
public function addUser()
|
||||
{
|
||||
// 1) Ensure JSON output and session
|
||||
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.
|
||||
// Setup mode means the "setup" query parameter is passed
|
||||
// and users.txt is missing, empty, or contains only whitespace.
|
||||
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
||||
if ($isSetup && (!file_exists($usersFile) || filesize($usersFile) == 0 || trim(file_get_contents($usersFile)) === '')) {
|
||||
// Allow initial admin creation without session or CSRF checks.
|
||||
// 2) Determine setup mode (first-ever admin creation)
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
||||
$setupMode = false;
|
||||
if (
|
||||
$isSetup && (! file_exists($usersFile)
|
||||
|| filesize($usersFile) === 0
|
||||
|| trim(file_get_contents($usersFile)) === ''
|
||||
)
|
||||
) {
|
||||
$setupMode = true;
|
||||
} else {
|
||||
$setupMode = false;
|
||||
// In non-setup mode, perform CSRF token and authentication checks.
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
// 3) In non-setup, enforce CSRF + auth checks
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = trim($headersArr['x-csrf-token'] ?? '');
|
||||
|
||||
// 3a) Soft-fail CSRF: on mismatch, regenerate and return new token
|
||||
if ($receivedToken !== $_SESSION['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;
|
||||
}
|
||||
|
||||
// 3b) Must be logged in as admin
|
||||
if (
|
||||
!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||
!isset($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true
|
||||
empty($_SESSION['authenticated'])
|
||||
|| $_SESSION['authenticated'] !== true
|
||||
|| empty($_SESSION['isAdmin'])
|
||||
|| $_SESSION['isAdmin'] !== true
|
||||
) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the JSON input data.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$newUsername = trim($data["username"] ?? "");
|
||||
$newPassword = trim($data["password"] ?? "");
|
||||
// 4) Parse input
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||
$newUsername = trim($data['username'] ?? '');
|
||||
$newPassword = trim($data['password'] ?? '');
|
||||
|
||||
// In setup mode, force the new user to be an admin.
|
||||
// 5) Determine admin flag
|
||||
if ($setupMode) {
|
||||
$isAdmin = "1";
|
||||
$isAdmin = '1';
|
||||
} else {
|
||||
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0";
|
||||
$isAdmin = !empty($data['isAdmin']) ? '1' : '0';
|
||||
}
|
||||
|
||||
// Validate that a username and password are provided.
|
||||
if (!$newUsername || !$newPassword) {
|
||||
// 6) Validate fields
|
||||
if ($newUsername === '' || $newPassword === '') {
|
||||
echo json_encode(["error" => "Username and password required"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate username format.
|
||||
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;
|
||||
}
|
||||
|
||||
// Delegate the business logic to the model.
|
||||
// 7) Delegate to model
|
||||
$result = userModel::addUser($newUsername, $newPassword, $isAdmin, $setupMode);
|
||||
|
||||
// 8) Return model result
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -847,147 +867,151 @@ class UserController
|
||||
* )
|
||||
*/
|
||||
|
||||
public function verifyTOTP()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
||||
|
||||
// Rate‑limit
|
||||
if (!isset($_SESSION['totp_failures'])) {
|
||||
$_SESSION['totp_failures'] = 0;
|
||||
}
|
||||
if ($_SESSION['totp_failures'] >= 5) {
|
||||
http_response_code(429);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Must be authenticated OR pending login
|
||||
if (!((!empty($_SESSION['authenticated'])) || isset($_SESSION['pending_login_user']))) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// CSRF check
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$csrfHeader = $headersArr['x-csrf-token'] ?? '';
|
||||
if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Parse and validate input
|
||||
$inputData = json_decode(file_get_contents("php://input"), true);
|
||||
$code = trim($inputData['totp_code'] ?? '');
|
||||
if (!preg_match('/^\d{6}$/', $code)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// TFA helper
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||||
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
|
||||
'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
|
||||
);
|
||||
|
||||
// Pending‑login flow (first password step passed)
|
||||
if (isset($_SESSION['pending_login_user'])) {
|
||||
$username = $_SESSION['pending_login_user'];
|
||||
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
||||
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
||||
|
||||
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
||||
$_SESSION['totp_failures']++;
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// === Issue “remember me” token if requested ===
|
||||
if ($rememberMe) {
|
||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + 30 * 24 * 60 * 60;
|
||||
$all = [];
|
||||
|
||||
if (file_exists($tokFile)) {
|
||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||
$all = json_decode($dec, true) ?: [];
|
||||
}
|
||||
$all[$token] = [
|
||||
'username' => $username,
|
||||
'expiry' => $expiry,
|
||||
'isAdmin' => $_SESSION['isAdmin']
|
||||
];
|
||||
file_put_contents(
|
||||
$tokFile,
|
||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||
LOCK_EX
|
||||
);
|
||||
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
|
||||
// Persistent cookie
|
||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||
|
||||
// Re‑issue PHP session cookie
|
||||
setcookie(
|
||||
session_name(),
|
||||
session_id(),
|
||||
$expiry,
|
||||
'/',
|
||||
'',
|
||||
$secure,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Finalize login
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['username'] = $username;
|
||||
$_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1");
|
||||
$_SESSION['folderOnly'] = loadUserPermissions($username);
|
||||
|
||||
// Clean up
|
||||
unset(
|
||||
$_SESSION['pending_login_user'],
|
||||
$_SESSION['pending_login_secret'],
|
||||
$_SESSION['pending_login_remember_me'],
|
||||
$_SESSION['totp_failures']
|
||||
);
|
||||
|
||||
echo json_encode(['status' => 'ok', 'message' => 'Login successful']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Setup/verification flow (not pending)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if (!$username) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$totpSecret = userModel::getTOTPSecret($username);
|
||||
if (!$totpSecret) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||
$_SESSION['totp_failures']++;
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Successful setup/verification
|
||||
unset($_SESSION['totp_failures']);
|
||||
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
||||
}
|
||||
public function verifyTOTP()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
||||
|
||||
// Rate‑limit
|
||||
if (!isset($_SESSION['totp_failures'])) {
|
||||
$_SESSION['totp_failures'] = 0;
|
||||
}
|
||||
if ($_SESSION['totp_failures'] >= 5) {
|
||||
http_response_code(429);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Must be authenticated OR pending login
|
||||
if (!((!empty($_SESSION['authenticated'])) || isset($_SESSION['pending_login_user']))) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// CSRF check
|
||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$csrfHeader = $headersArr['x-csrf-token'] ?? '';
|
||||
if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Parse and validate input
|
||||
$inputData = json_decode(file_get_contents("php://input"), true);
|
||||
$code = trim($inputData['totp_code'] ?? '');
|
||||
if (!preg_match('/^\d{6}$/', $code)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// TFA helper
|
||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||||
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
|
||||
'FileRise',
|
||||
6,
|
||||
30,
|
||||
\RobThree\Auth\Algorithm::Sha1
|
||||
);
|
||||
|
||||
// Pending‑login flow (first password step passed)
|
||||
if (isset($_SESSION['pending_login_user'])) {
|
||||
$username = $_SESSION['pending_login_user'];
|
||||
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
||||
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
||||
|
||||
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
||||
$_SESSION['totp_failures']++;
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// === Issue “remember me” token if requested ===
|
||||
if ($rememberMe) {
|
||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + 30 * 24 * 60 * 60;
|
||||
$all = [];
|
||||
|
||||
if (file_exists($tokFile)) {
|
||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||
$all = json_decode($dec, true) ?: [];
|
||||
}
|
||||
$isAdmin = ((int)userModel::getUserRole($username) === 1);
|
||||
$all[$token] = [
|
||||
'username' => $username,
|
||||
'expiry' => $expiry,
|
||||
'isAdmin' => $isAdmin
|
||||
];
|
||||
file_put_contents(
|
||||
$tokFile,
|
||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||
LOCK_EX
|
||||
);
|
||||
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
|
||||
// Persistent cookie
|
||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||
|
||||
// Re‑issue PHP session cookie
|
||||
setcookie(
|
||||
session_name(),
|
||||
session_id(),
|
||||
$expiry,
|
||||
'/',
|
||||
'',
|
||||
$secure,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Finalize login
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['username'] = $username;
|
||||
$_SESSION['isAdmin'] = $isAdmin;
|
||||
$_SESSION['folderOnly'] = loadUserPermissions($username);
|
||||
|
||||
// Clean up
|
||||
unset(
|
||||
$_SESSION['pending_login_user'],
|
||||
$_SESSION['pending_login_secret'],
|
||||
$_SESSION['pending_login_remember_me'],
|
||||
$_SESSION['totp_failures']
|
||||
);
|
||||
|
||||
echo json_encode(['status' => 'ok', 'message' => 'Login successful']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Setup/verification flow (not pending)
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
if (!$username) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$totpSecret = userModel::getTOTPSecret($username);
|
||||
if (!$totpSecret) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||||
$_SESSION['totp_failures']++;
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Successful setup/verification
|
||||
unset($_SESSION['totp_failures']);
|
||||
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
require_once PROJECT_ROOT . '/config/config.php';
|
||||
|
||||
class AuthModel {
|
||||
class AuthModel
|
||||
{
|
||||
|
||||
/**
|
||||
* Retrieves the user's role from the users file.
|
||||
@@ -11,7 +12,8 @@ class AuthModel {
|
||||
* @param string $username
|
||||
* @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;
|
||||
if (file_exists($usersFile)) {
|
||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
@@ -23,7 +25,7 @@ class AuthModel {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Authenticates the user using form-based credentials.
|
||||
*
|
||||
@@ -31,7 +33,8 @@ class AuthModel {
|
||||
* @param string $password
|
||||
* @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;
|
||||
if (!file_exists($usersFile)) {
|
||||
return false;
|
||||
@@ -51,14 +54,15 @@ class AuthModel {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads failed login attempts from a file.
|
||||
*
|
||||
* @param string $file
|
||||
* @return array
|
||||
*/
|
||||
public static function loadFailedAttempts(string $file): array {
|
||||
public static function loadFailedAttempts(string $file): array
|
||||
{
|
||||
if (file_exists($file)) {
|
||||
$data = json_decode(file_get_contents($file), true);
|
||||
if (is_array($data)) {
|
||||
@@ -67,7 +71,7 @@ class AuthModel {
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves failed login attempts into a file.
|
||||
*
|
||||
@@ -75,17 +79,19 @@ class AuthModel {
|
||||
* @param array $data
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a user's TOTP secret from the users file.
|
||||
*
|
||||
* @param string $username
|
||||
* @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;
|
||||
if (!file_exists($usersFile)) {
|
||||
return null;
|
||||
@@ -98,14 +104,15 @@ class AuthModel {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the folder-only permission for a given user.
|
||||
*
|
||||
* @param string $username
|
||||
* @return bool
|
||||
*/
|
||||
public static function loadFolderPermission(string $username): bool {
|
||||
public static function loadFolderPermission(string $username): bool
|
||||
{
|
||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||
if (file_exists($permissionsFile)) {
|
||||
$content = file_get_contents($permissionsFile);
|
||||
@@ -121,4 +128,31 @@ class AuthModel {
|
||||
}
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
194
start.sh
194
start.sh
@@ -1,162 +1,112 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
echo "🚀 Running start.sh..."
|
||||
|
||||
# Warn if default persistent tokens key is in use
|
||||
if [ "$PERSISTENT_TOKENS_KEY" = "default_please_change_this_key" ]; then
|
||||
echo "⚠️ WARNING: Using default persistent tokens key. Please override PERSISTENT_TOKENS_KEY for production."
|
||||
# 1) Token‐key warning
|
||||
if [ "${PERSISTENT_TOKENS_KEY}" = "default_please_change_this_key" ]; then
|
||||
echo "⚠️ WARNING: Using default persistent tokens key—override for production."
|
||||
fi
|
||||
|
||||
# Update config.php based on environment variables
|
||||
# 2) Update config.php based on environment variables
|
||||
CONFIG_FILE="/var/www/config/config.php"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
echo "🔄 Updating config.php based on environment variables..."
|
||||
if [ -n "$TIMEZONE" ]; then
|
||||
echo " Setting TIMEZONE to $TIMEZONE"
|
||||
sed -i "s|define('TIMEZONE',[[:space:]]*'[^']*');|define('TIMEZONE', '$TIMEZONE');|" "$CONFIG_FILE"
|
||||
fi
|
||||
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"
|
||||
if [ -f "${CONFIG_FILE}" ]; then
|
||||
echo "🔄 Updating config.php from env vars..."
|
||||
[ -n "${TIMEZONE:-}" ] && sed -i "s|define('TIMEZONE',[[:space:]]*'[^']*');|define('TIMEZONE', '${TIMEZONE}');|" "${CONFIG_FILE}"
|
||||
[ -n "${DATE_TIME_FORMAT:-}" ] && sed -i "s|define('DATE_TIME_FORMAT',[[:space:]]*'[^']*');|define('DATE_TIME_FORMAT', '${DATE_TIME_FORMAT}');|" "${CONFIG_FILE}"
|
||||
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||
sed -i "s|define('TOTAL_UPLOAD_SIZE',[[:space:]]*'[^']*');|define('TOTAL_UPLOAD_SIZE', '${TOTAL_UPLOAD_SIZE}');|" "${CONFIG_FILE}"
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# Update PHP upload limits at runtime if TOTAL_UPLOAD_SIZE is set.
|
||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
||||
echo "🔄 Updating PHP upload limits with TOTAL_UPLOAD_SIZE=$TOTAL_UPLOAD_SIZE"
|
||||
echo "upload_max_filesize = $TOTAL_UPLOAD_SIZE" > /etc/php/8.3/apache2/conf.d/99-custom.ini
|
||||
echo "post_max_size = $TOTAL_UPLOAD_SIZE" >> /etc/php/8.3/apache2/conf.d/99-custom.ini
|
||||
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||
echo "🔄 Setting PHP upload limits to ${TOTAL_UPLOAD_SIZE}"
|
||||
cat > /etc/php/8.3/apache2/conf.d/99-custom.ini <<EOF
|
||||
upload_max_filesize = ${TOTAL_UPLOAD_SIZE}
|
||||
post_max_size = ${TOTAL_UPLOAD_SIZE}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Update Apache LimitRequestBody based on TOTAL_UPLOAD_SIZE if set.
|
||||
if [ -n "$TOTAL_UPLOAD_SIZE" ]; then
|
||||
size_str=$(echo "$TOTAL_UPLOAD_SIZE" | tr '[:upper:]' '[:lower:]')
|
||||
factor=1
|
||||
# 4) Adjust Apache LimitRequestBody
|
||||
if [ -n "${TOTAL_UPLOAD_SIZE:-}" ]; then
|
||||
# convert to bytes
|
||||
size_str=$(echo "${TOTAL_UPLOAD_SIZE}" | tr '[:upper:]' '[:lower:]')
|
||||
case "${size_str: -1}" in
|
||||
g)
|
||||
factor=$((1024*1024*1024))
|
||||
size_num=${size_str%g}
|
||||
;;
|
||||
m)
|
||||
factor=$((1024*1024))
|
||||
size_num=${size_str%m}
|
||||
;;
|
||||
k)
|
||||
factor=1024
|
||||
size_num=${size_str%k}
|
||||
;;
|
||||
*)
|
||||
size_num=$size_str
|
||||
;;
|
||||
g) factor=$((1024*1024*1024)); num=${size_str%g} ;;
|
||||
m) factor=$((1024*1024)); num=${size_str%m} ;;
|
||||
k) factor=1024; num=${size_str%k} ;;
|
||||
*) factor=1; num=${size_str} ;;
|
||||
esac
|
||||
LIMIT_REQUEST_BODY=$((size_num * factor))
|
||||
echo "🔄 Setting Apache LimitRequestBody to $LIMIT_REQUEST_BODY bytes (from TOTAL_UPLOAD_SIZE=$TOTAL_UPLOAD_SIZE)"
|
||||
cat <<EOF > /etc/apache2/conf-enabled/limit_request_body.conf
|
||||
LIMIT_REQUEST_BODY=$(( num * factor ))
|
||||
echo "🔄 Setting Apache LimitRequestBody to ${LIMIT_REQUEST_BODY} bytes"
|
||||
cat > /etc/apache2/conf-enabled/limit_request_body.conf <<EOF
|
||||
<Directory "/var/www/public">
|
||||
LimitRequestBody $LIMIT_REQUEST_BODY
|
||||
LimitRequestBody ${LIMIT_REQUEST_BODY}
|
||||
</Directory>
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Set Apache Timeout (default is 300 seconds)
|
||||
echo "🔄 Setting Apache Timeout to 600 seconds"
|
||||
cat <<EOF > /etc/apache2/conf-enabled/timeout.conf
|
||||
# 5) Configure Apache timeout (600s)
|
||||
cat > /etc/apache2/conf-enabled/timeout.conf <<EOF
|
||||
Timeout 600
|
||||
EOF
|
||||
|
||||
echo "🔥 Final Apache Timeout configuration:"
|
||||
cat /etc/apache2/conf-enabled/timeout.conf
|
||||
|
||||
# Update Apache ports if environment variables are provided
|
||||
if [ -n "$HTTP_PORT" ]; then
|
||||
echo "🔄 Setting Apache HTTP port to $HTTP_PORT"
|
||||
sed -i "s/^Listen 80$/Listen $HTTP_PORT/" /etc/apache2/ports.conf
|
||||
sed -i "s/<VirtualHost \*:80>/<VirtualHost *:$HTTP_PORT>/" /etc/apache2/sites-available/000-default.conf
|
||||
# 6) Override ports if provided
|
||||
if [ -n "${HTTP_PORT:-}" ]; then
|
||||
sed -i "s/^Listen 80$/Listen ${HTTP_PORT}/" /etc/apache2/ports.conf
|
||||
sed -i "s/<VirtualHost \*:80>/<VirtualHost *:${HTTP_PORT}>/" /etc/apache2/sites-available/000-default.conf
|
||||
fi
|
||||
if [ -n "${HTTPS_PORT:-}" ]; then
|
||||
sed -i "s/^Listen 443$/Listen ${HTTPS_PORT}/" /etc/apache2/ports.conf
|
||||
fi
|
||||
|
||||
if [ -n "$HTTPS_PORT" ]; then
|
||||
echo "🔄 Setting Apache HTTPS port to $HTTPS_PORT"
|
||||
sed -i "s/^Listen 443$/Listen $HTTPS_PORT/" /etc/apache2/ports.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
|
||||
# 7) Set ServerName
|
||||
if [ -n "${SERVER_NAME:-}" ]; then
|
||||
echo "ServerName ${SERVER_NAME}" >> /etc/apache2/apache2.conf
|
||||
else
|
||||
echo "🔄 Setting Apache ServerName to default: FileRise"
|
||||
echo "ServerName FileRise" >> /etc/apache2/apache2.conf
|
||||
fi
|
||||
|
||||
echo "Final /etc/apache2/ports.conf content:"
|
||||
cat /etc/apache2/ports.conf
|
||||
# 8) Prepare dynamic data directories with least privilege
|
||||
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."
|
||||
|
||||
# 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)
|
||||
# 9) Initialize persistent files if absent
|
||||
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
|
||||
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
|
||||
else
|
||||
echo "ℹ️ users.txt already exists; preserving persistent data."
|
||||
fi
|
||||
|
||||
# Create createdTags.json only if it doesn't already exist (preserving persistent data)
|
||||
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
|
||||
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
|
||||
else
|
||||
echo "ℹ️ createdTags.json already exists; preserving persistent data."
|
||||
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 (99-custom.ini):"
|
||||
cat /etc/php/8.3/apache2/conf.d/99-custom.ini
|
||||
|
||||
echo "🔥 Final Apache configuration (limit_request_body.conf):"
|
||||
cat /etc/apache2/conf-enabled/limit_request_body.conf
|
||||
|
||||
echo "🔥 Starting Apache..."
|
||||
exec apachectl -D FOREGROUND
|
||||
exec apachectl -D FOREGROUND
|
||||
Reference in New Issue
Block a user