release(v1.7.0): asset cache-busting pipeline, public siteConfig cache, JS core split, and caching/security polish
This commit is contained in:
44
.github/workflows/release-on-version.yml
vendored
44
.github/workflows/release-on-version.yml
vendored
@@ -55,17 +55,45 @@ jobs:
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build zip artifact
|
||||
- name: Build zip artifact (stamped)
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ZIP="FileRise-${{ steps.ver.outputs.version }}.zip"
|
||||
zip -r "$ZIP" . \
|
||||
-x "./.git/*" "./.github/*" \
|
||||
"./resources/*" "./resources/**" \
|
||||
"./.dockerignore" "./.gitattributes" "./.gitignore" \
|
||||
"$ZIP" "${ZIP}.sha256" >/dev/null
|
||||
VER="${{ steps.ver.outputs.version }}" # e.g. v1.6.12
|
||||
ZIP="FileRise-${VER}.zip"
|
||||
|
||||
# Clean staging copy (exclude dotfiles you don’t want)
|
||||
rm -rf staging
|
||||
rsync -a \
|
||||
--exclude '.git' --exclude '.github' \
|
||||
--exclude 'resources' \
|
||||
--exclude '.dockerignore' --exclude '.gitattributes' --exclude '.gitignore' \
|
||||
./ staging/
|
||||
|
||||
# Stamp IN THE STAGING COPY
|
||||
./scripts/stamp-assets.sh "${VER}" "$(pwd)/staging"
|
||||
|
||||
- name: Verify placeholders are gone (staging)
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ROOT="$(pwd)/staging"
|
||||
if grep -R -n "{{APP_QVER}}\|{{APP_VER}}" "$ROOT" --include='*.html' --include='*.php' --include='*.css' --include='*.js' 2>/dev/null; then
|
||||
echo "ERROR: Found unreplaced placeholders above in staging."
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: No unreplaced placeholders in staging."
|
||||
|
||||
- name: Zip stamped staging
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VER="${{ steps.ver.outputs.version }}"
|
||||
ZIP="FileRise-${VER}.zip"
|
||||
(cd staging && zip -r "../$ZIP" . >/dev/null)
|
||||
|
||||
- name: Compute SHA-256 checksum
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
@@ -127,12 +155,12 @@ jobs:
|
||||
SHA="${{ steps.sum.outputs.sha }}"
|
||||
|
||||
{
|
||||
echo "## ${VER}"
|
||||
echo
|
||||
if [[ -s CHANGELOG_SNIPPET.md ]]; then
|
||||
cat CHANGELOG_SNIPPET.md
|
||||
echo
|
||||
fi
|
||||
echo "## ${VER}"
|
||||
echo "### Full Changelog"
|
||||
echo "[${PREV} → ${VER}](${COMPARE_URL})"
|
||||
echo
|
||||
|
||||
35
.github/workflows/sync-changelog.yml
vendored
35
.github/workflows/sync-changelog.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "No release(vX.Y.Z) tag in commit message; skipping bump."
|
||||
fi
|
||||
|
||||
- name: Update public/js/version.js
|
||||
- name: Update public/js/version.js (source of truth)
|
||||
if: steps.ver.outputs.version != ''
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -42,43 +42,20 @@ jobs:
|
||||
window.APP_VERSION = '${{ steps.ver.outputs.version }}';
|
||||
EOF
|
||||
|
||||
- name: Stamp asset cache-busters (?v=...) in HTML/CSS and {{APP_VER}} everywhere
|
||||
if: steps.ver.outputs.version != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VER="${{ steps.ver.outputs.version }}" # e.g. v1.6.9
|
||||
QVER="${VER#v}" # e.g. 1.6.9
|
||||
echo "Stamping ?v=${QVER} and {{APP_VER}}=${VER}"
|
||||
# ✂️ REMOVED: repo stamping of HTML/CSS/JS
|
||||
|
||||
# 1) Only stamp ?v= in HTML/CSS (avoid JS concatenation issues)
|
||||
mapfile -t html_css < <(git ls-files -- 'public/*.html' 'public/**/*.html' 'public/*.php' 'public/**/*.css')
|
||||
for f in "${html_css[@]}"; do
|
||||
sed -E -i "s/(\?v=)[^\"'&<>\s]*/\1${QVER}/g" "$f"
|
||||
sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f"
|
||||
done
|
||||
|
||||
# 2) For JS, only replace the {{APP_VER}} placeholder (do NOT touch ?v=)
|
||||
mapfile -t jsfiles < <(git ls-files -- 'public/*.js' 'public/**/*.js')
|
||||
for f in "${jsfiles[@]}"; do
|
||||
sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f"
|
||||
done
|
||||
|
||||
echo "Changed files:"
|
||||
git status --porcelain | awk '{print $2}' | sed 's/^/ - /'
|
||||
|
||||
- name: Commit version bump + stamped assets
|
||||
- name: Commit version.js only
|
||||
if: steps.ver.outputs.version != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add public/js/version.js public
|
||||
git add public/js/version.js
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore(release): set APP_VERSION and stamp assets to ${{ steps.ver.outputs.version }} [skip ci]"
|
||||
git commit -m "chore(release): set APP_VERSION to ${{ steps.ver.outputs.version }} [skip ci]"
|
||||
git push
|
||||
fi
|
||||
|
||||
@@ -110,6 +87,6 @@ jobs:
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore: sync CHANGELOG.md and VERSION (${{ steps.ver.outputs.version }}) from FileRise"
|
||||
git commit -m "chore: sync CHANGELOG.md + VERSION (${{ steps.ver.outputs.version }}) from FileRise"
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -1,5 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 10/29/2025 (v1.7.0)
|
||||
|
||||
release(v1.7.0): asset cache-busting pipeline, public siteConfig cache, JS core split, and caching/security polish
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- Public, non-sensitive site config cache:
|
||||
- Add `AdminModel::buildPublicSubset()` and `writeSiteConfig()` to write `USERS_DIR/siteConfig.json`.
|
||||
- New endpoint `public/api/siteConfig.php` + `UserController::siteConfig()` to serve the public subset (regenerates if stale).
|
||||
- Frontend now reads `/api/siteConfig.php` (safe subset) instead of `/api/admin/getConfig.php`.
|
||||
- Frontend module versioning:
|
||||
- Replace all module imports with `?v={{APP_QVER}}` query param so the release/Docker stamper can pin exact versions.
|
||||
- Add `scripts/stamp-assets.sh` to stamp `?v=` and `{{APP_VER}}/{{APP_QVER}}` in **staging** for ZIP/Docker builds.
|
||||
|
||||
### 🧩 Refactors
|
||||
|
||||
- Extract shared boot/bootstrap logic into `public/js/appCore.js`:
|
||||
- CSRF helpers (`setCsrfToken`, `getCsrfToken`, `loadCsrfToken`)
|
||||
- `initializeApp()`, `triggerLogout()`
|
||||
- Keep `main.js` lean; wrap global `fetch` once to append/rotate CSRF.
|
||||
- Update imports across JS modules to use versioned module URLs.
|
||||
|
||||
### 🚀 Performance
|
||||
|
||||
- Aggressive, safe caching for versioned assets:
|
||||
- `.htaccess`: `?v=…` ⇒ `Cache-Control: max-age=31536000, immutable`.
|
||||
- Unversioned JS/CSS short cache (1h), other static (7d).
|
||||
- Eliminate duplicate `main.js` loads and tighten CodeMirror mode loading.
|
||||
|
||||
### 🔒 Security / Hardening
|
||||
|
||||
- `.htaccess`:
|
||||
- Conditional HSTS only when HTTPS, add CORP and X-Permitted-Cross-Domain-Policies.
|
||||
- CSP kept strict for modules, workers, blobs.
|
||||
- Admin config exposure reduced to a curated subset in `siteConfig.json`.
|
||||
|
||||
### 🧪 CI/CD / Release
|
||||
|
||||
- **FileRise repo**
|
||||
- `sync-changelog.yml`: keep `public/js/version.js` as source-of-truth only (no repo-wide stamping).
|
||||
- `release-on-version.yml`: build **stamped** ZIP from a staging copy via `scripts/stamp-assets.sh`, verify placeholders removed, attach checksum.
|
||||
- **filerise-docker repo**
|
||||
- Read `VERSION`, checkout app to `app/`, run stamper inside build context before `docker buildx`, tag `latest` and `:${VERSION}`.
|
||||
|
||||
### 🔧 Defaults
|
||||
|
||||
- Sample/admin config defaults now set `disableBasicAuth: true` (safer default). Existing installations keep their current setting.
|
||||
|
||||
### 📂 Notable file changes
|
||||
|
||||
- `src/models/AdminModel.php` (+public subset +atomic write)
|
||||
- `src/controllers/UserController.php` (+siteConfig action)
|
||||
- `public/api/siteConfig.php` (new)
|
||||
- `public/js/appCore.js` (new), `public/js/main.js` (slim, uses appCore)
|
||||
- Many `public/js/*.js` import paths updated to `?v={{APP_QVER}}`
|
||||
- `public/.htaccess` (caching & headers)
|
||||
- `scripts/stamp-assets.sh` (new)
|
||||
|
||||
### ⚠️ Upgrade notes
|
||||
|
||||
- Ensure `USERS_DIR` is writable by web server for `siteConfig.json`.
|
||||
- Proxies/edge caches: the new `?v=` scheme enables long-lived immutable caching; purge is automatic on version bump.
|
||||
- If you previously read admin config directly on the client, it now reads `/api/siteConfig.php`.
|
||||
|
||||
---
|
||||
|
||||
## Changes 10/28/2025 (v1.6.11)
|
||||
|
||||
release(v1.6.11) fix(ui/dragAndDrop) restore floating zones toggle click action
|
||||
|
||||
@@ -11,14 +11,16 @@ DirectoryIndex index.html
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
# If you want forced HTTPS behind a proxy, keep this off here and do it at the proxy
|
||||
#RewriteCond %{HTTPS} off
|
||||
#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# MIME types for fonts/SVG
|
||||
# MIME types (fonts/SVG/ESM)
|
||||
<IfModule mod_mime.c>
|
||||
AddType font/woff2 .woff2
|
||||
AddType font/woff .woff
|
||||
AddType image/svg+xml .svg
|
||||
AddType application/javascript .mjs
|
||||
</IfModule>
|
||||
|
||||
# Security headers
|
||||
@@ -26,43 +28,53 @@ RewriteEngine On
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
# 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"
|
||||
|
||||
# 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'"
|
||||
</IfModule>
|
||||
|
||||
# Caching
|
||||
SetEnvIfNoCase QUERY_STRING "(^|&)v=" has_version_param=1
|
||||
<IfModule mod_headers.c>
|
||||
# HTML/PHP: no cache (app shell)
|
||||
<FilesMatch "\.(html?|php)$">
|
||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
|
||||
# version.js is your source-of-truth; keep it non-cacheable so dev/CI flips show up
|
||||
<FilesMatch "^js/version\.js$">
|
||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.(js|css)$">
|
||||
Header set Cache-Control "public, max-age=3600, must-revalidate" env=!has_version_param
|
||||
</FilesMatch>
|
||||
# Unversioned JS/CSS (dev): 1 hour
|
||||
<FilesMatch "\.(?:m?js|css)$">
|
||||
Header set Cache-Control "public, max-age=3600, must-revalidate" env=!has_version_param
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.(png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$">
|
||||
Header set Cache-Control "public, max-age=604800" env=!has_version_param
|
||||
</FilesMatch>
|
||||
# Unversioned static assets (dev): 7 days
|
||||
<FilesMatch "\.(?:png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$">
|
||||
Header set Cache-Control "public, max-age=604800" env=!has_version_param
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.(js|css|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$">
|
||||
Header set Cache-Control "public, max-age=31536000, immutable" env=has_version_param
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
# Versioned assets (?v=...): 1 year + immutable
|
||||
<FilesMatch "\.(?:m?js|css|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$">
|
||||
Header set Cache-Control "public, max-age=31536000, immutable" env=has_version_param
|
||||
</FilesMatch>
|
||||
|
||||
# Compression (only if module exists)
|
||||
# Compression (if modules exist)
|
||||
<IfModule mod_brotli.c>
|
||||
BrotliCompressionQuality 5
|
||||
AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json image/svg+xml
|
||||
|
||||
@@ -19,12 +19,8 @@ if (isset($_GET['spec'])) {
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>FileRise API Docs</title>
|
||||
|
||||
<!-- Local ReDoc bundle -->
|
||||
<script defer src="/vendor/redoc/redoc.standalone.js?v=1.6.11"></script>
|
||||
|
||||
<!-- Your init (also local) -->
|
||||
<script defer src="/js/redoc-init.js?v=1.6.11"></script>
|
||||
<script defer src="/vendor/redoc/redoc.standalone.js?v={{APP_QVER}}"></script>
|
||||
<script defer src="/js/redoc-init.js?v={{APP_QVER}}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="/api.php?spec=1"></redoc>
|
||||
|
||||
9
public/api/siteConfig.php
Normal file
9
public/api/siteConfig.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/siteConfig.php
|
||||
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||
|
||||
$userController = new UserController();
|
||||
$userController->siteConfig();
|
||||
@@ -27,7 +27,9 @@ body {
|
||||
padding-left: 30px !important;
|
||||
padding-right: 30px !important;
|
||||
}}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.zones-toggle { left: 85px !important; }
|
||||
}
|
||||
/* ===========================================================
|
||||
HEADER & NAVIGATION
|
||||
=========================================================== */
|
||||
@@ -35,7 +37,7 @@ body {
|
||||
/************************************************************/
|
||||
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
|
||||
/************************************************************/
|
||||
|
||||
.header-logo .logo { height: 50px; width: auto; display: block; }
|
||||
.btn-login {
|
||||
margin-top: 10px;
|
||||
}/* Color overrides */
|
||||
|
||||
@@ -11,29 +11,31 @@
|
||||
<meta name="share-url" content="">
|
||||
|
||||
<style>.main-wrapper{display:none}#loadingOverlay{position:fixed;inset:0;background:var(--bg-color,#fff);z-index:9999;display:flex;align-items:center;justify-content:center}</style>
|
||||
<link rel="stylesheet" href="/css/vendor/roboto.css?v=1.6.11">
|
||||
<link rel="stylesheet" href="/css/vendor/material-icons.css?v=1.6.11">
|
||||
<link rel="stylesheet" href="/css/vendor/roboto.css?v={{APP_QVER}}">
|
||||
<link rel="stylesheet" href="/css/vendor/material-icons.css?v={{APP_QVER}}">
|
||||
|
||||
<!-- Bootstrap CSS (local) -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap/4.5.2/bootstrap.min.css?v=1.6.11">
|
||||
<link rel="stylesheet" href="/vendor/bootstrap/4.5.2/bootstrap.min.css?v={{APP_QVER}}">
|
||||
|
||||
<!-- CodeMirror CSS (local) -->
|
||||
<link rel="stylesheet" href="/vendor/codemirror/5.65.5/codemirror.min.css?v=1.6.11">
|
||||
<link rel="stylesheet" href="/vendor/codemirror/5.65.5/theme/material-darker.min.css?v=1.6.11">
|
||||
<link rel="stylesheet" href="/vendor/codemirror/5.65.5/codemirror.min.css?v={{APP_QVER}}">
|
||||
<link rel="stylesheet" href="/vendor/codemirror/5.65.5/theme/material-darker.min.css?v={{APP_QVER}}">
|
||||
|
||||
<!-- app CSS -->
|
||||
<link rel="stylesheet" href="/css/styles.css?v=1.6.11">
|
||||
<link rel="stylesheet" href="/css/styles.css?v={{APP_QVER}}">
|
||||
|
||||
<!-- Libraries (JS) -->
|
||||
<script src="/vendor/dompurify/2.4.0/purify.min.js?v=1.6.11"></script>
|
||||
<script src="/vendor/fuse/6.6.2/fuse.min.js?v=1.6.11"></script>
|
||||
<script src="/vendor/resumable/1.1.0/resumable.min.js?v=1.6.11"></script>
|
||||
<script src="/vendor/dompurify/2.4.0/purify.min.js?v={{APP_QVER}}"></script>
|
||||
<script src="/vendor/fuse/6.6.2/fuse.min.js?v={{APP_QVER}}"></script>
|
||||
<script src="/vendor/resumable/1.1.0/resumable.min.js?v={{APP_QVER}}"></script>
|
||||
|
||||
<!-- CodeMirror core FIRST, then modes -->
|
||||
<script src="/vendor/codemirror/5.65.5/codemirror.min.js?v=1.6.11"></script>
|
||||
<!-- CodeMirror core FIRST -->
|
||||
<script src="/vendor/codemirror/5.65.5/codemirror.min.js?v={{APP_QVER}}"></script>
|
||||
|
||||
<script src="/js/version.js?v=1.6.11"></script>
|
||||
<script type="module" src="/js/main.js"></script>
|
||||
<script src="/js/version.js?v={{APP_QVER}}"></script>
|
||||
|
||||
<link rel="modulepreload" href="/js/main.js?v={{APP_QVER}}">
|
||||
<script type="module" src="/js/main.js?v={{APP_QVER}}"></script>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -42,67 +44,7 @@
|
||||
<div class="header-left">
|
||||
<a href="index.html">
|
||||
<div class="header-logo">
|
||||
<svg version="1.1" id="filingCabinetLogo" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" xml:space="preserve">
|
||||
<defs>
|
||||
<!-- Gradient for the cabinet body -->
|
||||
<linearGradient id="cabinetGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<!-- Drop shadow filter with animated attributes for a lifting effect -->
|
||||
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow id="dropShadow" dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2">
|
||||
<!-- Animate the vertical offset: from 2 to 1 (as it rises), hold, then back to 2 -->
|
||||
<animate attributeName="dy" values="2;1;1;2" keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
||||
<!-- Animate the blur similarly: from 2 to 1.5 then back to 2 -->
|
||||
<animate attributeName="stdDeviation" values="2;1.5;1.5;2" keyTimes="0;0.2;0.8;1" dur="5s"
|
||||
fill="freeze" />
|
||||
</feDropShadow>
|
||||
</filter>
|
||||
</defs>
|
||||
<style type="text/css">
|
||||
/* Cabinet with gradient, white outline, and drop shadow */
|
||||
.cabinet {
|
||||
fill: url(#cabinetGradient);
|
||||
stroke: white;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.divider {
|
||||
stroke: #1565C0;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
fill: #FFFFFF;
|
||||
}
|
||||
|
||||
.handle {
|
||||
fill: #1565C0;
|
||||
}
|
||||
</style>
|
||||
<!-- Group that will animate upward and then back down once -->
|
||||
<g id="cabinetGroup">
|
||||
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
|
||||
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
|
||||
<!-- Divider lines for drawers -->
|
||||
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
|
||||
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
|
||||
<!-- Drawers with Handles -->
|
||||
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||
<circle cx="54" cy="27" r="1.5" class="handle" />
|
||||
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||
<circle cx="54" cy="39" r="1.5" class="handle" />
|
||||
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||
<circle cx="54" cy="51" r="1.5" class="handle" />
|
||||
<!-- Additional detail: a small top handle on the cabinet door -->
|
||||
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
|
||||
<!-- Animate transform: rises by 2 pixels over 1s, holds for 3s, then falls over 1s (total 5s) -->
|
||||
<animateTransform attributeName="transform" type="translate" values="0 0; 0 -2; 0 -2; 0 0"
|
||||
keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
||||
</g>
|
||||
</svg>
|
||||
<img src="/assets/logo.svg?v={{APP_QVER}}" alt="FileRise" class="logo" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// adminPanel.js
|
||||
import { t } from './i18n.js';
|
||||
import { loadAdminConfigFunc } from './auth.js';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
|
||||
const version = window.APP_VERSION || "dev";
|
||||
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||
|
||||
160
public/js/appCore.js
Normal file
160
public/js/appCore.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// /js/appCore.js
|
||||
import { showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js?v={{APP_QVER}}';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js?v={{APP_QVER}}';
|
||||
import { initTagSearch } from './fileTags.js?v={{APP_QVER}}';
|
||||
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { initUpload } from './upload.js?v={{APP_QVER}}';
|
||||
import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||||
|
||||
// Keep a bound handle to the native fetch so wrappers elsewhere never recurse
|
||||
const _nativeFetch = window.fetch.bind(window);
|
||||
|
||||
/* =========================
|
||||
CSRF UTILITIES (shared)
|
||||
========================= */
|
||||
export function setCsrfToken(token) {
|
||||
if (!token) return;
|
||||
window.csrfToken = token;
|
||||
localStorage.setItem('csrf', token);
|
||||
|
||||
// meta tag for easy access in other places
|
||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.content = token;
|
||||
}
|
||||
|
||||
export function getCsrfToken() {
|
||||
return window.csrfToken || localStorage.getItem('csrf') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap/refresh CSRF from the server.
|
||||
* Uses the native fetch to avoid wrapper loops and accepts rotated tokens via header.
|
||||
*/
|
||||
export async function loadCsrfToken() {
|
||||
const res = await _nativeFetch('/api/auth/token.php', { method: 'GET', credentials: 'include' });
|
||||
|
||||
// header-based rotation
|
||||
const hdr = res.headers.get('X-CSRF-Token');
|
||||
if (hdr) setCsrfToken(hdr);
|
||||
|
||||
// body (if provided)
|
||||
let body = {};
|
||||
try { body = await res.json(); } catch { /* token endpoint may return empty */ }
|
||||
|
||||
const token = body.csrf_token || getCsrfToken();
|
||||
setCsrfToken(token);
|
||||
|
||||
// share-url meta should reflect the actual origin
|
||||
const actualShare = window.location.origin;
|
||||
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||||
if (!shareMeta) {
|
||||
shareMeta = document.createElement('meta');
|
||||
shareMeta.name = 'share-url';
|
||||
document.head.appendChild(shareMeta);
|
||||
}
|
||||
shareMeta.content = actualShare;
|
||||
|
||||
return { csrf_token: token, share_url: actualShare };
|
||||
}
|
||||
|
||||
/* =========================
|
||||
APP INIT (shared)
|
||||
========================= */
|
||||
export function initializeApp() {
|
||||
const saved = parseInt(localStorage.getItem('rowHeight') || '48', 10);
|
||||
document.documentElement.style.setProperty('--file-row-height', saved + 'px');
|
||||
|
||||
const last = localStorage.getItem('lastOpenedFolder');
|
||||
window.currentFolder = last ? last : "root";
|
||||
|
||||
const stored = localStorage.getItem('showFoldersInList');
|
||||
window.showFoldersInList = stored === null ? true : stored === 'true';
|
||||
|
||||
// Load public site config early (safe subset)
|
||||
loadAdminConfigFunc();
|
||||
|
||||
// Enable tag search UI; initial file list load is controlled elsewhere
|
||||
initTagSearch();
|
||||
|
||||
// Hook DnD relay from fileList area into upload area
|
||||
const fileListArea = document.getElementById('fileListContainer');
|
||||
const uploadArea = document.getElementById('uploadDropArea');
|
||||
if (fileListArea && uploadArea) {
|
||||
fileListArea.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
fileListArea.classList.add('drop-hover');
|
||||
});
|
||||
fileListArea.addEventListener('dragleave', () => {
|
||||
fileListArea.classList.remove('drop-hover');
|
||||
});
|
||||
fileListArea.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
fileListArea.classList.remove('drop-hover');
|
||||
uploadArea.dispatchEvent(new DragEvent('drop', {
|
||||
dataTransfer: e.dataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// App subsystems
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
|
||||
// Only run trash/restore for admins
|
||||
const isAdmin =
|
||||
localStorage.getItem('isAdmin') === '1' || localStorage.getItem('isAdmin') === 'true';
|
||||
if (isAdmin) {
|
||||
setupTrashRestoreDelete();
|
||||
}
|
||||
|
||||
// Small help tooltip toggle
|
||||
const helpBtn = document.getElementById("folderHelpBtn");
|
||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||
if (helpBtn && helpTooltip) {
|
||||
helpBtn.addEventListener("click", () => {
|
||||
helpTooltip.style.display =
|
||||
helpTooltip.style.display === "block" ? "none" : "block";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
LOGOUT (shared)
|
||||
========================= */
|
||||
export function triggerLogout() {
|
||||
_nativeFetch("/api/auth/logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": getCsrfToken() }
|
||||
})
|
||||
.then(() => window.location.reload(true))
|
||||
.catch(() => { /* no-op */ });
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Global UX guard (unchanged)
|
||||
========================= */
|
||||
window.addEventListener("unhandledrejection", (ev) => {
|
||||
const msg = (ev?.reason && ev.reason.message) || "";
|
||||
if (msg === "auth") {
|
||||
showToast(t("please_sign_in_again") || "Please sign in again.", "error");
|
||||
ev.preventDefault();
|
||||
} else if (msg === "forbidden") {
|
||||
showToast(t("no_access_to_resource") || "You don’t have access to that.", "error");
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations } from './i18n.js';
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
import { t, applyTranslations } from './i18n.js?v={{APP_QVER}}';
|
||||
import {
|
||||
toggleVisibility,
|
||||
showToast as originalShowToast,
|
||||
attachEnterKeyListener,
|
||||
showCustomConfirmModal
|
||||
} from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { initFileActions } from './fileActions.js';
|
||||
import { renderFileTable } from './fileListView.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
} from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { renderFileTable } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import {
|
||||
openTOTPLoginModal as originalOpenTOTPLoginModal,
|
||||
openUserPanel,
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
closeTOTPModal,
|
||||
setLastLoginData,
|
||||
openApiModal
|
||||
} from './authModals.js';
|
||||
import { openAdminPanel } from './adminPanel.js';
|
||||
import { initializeApp, triggerLogout } from './main.js';
|
||||
} from './authModals.js?v={{APP_QVER}}';
|
||||
import { openAdminPanel } from './adminPanel.js?v={{APP_QVER}}';
|
||||
import { initializeApp, triggerLogout } from './appCore.js?v={{APP_QVER}}';
|
||||
|
||||
// Production OIDC configuration (override via API as needed)
|
||||
const currentOIDCConfig = {
|
||||
@@ -180,7 +180,7 @@ function updateLoginOptionsUIFromStorage() {
|
||||
}
|
||||
|
||||
export function loadAdminConfigFunc() {
|
||||
return fetch("/api/admin/getConfig.php", { credentials: "include" })
|
||||
return fetch("/api/siteConfig.php", { credentials: "include" })
|
||||
.then(async (response) => {
|
||||
// If a proxy or some edge returns 204/empty, handle gracefully
|
||||
let config = {};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
import { loadAdminConfigFunc, updateAuthenticatedUI } from './auth.js';
|
||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js?v={{APP_QVER}}';
|
||||
import { loadAdminConfigFunc, updateAuthenticatedUI } from './auth.js?v={{APP_QVER}}';
|
||||
|
||||
let lastLoginData = null;
|
||||
export function setLastLoginData(data) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// domUtils.js
|
||||
import { t } from './i18n.js';
|
||||
import { openDownloadModal } from './fileActions.js';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}';
|
||||
|
||||
// Basic DOM Helpers
|
||||
export function toggleVisibility(elementId, shouldShow) {
|
||||
|
||||
@@ -489,18 +489,12 @@ function mountHeaderToggle(btn) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function ensureZonesToggle() {
|
||||
const isAuthed = document.body.classList.contains('authenticated');
|
||||
let btn = document.getElementById('sidebarToggleFloating');
|
||||
const host = getHeaderHost();
|
||||
if (!host) return;
|
||||
|
||||
// If not authenticated, make sure the button is gone and bail.
|
||||
if (!isAuthed) {
|
||||
if (btn) btn.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure the host is a positioning context
|
||||
const hostStyle = getComputedStyle(host);
|
||||
if (hostStyle.position === 'static') {
|
||||
@@ -542,6 +536,7 @@ function ensureZonesToggle() {
|
||||
padding: '0',
|
||||
lineHeight: '0'
|
||||
});
|
||||
btn.classList.add('zones-toggle');
|
||||
|
||||
// Dark mode polish
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// fileActions.js
|
||||
import { showToast, attachEnterKeyListener } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { formatFolderName } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
import { showToast, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { formatFolderName } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
export function handleDeleteSelected(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// fileDragDrop.js
|
||||
import { showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
|
||||
export function fileDragStartHandler(event) {
|
||||
const row = event.currentTarget;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// fileEditor.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
// thresholds for editor behavior
|
||||
const EDITOR_PLAIN_THRESHOLD = 5 * 1024 * 1024; // >5 MiB => force plain text, lighter settings
|
||||
@@ -14,30 +14,30 @@ const CM_LOCAL = "/vendor/codemirror/5.65.5/";
|
||||
// Which mode file to load for a given name/mime
|
||||
const MODE_URL = {
|
||||
// core/common
|
||||
"xml": "mode/xml/xml.min.js",
|
||||
"css": "mode/css/css.min.js",
|
||||
"javascript": "mode/javascript/javascript.min.js",
|
||||
"xml": "mode/xml/xml.min.js?v={{APP_QVER}}",
|
||||
"css": "mode/css/css.min.js?v={{APP_QVER}}",
|
||||
"javascript": "mode/javascript/javascript.min.js?v={{APP_QVER}}",
|
||||
|
||||
// meta / combos
|
||||
"htmlmixed": "mode/htmlmixed/htmlmixed.min.js",
|
||||
"application/x-httpd-php": "mode/php/php.min.js",
|
||||
"htmlmixed": "mode/htmlmixed/htmlmixed.min.js?v={{APP_QVER}}",
|
||||
"application/x-httpd-php": "mode/php/php.min.js?v={{APP_QVER}}",
|
||||
|
||||
// docs / data
|
||||
"markdown": "mode/markdown/markdown.min.js",
|
||||
"yaml": "mode/yaml/yaml.min.js",
|
||||
"properties": "mode/properties/properties.min.js",
|
||||
"sql": "mode/sql/sql.min.js",
|
||||
"markdown": "mode/markdown/markdown.min.js?v={{APP_QVER}}",
|
||||
"yaml": "mode/yaml/yaml.min.js?v={{APP_QVER}}",
|
||||
"properties": "mode/properties/properties.min.js?v={{APP_QVER}}",
|
||||
"sql": "mode/sql/sql.min.js?v={{APP_QVER}}",
|
||||
|
||||
// shells
|
||||
"shell": "mode/shell/shell.min.js",
|
||||
"shell": "mode/shell/shell.min.js?v={{APP_QVER}}",
|
||||
|
||||
// languages
|
||||
"python": "mode/python/python.min.js",
|
||||
"text/x-csrc": "mode/clike/clike.min.js",
|
||||
"text/x-c++src": "mode/clike/clike.min.js",
|
||||
"text/x-java": "mode/clike/clike.min.js",
|
||||
"text/x-csharp": "mode/clike/clike.min.js",
|
||||
"text/x-kotlin": "mode/clike/clike.min.js"
|
||||
"python": "mode/python/python.min.js?v={{APP_QVER}}",
|
||||
"text/x-csrc": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||
"text/x-c++src": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||
"text/x-java": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||
"text/x-csharp": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||
"text/x-kotlin": "mode/clike/clike.min.js?v={{APP_QVER}}"
|
||||
};
|
||||
|
||||
// Map any mime/alias to the key we use in MODE_URL
|
||||
@@ -54,7 +54,7 @@ const MODE_LOAD_TIMEOUT_MS = 2500; // allow closing immediately; don't wait fore
|
||||
function loadScriptOnce(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ver = (window.APP_VERSION ?? 'dev').replace(/^v/, ''); // "v1.6.9" -> "1.6.9"
|
||||
const withQS = url + '?v=' + ver;
|
||||
const withQS = url; //+ '?v=' + ver;
|
||||
|
||||
const key = `cm:${withQS}`;
|
||||
let s = document.querySelector(`script[data-key="${key}"]`);
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
updateRowHighlight,
|
||||
toggleRowSelection,
|
||||
attachEnterKeyListener
|
||||
} from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { bindFileListContextMenu } from './fileMenu.js';
|
||||
import { openDownloadModal } from './fileActions.js';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||
} from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { bindFileListContextMenu } from './fileMenu.js?v={{APP_QVER}}';
|
||||
import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
|
||||
import {
|
||||
getParentFolder,
|
||||
updateBreadcrumbTitle,
|
||||
@@ -24,13 +24,13 @@ import {
|
||||
hideFolderManagerContextMenu,
|
||||
openRenameFolderModal,
|
||||
openDeleteFolderModal
|
||||
} from './folderManager.js';
|
||||
import { openFolderShareModal } from './folderShareModal.js';
|
||||
} from './folderManager.js?v={{APP_QVER}}';
|
||||
import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}';
|
||||
import {
|
||||
folderDragOverHandler,
|
||||
folderDragLeaveHandler,
|
||||
folderDropHandler
|
||||
} from './fileDragDrop.js';
|
||||
} from './fileDragDrop.js?v={{APP_QVER}}';
|
||||
|
||||
export let fileData = [];
|
||||
export let sortOrder = { column: "uploaded", ascending: true };
|
||||
@@ -750,7 +750,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./fileEditor.js');
|
||||
const m = await import('./fileEditor.js?v={{APP_QVER}}');
|
||||
m.editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||
});
|
||||
});
|
||||
@@ -759,7 +759,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./fileActions.js');
|
||||
const m = await import('./fileActions.js?v={{APP_QVER}}');
|
||||
m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||
});
|
||||
});
|
||||
@@ -768,7 +768,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".preview-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./filePreview.js');
|
||||
const m = await import('./filePreview.js?v={{APP_QVER}}');
|
||||
m.previewFile(btn.dataset.previewUrl, btn.dataset.previewName);
|
||||
});
|
||||
});
|
||||
@@ -822,7 +822,7 @@ function wireSelectAll(fileListContent) {
|
||||
const fileName = this.getAttribute("data-file");
|
||||
const file = fileData.find(f => f.name === fileName);
|
||||
if (file) {
|
||||
import('./filePreview.js').then(module => {
|
||||
import('./filePreview.js?v={{APP_QVER}}').then(module => {
|
||||
module.openShareModal(file, folder);
|
||||
});
|
||||
}
|
||||
@@ -831,7 +831,7 @@ function wireSelectAll(fileListContent) {
|
||||
updateFileActionButtons();
|
||||
document.querySelectorAll("#fileList tbody tr").forEach(row => {
|
||||
row.setAttribute("draggable", "true");
|
||||
import('./fileDragDrop.js').then(module => {
|
||||
import('./fileDragDrop.js?v={{APP_QVER}}').then(module => {
|
||||
row.addEventListener("dragstart", module.fileDragStartHandler);
|
||||
});
|
||||
});
|
||||
@@ -1085,7 +1085,7 @@ function wireSelectAll(fileListContent) {
|
||||
// preview clicks (dynamic import to avoid global dependency)
|
||||
fileListContent.querySelectorAll(".gallery-preview").forEach(el => {
|
||||
el.addEventListener("click", async () => {
|
||||
const m = await import('./filePreview.js');
|
||||
const m = await import('./filePreview.js?v={{APP_QVER}}');
|
||||
m.previewFile(el.dataset.previewUrl, el.dataset.previewName);
|
||||
});
|
||||
});
|
||||
@@ -1102,7 +1102,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./fileEditor.js');
|
||||
const m = await import('./fileEditor.js?v={{APP_QVER}}');
|
||||
m.editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||
});
|
||||
});
|
||||
@@ -1111,7 +1111,7 @@ function wireSelectAll(fileListContent) {
|
||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||
btn.addEventListener("click", async e => {
|
||||
e.stopPropagation();
|
||||
const m = await import('./fileActions.js');
|
||||
const m = await import('./fileActions.js?v={{APP_QVER}}');
|
||||
m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||
});
|
||||
});
|
||||
@@ -1123,7 +1123,7 @@ function wireSelectAll(fileListContent) {
|
||||
const fileName = btn.dataset.file;
|
||||
const fileObj = fileData.find(f => f.name === fileName);
|
||||
if (fileObj) {
|
||||
import('./filePreview.js').then(m => m.openShareModal(fileObj, folder));
|
||||
import('./filePreview.js?v={{APP_QVER}}').then(m => m.openShareModal(fileObj, folder));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// fileManager.js
|
||||
import './fileListView.js';
|
||||
import './filePreview.js';
|
||||
import './fileEditor.js';
|
||||
import './fileDragDrop.js';
|
||||
import './fileMenu.js';
|
||||
import { initFileActions } from './fileActions.js';
|
||||
import './fileListView.js?v={{APP_QVER}}';
|
||||
import './filePreview.js?v={{APP_QVER}}';
|
||||
import './fileEditor.js?v={{APP_QVER}}';
|
||||
import './fileDragDrop.js?v={{APP_QVER}}';
|
||||
import './fileMenu.js?v={{APP_QVER}}';
|
||||
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||
|
||||
// Initialize file action buttons.
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
@@ -14,7 +14,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// Attach folder drag-and-drop support for folder tree nodes.
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll(".folder-option").forEach(el => {
|
||||
import('./fileDragDrop.js').then(module => {
|
||||
import('./fileDragDrop.js?v={{APP_QVER}}').then(module => {
|
||||
el.addEventListener("dragover", module.folderDragOverHandler);
|
||||
el.addEventListener("dragleave", module.folderDragLeaveHandler);
|
||||
el.addEventListener("drop", module.folderDropHandler);
|
||||
@@ -32,7 +32,7 @@ document.addEventListener("keydown", function(e) {
|
||||
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
||||
if (selectedCheckboxes.length > 0) {
|
||||
e.preventDefault();
|
||||
import('./fileActions.js').then(module => {
|
||||
import('./fileActions.js?v={{APP_QVER}}').then(module => {
|
||||
module.handleDeleteSelected(new Event("click"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// fileMenu.js
|
||||
import { updateRowHighlight, showToast } from './domUtils.js';
|
||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js';
|
||||
import { previewFile } from './filePreview.js';
|
||||
import { editFile } from './fileEditor.js';
|
||||
import { canEditFile, fileData } from './fileListView.js';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||
import { t } from './i18n.js';
|
||||
import { updateRowHighlight, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { previewFile } from './filePreview.js?v={{APP_QVER}}';
|
||||
import { editFile } from './fileEditor.js?v={{APP_QVER}}';
|
||||
import { canEditFile, fileData } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
export function showFileContextMenu(x, y, menuItems) {
|
||||
let menu = document.getElementById("fileContextMenu");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// filePreview.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { fileData } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { fileData } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
export function openShareModal(file, folder) {
|
||||
// Remove any existing modal
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
// adding tags to files (with a global tag store for reuse),
|
||||
// updating the file row display with tag badges,
|
||||
// filtering the file list by tag, and persisting tag data.
|
||||
import { escapeHTML } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { renderFileTable, renderGalleryView } from './fileListView.js';
|
||||
import { escapeHTML } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { renderFileTable, renderGalleryView } from './fileListView.js?v={{APP_QVER}}';
|
||||
|
||||
export function openTagModal(file) {
|
||||
// Create the modal element.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// folderManager.js
|
||||
|
||||
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';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}';
|
||||
import { fetchWithCsrf } from './auth.js?v={{APP_QVER}}';
|
||||
import { loadCsrfToken } from './appCore.js?v={{APP_QVER}}';
|
||||
|
||||
/* ----------------------
|
||||
Helpers: safe JSON + state
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// js/folderShareModal.js
|
||||
import { escapeHTML, showToast } from './domUtils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
export function openFolderShareModal(folder) {
|
||||
// Remove any existing modal
|
||||
|
||||
@@ -1,55 +1,65 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, fetchWithCsrf, checkAuthentication, loadAdminConfigFunc } from './auth.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||
import { displayFilePreview } from './filePreview.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload } from './fileActions.js';
|
||||
import { editFile, saveFile } from './fileEditor.js';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||
// /js/main.js
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { initUpload } from './upload.js?v={{APP_QVER}}';
|
||||
import { initAuth, fetchWithCsrf, checkAuthentication, loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js?v={{APP_QVER}}';
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js?v={{APP_QVER}}';
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js?v={{APP_QVER}}';
|
||||
import { displayFilePreview } from './filePreview.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { editFile, saveFile } from './fileEditor.js?v={{APP_QVER}}';
|
||||
import { t, applyTranslations, setLocale } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
// NEW: import shared helpers from appCore (moved out of main.js)
|
||||
import {
|
||||
initializeApp,
|
||||
loadCsrfToken,
|
||||
triggerLogout,
|
||||
setCsrfToken,
|
||||
getCsrfToken
|
||||
} from './appCore.js?v={{APP_QVER}}';
|
||||
|
||||
/* =========================
|
||||
CSRF HOTFIX UTILITIES
|
||||
========================= */
|
||||
const _nativeFetch = window.fetch; // keep the real fetch
|
||||
|
||||
function setCsrfToken(token) {
|
||||
if (!token) return;
|
||||
window.csrfToken = token;
|
||||
localStorage.setItem('csrf', token);
|
||||
|
||||
// meta tag for easy access in other places
|
||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.content = token;
|
||||
}
|
||||
function getCsrfToken() {
|
||||
return window.csrfToken || localStorage.getItem('csrf') || '';
|
||||
}
|
||||
// Keep a handle to the native fetch so wrappers never recurse
|
||||
const _nativeFetch = window.fetch.bind(window);
|
||||
|
||||
// Seed CSRF from storage ASAP (before any requests)
|
||||
setCsrfToken(getCsrfToken());
|
||||
|
||||
// Wrap the existing fetchWithCsrf so we also capture rotated tokens from headers.
|
||||
// Wrap fetch so *all* callers get CSRF header + token rotation, without recursion
|
||||
async function fetchWithCsrfAndRefresh(input, init = {}) {
|
||||
const res = await fetchWithCsrf(input, init);
|
||||
const headers = new Headers(init?.headers || {});
|
||||
const token = getCsrfToken();
|
||||
|
||||
if (token && !headers.has('X-CSRF-Token')) {
|
||||
headers.set('X-CSRF-Token', token);
|
||||
}
|
||||
|
||||
const res = await _nativeFetch(input, {
|
||||
credentials: 'include',
|
||||
...init,
|
||||
headers,
|
||||
});
|
||||
|
||||
try {
|
||||
const rotated = res.headers?.get('X-CSRF-Token');
|
||||
if (rotated) setCsrfToken(rotated);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// Replace global fetch with the wrapped version so *all* callers benefit.
|
||||
window.fetch = fetchWithCsrfAndRefresh;
|
||||
// Avoid double-wrapping if this module re-evaluates for any reason
|
||||
if (!window.fetch || !window.fetch._frWrapped) {
|
||||
const wrapped = fetchWithCsrfAndRefresh;
|
||||
Object.defineProperty(wrapped, '_frWrapped', { value: true });
|
||||
window.fetch = wrapped;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
SAFE API HELPERS
|
||||
@@ -84,6 +94,7 @@ export async function apiPOSTJSON(url, body, opts = {}) {
|
||||
// Optional: expose on window for legacy callers
|
||||
window.apiGETJSON = apiGETJSON;
|
||||
window.apiPOSTJSON = apiPOSTJSON;
|
||||
window.triggerLogout = triggerLogout; // expose the moved helper
|
||||
|
||||
// Global handler to keep UX friendly if something forgets to catch
|
||||
window.addEventListener("unhandledrejection", (ev) => {
|
||||
@@ -98,134 +109,16 @@ window.addEventListener("unhandledrejection", (ev) => {
|
||||
});
|
||||
|
||||
/* =========================
|
||||
APP INIT
|
||||
BOOTSTRAP
|
||||
========================= */
|
||||
|
||||
export function initializeApp() {
|
||||
const saved = parseInt(localStorage.getItem('rowHeight') || '48', 10);
|
||||
document.documentElement.style.setProperty('--file-row-height', saved + 'px');
|
||||
|
||||
//window.currentFolder = "root";
|
||||
const last = localStorage.getItem('lastOpenedFolder');
|
||||
window.currentFolder = last ? last : "root";
|
||||
const stored = localStorage.getItem('showFoldersInList');
|
||||
window.showFoldersInList = stored === null ? true : stored === 'true';
|
||||
loadAdminConfigFunc();
|
||||
initTagSearch();
|
||||
//loadFileList(window.currentFolder);
|
||||
|
||||
const fileListArea = document.getElementById('fileListContainer');
|
||||
const uploadArea = document.getElementById('uploadDropArea');
|
||||
if (fileListArea && uploadArea) {
|
||||
fileListArea.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
fileListArea.classList.add('drop-hover');
|
||||
});
|
||||
fileListArea.addEventListener('dragleave', () => {
|
||||
fileListArea.classList.remove('drop-hover');
|
||||
});
|
||||
fileListArea.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
fileListArea.classList.remove('drop-hover');
|
||||
uploadArea.dispatchEvent(new DragEvent('drop', {
|
||||
dataTransfer: e.dataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
// Only run trash/restore for admins
|
||||
const isAdmin =
|
||||
localStorage.getItem('isAdmin') === '1' || localStorage.getItem('isAdmin') === 'true';
|
||||
if (isAdmin) {
|
||||
setupTrashRestoreDelete();
|
||||
}
|
||||
|
||||
const helpBtn = document.getElementById("folderHelpBtn");
|
||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||
if (helpBtn && helpTooltip) {
|
||||
helpBtn.addEventListener("click", () => {
|
||||
helpTooltip.style.display =
|
||||
helpTooltip.style.display === "block" ? "none" : "block";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap/refresh CSRF from the server.
|
||||
* Uses the *native* fetch to avoid any wrapper loops and to work even if we don't
|
||||
* yet have a token. Also accepts a rotated token from the response header.
|
||||
*/
|
||||
export function loadCsrfToken() {
|
||||
return _nativeFetch('/api/auth/token.php', { method: 'GET', credentials: 'include' })
|
||||
.then(async res => {
|
||||
// header-based rotation
|
||||
const hdr = res.headers.get('X-CSRF-Token');
|
||||
if (hdr) setCsrfToken(hdr);
|
||||
|
||||
// body (if provided)
|
||||
let body = {};
|
||||
try { body = await res.json(); } catch { /* token endpoint may return empty */ }
|
||||
|
||||
const token = body.csrf_token || getCsrfToken();
|
||||
setCsrfToken(token);
|
||||
|
||||
// share-url meta should reflect the actual origin
|
||||
const actualShare = window.location.origin;
|
||||
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||||
if (!shareMeta) {
|
||||
shareMeta = document.createElement('meta');
|
||||
shareMeta.name = 'share-url';
|
||||
document.head.appendChild(shareMeta);
|
||||
}
|
||||
shareMeta.content = actualShare;
|
||||
|
||||
return { csrf_token: token, share_url: actualShare };
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Immediately clear “?logout=1” flag
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('logout') === '1') {
|
||||
localStorage.removeItem("username");
|
||||
localStorage.removeItem("userTOTPEnabled");
|
||||
}
|
||||
|
||||
export function triggerLogout() {
|
||||
_nativeFetch("/api/auth/logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": getCsrfToken() }
|
||||
})
|
||||
.then(() => {
|
||||
document.body.classList.remove('authenticated');
|
||||
window.location.reload(true);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// Expose functions for inline handlers.
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
window.toggleAllCheckboxes = toggleAllCheckboxes;
|
||||
window.editFile = editFile;
|
||||
window.saveFile = saveFile;
|
||||
window.renameFile = renameFile;
|
||||
window.confirmSingleDownload = confirmSingleDownload;
|
||||
window.openDownloadModal = openDownloadModal;
|
||||
|
||||
// Global variable for the current folder.
|
||||
window.currentFolder = "root";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Load admin config early
|
||||
// Load site config early (safe subset)
|
||||
loadAdminConfigFunc();
|
||||
|
||||
// i18n
|
||||
@@ -242,12 +135,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// 3) If authenticated, start app
|
||||
checkAuthentication().then(authenticated => {
|
||||
if (authenticated) {
|
||||
document.body.classList.add('authenticated');
|
||||
const overlay = document.getElementById('loadingOverlay');
|
||||
if (overlay) overlay.remove();
|
||||
initializeApp();
|
||||
} else {
|
||||
document.body.classList.remove('authenticated');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -309,4 +199,17 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
window.scrollBy(0, SCROLL_SPEED);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Expose functions for inline handlers
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
window.toggleAllCheckboxes = toggleAllCheckboxes;
|
||||
window.editFile = editFile;
|
||||
window.saveFile = saveFile;
|
||||
window.renameFile = renameFile;
|
||||
window.confirmSingleDownload = confirmSingleDownload;
|
||||
window.openDownloadModal = openDownloadModal;
|
||||
|
||||
// Global variable for the current folder (initial default; initializeApp will update)
|
||||
window.currentFolder = "root";
|
||||
@@ -1,9 +1,9 @@
|
||||
// trashRestoreDelete.js
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { t } from './i18n.js';
|
||||
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||
import { toggleVisibility, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
function showConfirm(message, onConfirm) {
|
||||
const modal = document.getElementById("customConfirmModal");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { initFileActions } from './fileActions.js';
|
||||
import { displayFilePreview } from './filePreview.js';
|
||||
import { showToast, escapeHTML } from './domUtils.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
import { loadFileList } from './fileListView.js';
|
||||
import { t } from './i18n.js';
|
||||
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||
import { displayFilePreview } from './filePreview.js?v={{APP_QVER}}';
|
||||
import { showToast, escapeHTML } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
|
||||
/* -----------------------------------------------------
|
||||
Helpers for Drag–and–Drop Folder Uploads (Original Code)
|
||||
|
||||
37
scripts/stamp-assets.sh
Normal file
37
scripts/stamp-assets.sh
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# usage: scripts/stamp-assets.sh v1.6.12 /path/to/target/dir
|
||||
set -euo pipefail
|
||||
VER="${1:?usage: stamp-assets.sh vX.Y.Z target_dir}"
|
||||
QVER="${VER#v}"
|
||||
TARGET="${2:-.}"
|
||||
|
||||
cd "$TARGET"
|
||||
|
||||
# Build file lists. Prefer git ls-files if we're in a repo, else use find.
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
mapfile -t HTML_CSS < <(git ls-files -- 'public/*.html' 'public/**/*.html' 'public/*.php' 'public/**/*.css' || true)
|
||||
mapfile -t JSFILES < <(git ls-files -- 'public/*.js' 'public/**/*.js' 'js/*.js' 'js/**/*.js' || true)
|
||||
else
|
||||
mapfile -t HTML_CSS < <(find public -type f \( -name '*.html' -o -name '*.php' -o -name '*.css' \) -print 2>/dev/null || true)
|
||||
mapfile -t JSFILES < <(find public js -type f -name '*.js' -print 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# HTML/CSS/PHP: stamp ?v=... and {{APP_VER}}
|
||||
for f in "${HTML_CSS[@]}"; do
|
||||
sed -E -i "s/(\?v=)[^\"'&<>\s]*/\1${QVER}/g" "$f"
|
||||
sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f"
|
||||
done
|
||||
|
||||
# JS: stamp placeholders and normalize any pre-existing ?v=...
|
||||
for f in "${JSFILES[@]}"; do
|
||||
sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f"
|
||||
sed -E -i "s/\{\{APP_QVER\}\}/${QVER}/g" "$f"
|
||||
perl -0777 -i -pe "s@(\.m?js)\?v=[^\"')]+@\1?v=${QVER}@g" "$f"
|
||||
done
|
||||
|
||||
# Optional: version.js fallback update
|
||||
if [[ -f public/js/version.js ]]; then
|
||||
sed -E -i "s/(APP_VERSION\s*=\s*['\"])v[^'\"]+(['\"])/\1${VER}\2/" public/js/version.js
|
||||
fi
|
||||
|
||||
echo "Stamped assets in ${TARGET} to ${VER} (${QVER})"
|
||||
@@ -99,7 +99,7 @@ class AdminController
|
||||
'header_title' => '',
|
||||
'loginOptions' => [
|
||||
'disableFormLogin' => false,
|
||||
'disableBasicAuth' => false,
|
||||
'disableBasicAuth' => true,
|
||||
'disableOIDCLogin' => true,
|
||||
'authBypass' => false,
|
||||
'authHeaderName' => 'X-Remote-User'
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||||
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||
|
||||
/**
|
||||
* UserController
|
||||
@@ -665,4 +666,38 @@ class UserController
|
||||
echo json_encode(['success' => true, 'url' => $url]);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function siteConfig(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersDir = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||||
$publicPath = $usersDir . 'siteConfig.json';
|
||||
$adminEncPath = $usersDir . 'adminConfig.json';
|
||||
|
||||
$publicMtime = is_file($publicPath) ? (int)@filemtime($publicPath) : 0;
|
||||
$adminMtime = is_file($adminEncPath) ? (int)@filemtime($adminEncPath) : 0;
|
||||
|
||||
// If public cache is present and fresh enough, serve it
|
||||
if ($publicMtime > 0 && $publicMtime >= $adminMtime) {
|
||||
$raw = @file_get_contents($publicPath);
|
||||
$data = is_string($raw) ? json_decode($raw, true) : null;
|
||||
if (is_array($data)) {
|
||||
echo json_encode($data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise regenerate from decrypted admin config
|
||||
$cfg = AdminModel::getConfig();
|
||||
if (isset($cfg['error'])) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $cfg['error']]);
|
||||
return;
|
||||
}
|
||||
|
||||
$public = AdminModel::buildPublicSubset($cfg);
|
||||
$w = AdminModel::writeSiteConfig($public); // best effort
|
||||
echo json_encode($public);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,51 @@ class AdminModel
|
||||
return (int)$val;
|
||||
}
|
||||
|
||||
public static function buildPublicSubset(array $config): array
|
||||
{
|
||||
return [
|
||||
'header_title' => $config['header_title'] ?? 'FileRise',
|
||||
'loginOptions' => [
|
||||
'disableFormLogin' => (bool)($config['loginOptions']['disableFormLogin'] ?? false),
|
||||
'disableBasicAuth' => (bool)($config['loginOptions']['disableBasicAuth'] ?? false),
|
||||
'disableOIDCLogin' => (bool)($config['loginOptions']['disableOIDCLogin'] ?? false),
|
||||
// do NOT include authBypass/authHeaderName here — admin-only
|
||||
],
|
||||
'globalOtpauthUrl' => $config['globalOtpauthUrl'] ?? '',
|
||||
'enableWebDAV' => (bool)($config['enableWebDAV'] ?? false),
|
||||
'sharedMaxUploadSize' => (int)($config['sharedMaxUploadSize'] ?? 0),
|
||||
'oidc' => [
|
||||
'providerUrl' => (string)($config['oidc']['providerUrl'] ?? ''),
|
||||
'redirectUri' => (string)($config['oidc']['redirectUri'] ?? ''),
|
||||
// never include clientId / clientSecret
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** Write USERS_DIR/siteConfig.json atomically (unencrypted). */
|
||||
public static function writeSiteConfig(array $publicSubset): array
|
||||
{
|
||||
$dest = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . 'siteConfig.json';
|
||||
$tmp = $dest . '.tmp';
|
||||
|
||||
$json = json_encode($publicSubset, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
if ($json === false) {
|
||||
return ["error" => "Failed to encode siteConfig.json"];
|
||||
}
|
||||
|
||||
if (file_put_contents($tmp, $json, LOCK_EX) === false) {
|
||||
return ["error" => "Failed to write temp siteConfig.json"];
|
||||
}
|
||||
|
||||
if (!@rename($tmp, $dest)) {
|
||||
@unlink($tmp);
|
||||
return ["error" => "Failed to move siteConfig.json into place"];
|
||||
}
|
||||
|
||||
@chmod($dest, 0664); // readable in bind mounts
|
||||
return ["success" => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the admin configuration file.
|
||||
*
|
||||
@@ -157,6 +202,14 @@ class AdminModel
|
||||
// Best-effort normalize perms for host visibility (user rw, group rw)
|
||||
@chmod($configFile, 0664);
|
||||
|
||||
$public = self::buildPublicSubset($configUpdate);
|
||||
$w = self::writeSiteConfig($public);
|
||||
// Don’t fail the whole update if public cache write had a minor issue.
|
||||
if (isset($w['error'])) {
|
||||
// Log but keep success for admin write
|
||||
error_log("AdminModel::writeSiteConfig warning: " . $w['error']);
|
||||
}
|
||||
|
||||
return ["success" => "Configuration updated successfully."];
|
||||
}
|
||||
|
||||
@@ -262,7 +315,7 @@ class AdminModel
|
||||
],
|
||||
'loginOptions' => [
|
||||
'disableFormLogin' => false,
|
||||
'disableBasicAuth' => false,
|
||||
'disableBasicAuth' => true,
|
||||
'disableOIDCLogin' => true
|
||||
],
|
||||
'globalOtpauthUrl' => "",
|
||||
|
||||
Reference in New Issue
Block a user