From d664a2f5d8d46cbf11239e528b98091ecb8e471b Mon Sep 17 00:00:00 2001
From: Ryan
Date: Fri, 31 Oct 2025 17:34:25 -0400
Subject: [PATCH] release(v1.7.3): lightweight boot pipeline, dramatically
faster first paint, deduped /api writes, sturdier uploads/auth
---
CHANGELOG.md | 84 ++
config/config.php | 16 -
public/.htaccess | 71 +-
public/css/styles.css | 11 +-
public/css/vendor/material-icons.css | 2 +-
public/css/vendor/roboto.css | 8 +-
public/index.html | 142 ++--
public/js/adminPanel.js | 518 ++++++++-----
public/js/appCore.js | 23 +-
public/js/auth.js | 364 ++++++---
public/js/defer-css.js | 20 +
public/js/fileActions.js | 5 +
public/js/fileEditor.js | 148 ++--
public/js/fileListView.js | 89 ++-
public/js/i18n.js | 2 +-
public/js/main.js | 1071 +++++++++++++++++++++-----
public/js/upload.js | 136 +++-
src/controllers/AdminController.php | 95 +--
src/controllers/AuthController.php | 5 +-
src/controllers/UploadController.php | 108 +--
src/models/UploadModel.php | 321 ++++----
21 files changed, 2272 insertions(+), 967 deletions(-)
create mode 100644 public/js/defer-css.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d8101e0..fa3d0c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,89 @@
# Changelog
+## Changes 10/31/2025 (v1.7.3)
+
+release(v1.7.3): lightweight boot pipeline, dramatically faster first paint, deduped /api writes, sturdier uploads/auth
+
+### 🎃 Highlights (advantages) 👻 🦇
+
+- ⚡ Faster, cleaner boot: a lightweight **main.js** decides auth/setup before painting, avoids flicker, and wires modules exactly once.
+- ♻️ Fewer duplicate actions: **request coalescer** dedupes POST/PUT/PATCH/DELETE to /api/* .
+- ✅ Truthy UX: global **toast bridge** queues early toasts and normalizes misleading “not found/already exists” messages after success.
+- 🔐 Smoother auth: CSRF priming/rotation + **TOTP step-up detection** across JSON & redirect paths; “Welcome back, `user`” toast once per tab.
+- 🌓 Polished UI: **dark-mode persistence with system fallback**, live siteConfig title application, higher-z modals, drag auto-scroll.
+- 🚀 Faster first paint & interactions: defer CodeMirror/Fuse/Resumable, promote preloaded CSS, and coalesce duplicate requests → snappier UI.
+- 🧭 Admin polish: live header title preview, masked OIDC fields with **Replace** flow, and a **read-only Sponsors/Donations** section.
+- 🧱 Safer & cache-smarter: opinionated .htaccess (CSP/HSTS/MIME/compression) + `?v={{APP_QVER}}` for versioned immutable assets.
+
+### Core bootstrap (main.js) overhaul
+
+- Early **toast bridge** (queues until domUtils is ready); expose `window.__FR_TOAST_FILTER__` for centralized rewrites/suppression.
+- **Result guard + request coalescer** wrapping `fetch`:
+ - Dedupes same-origin `/api/*` mutating requests for ~800ms using a stable key (method + path + normalized body).
+ - Tracks “last OK” JSON (`success|status|result=ok`) to suppress false-negative error toasts after success.
+- **Boot orchestrator** with hard guards:
+ - `__FR_FLAGS` (`booted`, `initialized`, `wired.*`, `bootPromise`, `entryStarted`) to prevent double init/leaks.
+ - **No-flicker login**: resolve `checkAuth()` + `setup` before showing UI; show login only when truly unauthenticated.
+ - **Heavy boot** for authed users: load i18n, `appCore.loadCsrfToken/initializeApp`, first file list, then light UI wiring.
+- **Auth flow**:
+ - `primeCsrf()` + `` management; persist token in localStorage.
+ - **TOTP** detection via header (`X-TOTP-Required`) & JSON (`totp_required` / `TOTP_REQUIRED`); calls `openTOTPLoginModal()`.
+ - **Welcome toast** once per tab via `sessionStorage.__fr_welcomed`.
+- **UI/UX niceties**:
+ - `applySiteConfig()` updates header title & login method visibility on both login & authed screens.
+ - Dark-mode persistence with system fallback, proper a11y labels/icons.
+ - Create dropdown/menu wiring with capture-phase outside-click + ESC close; modal cancel safeties.
+ - Lift modals above cards (z-index), **drag auto-scroll** near viewport edges.
+ - Dispatch legacy `DOMContentLoaded`/`load` **once** (supports older inline handlers).
+ - Username label refresh for existing `.user-name-label` without injecting new DOM.
+
+### Performance & UX changes
+
+- CSS/first paint:
+ - Preload Bootstrap & app CSS; promote at DOMContentLoaded; keep inline CSS minimal.
+ - Add `width/height/decoding/fetchpriority` to logo to reduce layout shift.
+- Search/editor/uploads:
+ - **fileListView.js**: lazy-load Fuse with instant substring fallback; `warmUpSearch()` hook.
+ - **fileEditor.js**: lazy-load CodeMirror core/theme/modes; start plain then upgrade; guard very large files gracefully.
+ - **upload.js**: lazy-load Resumable; resilient init; background warm-up; smarter addFile/submit; clearer toasts.
+- Toast/UX:
+ - Install early toast bridge; queue & normalize messages; neutral “Done.” when server returns misleading errors after success.
+
+### Correctness: uploads, paths, ACLs
+
+- **UploadController/UploadModel**: normalize folders via `ACL::normalizeFolder(rawurldecode())`; stricter segment checks; consistent base paths; safer metadata writes; proper chunk presence/merge & temp cleanup.
+
+### Auth hardening & resilience
+
+- **auth.js/main.js/appCore.js**: CSRF rotate/retry (JSON then x-www-form-urlencoded fallback); robust login handling; fewer misleading error toasts.
+- **AuthController**: OIDC username fallback to `email` or `sub` when `preferred_username` missing.
+
+### Admin panel
+
+- **adminPanel.js**:
+ - Live header title preview (instant update without reload).
+ - Masked OIDC client fields with **Replace** button; saved-value hints; only send secrets when replacing.
+ - **New “Sponsor / Donations” section (read-only)**:
+ - GitHub Sponsors → `https://github.com/sponsors/error311`
+ - Ko-fi → `https://ko-fi.com/error311`
+ - Includes **Copy** and **Open** buttons; values are fixed.
+- **AdminController**: boolean for `oidc.hasClientId/hasClientSecret` to drive masked inputs.
+
+### Security & caching (.htaccess)
+
+- Consolidated security headers (CSP, CORP, HSTS on HTTPS), MIME types, compression (Brotli/Deflate), TRACE disable.
+- Caching rules:
+ - HTML/version.js: no-cache; unversioned JS/CSS: 1h; unversioned static: 7d; **versioned assets `?v=`: 1y `immutable`**.
+- **config.php**: remove duplicate runtime headers (now via Apache) to avoid proxy/CDN conflicts.
+
+### Upgrade notes
+
+- No schema changes.
+- Ensure Apache modules (`headers`, `rewrite`, `brotli`/`deflate`) are available for the new .htaccess rules (fallbacks included).
+- Versioned assets mean users shouldn’t need a hard refresh; `?v={{APP_QVER}}` busts caches automatically.
+
+---
+
## Changes 10/29/2025 (v1.7.0 & v1.7.1 & v1.7.2)
release(v1.7.0): asset cache-busting pipeline, public siteConfig cache, JS core split, and caching/security polish
diff --git a/config/config.php b/config/config.php
index e0fa1b6..5f489bf 100644
--- a/config/config.php
+++ b/config/config.php
@@ -1,22 +1,6 @@
RewriteEngine On
-# If you want forced HTTPS behind a proxy, keep this off here and do it at the proxy
+
+# --- HTTPS redirect ---
+# Use ONE of these blocks.
+
+# A) Direct TLS on this server (enable this if Apache terminates HTTPS here)
#RewriteCond %{HTTPS} off
#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
-# MIME types (fonts/SVG/ESM)
+# B) Behind a reverse proxy/CDN that sets X-Forwarded-Proto
+#RewriteCond %{HTTP:X-Forwarded-Proto} =http [OR]
+#RewriteCond %{HTTP:X-Forwarded-Proto} ^$
+#RewriteCond %{HTTPS} !=on
+#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
+
+# Don't interfere with ACME/http-01 if you do your own certs
+#RewriteCond %{REQUEST_URI} ^/.well-known/acme-challenge/
+#RewriteRule - - [L]
+
+# --- MIME types (fonts/SVG/ESM) ---
AddType font/woff2 .woff2
AddType font/woff .woff
@@ -23,58 +37,57 @@ RewriteEngine On
AddType application/javascript .mjs
-# Security headers
+# --- Security headers ---
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set X-Content-Type-Options "nosniff"
- # HSTS: only if HTTPS (prevents mixed local dev warnings)
- Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" "expr=%{HTTPS} == 'on'"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set X-Download-Options "noopen"
Header always set Expect-CT "max-age=86400, enforce"
- # Nice extra hardening (same-origin resource sharing)
Header always set Cross-Origin-Resource-Policy "same-origin"
Header always set X-Permitted-Cross-Domain-Policies "none"
+ # HSTS only when actually on HTTPS
+ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" "expr=%{HTTPS} == 'on'"
- # CSP (modules, workers, blobs already accounted for)
- Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; worker-src 'self' blob:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self'"
+ # CSP (modules, blobs, workers, etc.)
+ Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; worker-src 'self' blob:; form-action 'self'"
-# Caching
-SetEnvIfNoCase QUERY_STRING "(^|&)v=" has_version_param=1
+# --- Caching (query-string based, no env vars needed) ---
- # HTML/PHP: no cache (app shell)
+ # HTML/PHP: no cache (only if PHP didn’t already set it)
- Header set Cache-Control "no-cache, no-store, must-revalidate"
- Header set Pragma "no-cache"
- Header set Expires "0"
+ Header setifempty Cache-Control "no-cache, no-store, must-revalidate"
+ Header setifempty Pragma "no-cache"
+ Header setifempty Expires "0"
- # version.js is your source-of-truth; keep it non-cacheable so dev/CI flips show up
+ # version.js: always non-cacheable
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
-# Unversioned JS/CSS (dev): 1 hour
-
- Header set Cache-Control "public, max-age=3600, must-revalidate" env=!has_version_param
-
+ # Unversioned JS/CSS: 1 hour
+
+ Header set Cache-Control "public, max-age=3600, must-revalidate" "expr=%{QUERY_STRING} !~ /(^|&)v=/"
+
-# Unversioned static assets (dev): 7 days
-
- Header set Cache-Control "public, max-age=604800" env=!has_version_param
-
+ # Unversioned static (images/fonts): 7 days
+
+ Header set Cache-Control "public, max-age=604800" "expr=%{QUERY_STRING} !~ /(^|&)v=/"
+
-# Versioned assets (?v=...): 1 year + immutable
-
- Header set Cache-Control "public, max-age=31536000, immutable" env=has_version_param
-
+ # Versioned assets (?v=...): 1 year + immutable
+
+ Header setifempty Cache-Control "public, max-age=31536000, immutable" "expr=%{QUERY_STRING} =~ /(^|&)v=/"
+
+
-# Compression (if modules exist)
+# --- Compression ---
BrotliCompressionQuality 5
AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json image/svg+xml
@@ -83,6 +96,6 @@ SetEnvIfNoCase QUERY_STRING "(^|&)v=" has_version_param=1
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml
-# Disable TRACE
+# --- Disable TRACE ---
RewriteCond %{REQUEST_METHOD} ^TRACE
RewriteRule .* - [F]
\ No newline at end of file
diff --git a/public/css/styles.css b/public/css/styles.css
index d501273..5ff1043 100644
--- a/public/css/styles.css
+++ b/public/css/styles.css
@@ -37,7 +37,11 @@ body {
/************************************************************/
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
/************************************************************/
- .header-logo .logo { height: 50px; width: auto; display: block; }
+ .header-logo .logo {
+ display:block;
+ max-width:100%;
+ height:auto; /* keep aspect ratio; HTML attrs set the intrinsic box */
+ }
.btn-login {
margin-top: 10px;
}/* Color overrides */
@@ -1598,7 +1602,7 @@ body {
#removeUserModal {
z-index: 5000 !important;
}#customConfirmModal {
- z-index: 6000 !important;
+ z-index: 12000 !important;
}.admin-panel-content {
background: #fff;
color: #000;
@@ -1867,4 +1871,5 @@ body {
#sidebarToggleFloating.is-collapsed {
background: #fafafa;
border-color: #e2e2e2;
- }
\ No newline at end of file
+ }
+
\ No newline at end of file
diff --git a/public/css/vendor/material-icons.css b/public/css/vendor/material-icons.css
index 3ba292f..1c6b8c2 100644
--- a/public/css/vendor/material-icons.css
+++ b/public/css/vendor/material-icons.css
@@ -4,7 +4,7 @@
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url(/fonts/material-icons/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2');
+ src: url('/fonts/material-icons/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2?v={{APP_QVER}}') format('woff2');
}
.material-icons {
diff --git a/public/css/vendor/roboto.css b/public/css/vendor/roboto.css
index ca9215f..5b716c6 100644
--- a/public/css/vendor/roboto.css
+++ b/public/css/vendor/roboto.css
@@ -4,7 +4,7 @@
font-style:normal;
font-weight:400;
font-display:swap;
- src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2') format('woff2');
+ src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2?v={{APP_QVER}}') format('woff2');
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
/* Roboto Regular 400 — latin */
@@ -13,7 +13,7 @@
font-style:normal;
font-weight:400;
font-display:swap;
- src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2') format('woff2');
+ src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2?v={{APP_QVER}}') format('woff2');
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
/* Roboto Medium 500 — latin-ext */
@@ -22,7 +22,7 @@
font-style:normal;
font-weight:500;
font-display:swap;
- src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2') format('woff2');
+ src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2?v={{APP_QVER}}') format('woff2');
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
/* Roboto Medium 500 — latin */
@@ -31,7 +31,7 @@
font-style:normal;
font-weight:500;
font-display:swap;
- src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2') format('woff2');
+ src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2?v={{APP_QVER}}') format('woff2');
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
diff --git a/public/index.html b/public/index.html
index 615a81a..32a550b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5,38 +5,58 @@
FileRise
+
+
+
+
+
+
-
-
-
+
+
-
-
+
+
+
-
-
-
+
+
+
+
-
-
+
+
+
-
-
-
-
+
+
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
-
@@ -44,7 +64,14 @@
@@ -103,7 +130,7 @@
-
+
+
+
@@ -215,7 +244,7 @@
@@ -233,7 +262,7 @@
+ data-i18n-key="rename" data-default>Rename
@@ -253,7 +282,7 @@
+ data-i18n-key="delete" data-default>Delete
@@ -289,7 +318,7 @@
selected files?
@@ -303,7 +332,7 @@
@@ -317,7 +346,7 @@
@@ -325,35 +354,20 @@
data-i18n-key="download_zip">Download ZIP
-
-
-
-
+
+
+
+
@@ -362,7 +376,7 @@
data-i18n-placeholder="newfile_placeholder" />
@@ -374,7 +388,7 @@
placeholder="files.zip" />
@@ -412,14 +426,14 @@
placeholder="Filename" />
-
+
-
@@ -453,7 +467,7 @@
Cancel
-
@@ -478,7 +492,7 @@
placeholder="Enter new file name" style="margin-top:10px;" />
Cancel
- Rename
+ Rename
@@ -486,7 +500,7 @@
diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js
index 998461b..156a84f 100644
--- a/public/js/adminPanel.js
+++ b/public/js/adminPanel.js
@@ -10,16 +10,16 @@ const adminTitle = `${t("admin_panel")} {
+ const title = (val || '').trim() || 'FileRise';
+ const h1 = document.querySelector('.header-title h1');
+ if (h1) h1.textContent = title;
+ document.title = title;
+ window.headerTitle = val || ''; // preserve raw value user typed
+ try { localStorage.setItem('headerTitle', title); } catch { }
+ };
+
+ // apply current value immediately + on each keystroke
+ apply(input.value);
+ input.addEventListener('input', (e) => apply(e.target.value));
+}
+
+function renderMaskedInput({ id, label, hasValue, isSecret = false }) {
+ const type = isSecret ? 'password' : 'text';
+ const disabled = hasValue ? 'disabled data-replace="0" placeholder="•••••• (saved)"' : '';
+ const replaceBtn = hasValue
+ ? `Replace`
+ : '';
+ const note = hasValue
+ ? `Saved — leave blank to keep`
+ : '';
+
+ return `
+
+ `;
+}
+
+function wireReplaceButtons(scope = document) {
+ scope.querySelectorAll('[data-replace-for]').forEach(btn => {
+ if (btn.__wired) return;
+ btn.__wired = true;
+ btn.addEventListener('click', () => {
+ const id = btn.getAttribute('data-replace-for');
+ const inp = scope.querySelector('#' + id);
+ if (!inp) return;
+ inp.disabled = false;
+ inp.dataset.replace = '1';
+ inp.placeholder = '';
+ inp.value = '';
+ btn.textContent = 'Keep saved value';
+ btn.removeAttribute('data-replace-for');
+ btn.addEventListener('click', () => { /* no-op after first toggle */ }, { once: true });
+ }, { once: true });
+ });
+}
+
function onShareFolderToggle(row, checked) {
const manage = qs(row, 'input[data-cap="manage"]');
const viewAll = qs(row, 'input[data-cap="view"]');
@@ -52,14 +112,14 @@ function onShareFileToggle(row, checked) {
const viewAll = qs(row, 'input[data-cap="view"]');
const viewOwn = qs(row, 'input[data-cap="viewOwn"]');
const hasView = !!(viewAll && viewAll.checked);
- const hasOwn = !!(viewOwn && viewOwn.checked);
+ const hasOwn = !!(viewOwn && viewOwn.checked);
if (!hasView && !hasOwn && viewOwn) {
viewOwn.checked = true;
}
}
function onWriteToggle(row, checked) {
- const caps = ["create","upload","edit","rename","copy","delete","extract"];
+ const caps = ["create", "upload", "edit", "rename", "copy", "delete", "extract"];
caps.forEach(c => {
const box = qs(row, `input[data-cap="${c}"]`);
if (box) box.checked = checked;
@@ -426,20 +486,21 @@ export function openAdminPanel() {
×
${adminTitle}