Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a18a8df7af | ||
|
|
8cf5a34ae9 | ||
|
|
55d5656139 | ||
|
|
04be05ad1e | ||
|
|
0469d183de | ||
|
|
b1de8679e0 | ||
|
|
f4f7ec0dca | ||
|
|
5a7c4704d0 | ||
|
|
8b880738d6 | ||
|
|
06c732971f | ||
|
|
ab75381acb | ||
|
|
b1bd903072 | ||
|
|
ab327acc8a | ||
|
|
2e98ceee4c |
12
.github/codeql/codeql-config.yml
vendored
12
.github/codeql/codeql-config.yml
vendored
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
name: "FileRise CodeQL config"
|
|
||||||
paths:
|
|
||||||
- "public/js"
|
|
||||||
- "api"
|
|
||||||
paths-ignore:
|
|
||||||
- "public/vendor/**"
|
|
||||||
- "public/css/vendor/**"
|
|
||||||
- "public/fonts/**"
|
|
||||||
- "public/**/*.min.js"
|
|
||||||
- "public/**/*.min.css"
|
|
||||||
- "public/**/*.map"
|
|
||||||
156
.github/workflows/release-on-version.yml
vendored
156
.github/workflows/release-on-version.yml
vendored
@@ -3,13 +3,12 @@ name: Release on version.js update
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: ["master"]
|
||||||
- master
|
|
||||||
paths:
|
paths:
|
||||||
- public/js/version.js
|
- public/js/version.js
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: "Bump version and sync Changelog to Docker Repo"
|
workflows: ["Bump version and sync Changelog to Docker Repo"]
|
||||||
types: completed
|
types: [completed]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -27,6 +26,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Ensure tags available
|
||||||
|
run: |
|
||||||
|
git fetch --tags --force --prune --quiet
|
||||||
|
|
||||||
- name: Read version from version.js
|
- name: Read version from version.js
|
||||||
id: ver
|
id: ver
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -45,7 +48,6 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
git fetch --tags --quiet
|
|
||||||
if git rev-parse -q --verify "refs/tags/${{ steps.ver.outputs.version }}" >/dev/null; then
|
if git rev-parse -q --verify "refs/tags/${{ steps.ver.outputs.version }}" >/dev/null; then
|
||||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "Tag ${{ steps.ver.outputs.version }} already exists. Skipping release."
|
echo "Tag ${{ steps.ver.outputs.version }} already exists. Skipping release."
|
||||||
@@ -53,7 +55,76 @@ jobs:
|
|||||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Prepare release notes from CHANGELOG.md (optional)
|
# Ensure the stamper is executable and has LF endings (helps if edited on Windows)
|
||||||
|
- name: Prep stamper script
|
||||||
|
if: steps.tagcheck.outputs.exists == 'false'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sed -i 's/\r$//' scripts/stamp-assets.sh || true
|
||||||
|
chmod +x scripts/stamp-assets.sh
|
||||||
|
|
||||||
|
- name: Build zip artifact (stamped)
|
||||||
|
if: steps.tagcheck.outputs.exists == 'false'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
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 (invoke via bash to avoid exec-bit issues)
|
||||||
|
bash ./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 -E "{{APP_QVER}}|{{APP_VER}}" "$ROOT" \
|
||||||
|
--include='*.html' --include='*.php' --include='*.css' --include='*.js' 2>/dev/null; then
|
||||||
|
echo "---- DEBUG (show 10 hits with context) ----"
|
||||||
|
grep -R -n -E "{{APP_QVER}}|{{APP_VER}}" "$ROOT" \
|
||||||
|
--include='*.html' --include='*.php' --include='*.css' --include='*.js' \
|
||||||
|
| head -n 10 | while IFS=: read -r file line _; do
|
||||||
|
echo ">>> $file:$line"
|
||||||
|
nl -ba "$file" | sed -n "$((line-3)),$((line+3))p" || true
|
||||||
|
echo "----------------------------------------"
|
||||||
|
done
|
||||||
|
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'
|
||||||
|
id: sum
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ZIP="FileRise-${{ steps.ver.outputs.version }}.zip"
|
||||||
|
SHA=$(shasum -a 256 "$ZIP" | awk '{print $1}')
|
||||||
|
echo "$SHA $ZIP" > "${ZIP}.sha256"
|
||||||
|
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Computed SHA-256: $SHA"
|
||||||
|
|
||||||
|
- name: Extract notes from CHANGELOG (optional)
|
||||||
if: steps.tagcheck.outputs.exists == 'false'
|
if: steps.tagcheck.outputs.exists == 'false'
|
||||||
id: notes
|
id: notes
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -66,45 +137,68 @@ jobs:
|
|||||||
/^## / && !found {found=1}
|
/^## / && !found {found=1}
|
||||||
found && /^---$/ {exit}
|
found && /^---$/ {exit}
|
||||||
found {print}
|
found {print}
|
||||||
' CHANGELOG.md > RELEASE_BODY.md || true
|
' CHANGELOG.md > CHANGELOG_SNIPPET.md || true
|
||||||
|
sed -i -e :a -e '/^\n*$/{$d;N;ba' -e '}' CHANGELOG_SNIPPET.md || true
|
||||||
# Trim trailing blank lines
|
if [[ -s CHANGELOG_SNIPPET.md ]]; then
|
||||||
sed -i -e :a -e '/^\n*$/{$d;N;ba' -e '}' RELEASE_BODY.md || true
|
NOTES_PATH="CHANGELOG_SNIPPET.md"
|
||||||
|
|
||||||
if [[ -s RELEASE_BODY.md ]]; then
|
|
||||||
NOTES_PATH="RELEASE_BODY.md"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo "path=$NOTES_PATH" >> "$GITHUB_OUTPUT"
|
echo "path=$NOTES_PATH" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: (optional) Build archive to attach
|
- name: Compute previous tag (for Full Changelog link)
|
||||||
|
if: steps.tagcheck.outputs.exists == 'false'
|
||||||
|
id: prev
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VER="${{ steps.ver.outputs.version }}"
|
||||||
|
PREV=$(git tag --list "v*" --sort=-v:refname | grep -v -F "$VER" | head -n1 || true)
|
||||||
|
if [[ -z "$PREV" ]]; then
|
||||||
|
PREV=$(git rev-list --max-parents=0 HEAD | tail -n1)
|
||||||
|
fi
|
||||||
|
echo "prev=$PREV" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Previous tag or baseline: $PREV"
|
||||||
|
|
||||||
|
- name: Build release body (snippet + full changelog + checksum)
|
||||||
if: steps.tagcheck.outputs.exists == 'false'
|
if: steps.tagcheck.outputs.exists == 'false'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
zip -r "FileRise-${{ steps.ver.outputs.version }}.zip" public/ README.md LICENSE >/dev/null || true
|
VER="${{ steps.ver.outputs.version }}"
|
||||||
|
PREV="${{ steps.prev.outputs.prev }}"
|
||||||
|
REPO="${GITHUB_REPOSITORY}"
|
||||||
|
COMPARE_URL="https://github.com/${REPO}/compare/${PREV}...${VER}"
|
||||||
|
ZIP="FileRise-${VER}.zip"
|
||||||
|
SHA="${{ steps.sum.outputs.sha }}"
|
||||||
|
|
||||||
# Path A: we have extracted notes -> use body_path
|
{
|
||||||
- name: Create GitHub Release (with CHANGELOG snippet)
|
echo
|
||||||
if: steps.tagcheck.outputs.exists == 'false' && steps.notes.outputs.path != ''
|
if [[ -s CHANGELOG_SNIPPET.md ]]; then
|
||||||
|
cat CHANGELOG_SNIPPET.md
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
echo "## ${VER}"
|
||||||
|
echo "### Full Changelog"
|
||||||
|
echo "[${PREV} → ${VER}](${COMPARE_URL})"
|
||||||
|
echo
|
||||||
|
echo "### SHA-256 (zip)"
|
||||||
|
echo '```'
|
||||||
|
echo "${SHA} ${ZIP}"
|
||||||
|
echo '```'
|
||||||
|
} > RELEASE_BODY.md
|
||||||
|
|
||||||
|
echo "Release body:"
|
||||||
|
sed -n '1,200p' RELEASE_BODY.md
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
if: steps.tagcheck.outputs.exists == 'false'
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.ver.outputs.version }}
|
tag_name: ${{ steps.ver.outputs.version }}
|
||||||
target_commitish: ${{ github.sha }}
|
target_commitish: ${{ github.sha }}
|
||||||
name: ${{ steps.ver.outputs.version }}
|
name: ${{ steps.ver.outputs.version }}
|
||||||
body_path: ${{ steps.notes.outputs.path }}
|
body_path: RELEASE_BODY.md
|
||||||
generate_release_notes: false
|
generate_release_notes: false
|
||||||
files: |
|
files: |
|
||||||
FileRise-${{ steps.ver.outputs.version }}.zip
|
FileRise-${{ steps.ver.outputs.version }}.zip
|
||||||
|
FileRise-${{ steps.ver.outputs.version }}.zip.sha256
|
||||||
# Path B: no notes -> let GitHub auto-generate from commits
|
|
||||||
- name: Create GitHub Release (auto notes)
|
|
||||||
if: steps.tagcheck.outputs.exists == 'false' && steps.notes.outputs.path == ''
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.ver.outputs.version }}
|
|
||||||
target_commitish: ${{ github.sha }}
|
|
||||||
name: ${{ steps.ver.outputs.version }}
|
|
||||||
generate_release_notes: true
|
|
||||||
files: |
|
|
||||||
FileRise-${{ steps.ver.outputs.version }}.zip
|
|
||||||
|
|||||||
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."
|
echo "No release(vX.Y.Z) tag in commit message; skipping bump."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Update public/js/version.js
|
- name: Update public/js/version.js (source of truth)
|
||||||
if: steps.ver.outputs.version != ''
|
if: steps.ver.outputs.version != ''
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -42,43 +42,20 @@ jobs:
|
|||||||
window.APP_VERSION = '${{ steps.ver.outputs.version }}';
|
window.APP_VERSION = '${{ steps.ver.outputs.version }}';
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Stamp asset cache-busters (?v=...) in HTML/CSS and {{APP_VER}} everywhere
|
# ✂️ REMOVED: repo stamping of HTML/CSS/JS
|
||||||
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}"
|
|
||||||
|
|
||||||
# 1) Only stamp ?v= in HTML/CSS (avoid JS concatenation issues)
|
- name: Commit version.js only
|
||||||
mapfile -t html_css < <(git ls-files -- 'public/*.html' 'public/**/*.html' 'public/*.css' '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
|
|
||||||
if: steps.ver.outputs.version != ''
|
if: steps.ver.outputs.version != ''
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
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
|
if git diff --cached --quiet; then
|
||||||
echo "No changes to commit"
|
echo "No changes to commit"
|
||||||
else
|
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
|
git push
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -110,6 +87,6 @@ jobs:
|
|||||||
if git diff --cached --quiet; then
|
if git diff --cached --quiet; then
|
||||||
echo "No changes to commit"
|
echo "No changes to commit"
|
||||||
else
|
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
|
git push origin main
|
||||||
fi
|
fi
|
||||||
|
|||||||
117
CHANGELOG.md
117
CHANGELOG.md
@@ -1,5 +1,122 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
### ✨ 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`.
|
||||||
|
|
||||||
|
### Additional changes/fixes for release
|
||||||
|
|
||||||
|
- `release-on-version.yml`
|
||||||
|
- normalize line endings (strip CRLF)
|
||||||
|
- stamp-assets.sh don’t rely on the exec; invoke via bash
|
||||||
|
|
||||||
|
release(v1.7.2): harden asset stamping & CI verification
|
||||||
|
|
||||||
|
### build(stamper)
|
||||||
|
|
||||||
|
- Rewrite scripts/stamp-assets.sh to be repo-agnostic and macOS/Windows friendly:
|
||||||
|
- Drop reliance on git ls-files/mapfile; use find + null-delimited loops
|
||||||
|
- Normalize CRLF to LF for all web assets before stamping
|
||||||
|
- Stamp ?v=<APP_QVER> in HTML/CSS/PHP and {{APP_VER}} everywhere
|
||||||
|
- Normalize any ".mjs|.js?v=..." occurrences inside JS (ESM imports/strings)
|
||||||
|
- Force-write public/js/version.js from VER (source of truth in stamped output)
|
||||||
|
- Print touched counts and fail fast if any {{APP_QVER}}|{{APP_VER}} remain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 10/28/2025 (v1.6.11)
|
||||||
|
|
||||||
|
release(v1.6.11) fix(ui/dragAndDrop) restore floating zones toggle click action
|
||||||
|
|
||||||
|
Re-add the click handler to toggle `zonesCollapsed` so the header
|
||||||
|
“sidebarToggleFloating” button actually expands/collapses the zones
|
||||||
|
again. This regressed in v1.6.10 during auth-gating refactor.
|
||||||
|
|
||||||
|
Refs: #regression #ux
|
||||||
|
|
||||||
|
chore(codeql): move config to repo root for default setup
|
||||||
|
|
||||||
|
- Relocate .github/codeql/codeql-config.yml to codeql-config.yml so GitHub default code scanning picks it up
|
||||||
|
- Keep paths: public/js, api
|
||||||
|
- Keep ignores: public/vendor/**, public/css/vendor/**, public/fonts/**, public/**/*.min.{js,css}, public/**/*.map
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 10/28/2025 (v1.6.10)
|
||||||
|
|
||||||
|
release(v1.6.10): self-host ReDoc, gate sidebar toggle on auth, and enrich release workflow
|
||||||
|
|
||||||
|
- Vendor ReDoc and add MIT license file under public/vendor/redoc/; switch api.php to local bundle to satisfy CSP (script-src 'self').
|
||||||
|
- main.js: add/remove body.authenticated on login/logout so UI can reflect auth state.
|
||||||
|
- dragAndDrop.js: only render sidebarToggleFloating when authenticated; stop event bubbling, keep dark-mode styles.
|
||||||
|
- sync-changelog.yml: also stamp ?v= in PHP templates (public/**/*.php).
|
||||||
|
- release-on-version.yml: build zip first, compute SHA-256, assemble release body with latest CHANGELOG snippet, “Full Changelog” compare link, and attach .sha256 alongside the zip.
|
||||||
|
- THIRD_PARTY.md: document ReDoc vendoring and rationale.
|
||||||
|
|
||||||
|
Refs: #security #csp #release
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 10/27/2025 (v1.6.9)
|
## Changes 10/27/2025 (v1.6.9)
|
||||||
|
|
||||||
release(v1.6.9): feat(core) localize assets, harden headers, and speed up load
|
release(v1.6.9): feat(core) localize assets, harden headers, and speed up load
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ docker run -d \
|
|||||||
-e DATE_TIME_FORMAT="m/d/y h:iA" \
|
-e DATE_TIME_FORMAT="m/d/y h:iA" \
|
||||||
-e TOTAL_UPLOAD_SIZE="5G" \
|
-e TOTAL_UPLOAD_SIZE="5G" \
|
||||||
-e SECURE="false" \
|
-e SECURE="false" \
|
||||||
-e PERSISTENT_TOKENS_KEY="please_change_this_@@" \
|
-e PERSISTENT_TOKENS_KEY="default_please_change_this_key" \
|
||||||
-e PUID="1000" \
|
-e PUID="1000" \
|
||||||
-e PGID="1000" \
|
-e PGID="1000" \
|
||||||
-e CHOWN_ON_START="true" \
|
-e CHOWN_ON_START="true" \
|
||||||
@@ -186,7 +186,7 @@ services:
|
|||||||
DATE_TIME_FORMAT: "m/d/y h:iA"
|
DATE_TIME_FORMAT: "m/d/y h:iA"
|
||||||
TOTAL_UPLOAD_SIZE: "10G"
|
TOTAL_UPLOAD_SIZE: "10G"
|
||||||
SECURE: "false"
|
SECURE: "false"
|
||||||
PERSISTENT_TOKENS_KEY: "please_change_this_@@"
|
PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
|
||||||
# Ownership & indexing
|
# Ownership & indexing
|
||||||
PUID: "1000" # Unraid users often use 99
|
PUID: "1000" # Unraid users often use 99
|
||||||
PGID: "1000" # Unraid users often use 100
|
PGID: "1000" # Unraid users often use 100
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ If you believe any attribution is missing or incorrect, please open an issue.
|
|||||||
- **Resumable.js 1.1.0** — MIT License
|
- **Resumable.js 1.1.0** — MIT License
|
||||||
**Files:** `public/vendor/resumable/1.1.0/resumable.min.js`
|
**Files:** `public/vendor/resumable/1.1.0/resumable.min.js`
|
||||||
|
|
||||||
|
- **ReDoc (redoc.standalone.js)** — MIT License
|
||||||
|
**Files:** `public/vendor/redoc/redoc.standalone.js`
|
||||||
|
**Notes:** Self-hosted to comply with `script-src 'self'` CSP.
|
||||||
|
|
||||||
> MIT-licensed code: see `licenses/mit.txt`.
|
> MIT-licensed code: see `licenses/mit.txt`.
|
||||||
> Apache-2.0–licensed code: see `licenses/apache-2.0.txt`.
|
> Apache-2.0–licensed code: see `licenses/apache-2.0.txt`.
|
||||||
|
|
||||||
12
codeql-config.yml
Normal file
12
codeql-config.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: FileRise CodeQL config
|
||||||
|
paths:
|
||||||
|
- public/js
|
||||||
|
- api
|
||||||
|
paths-ignore:
|
||||||
|
- public/vendor/**
|
||||||
|
- public/css/vendor/**
|
||||||
|
- public/fonts/**
|
||||||
|
- public/**/*.min.js
|
||||||
|
- public/**/*.min.css
|
||||||
|
- public/**/*.map
|
||||||
@@ -11,14 +11,16 @@ DirectoryIndex index.html
|
|||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
|
# If you want forced HTTPS behind a proxy, keep this off here and do it at the proxy
|
||||||
#RewriteCond %{HTTPS} off
|
#RewriteCond %{HTTPS} off
|
||||||
#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||||
|
|
||||||
# MIME types for fonts/SVG
|
# MIME types (fonts/SVG/ESM)
|
||||||
<IfModule mod_mime.c>
|
<IfModule mod_mime.c>
|
||||||
AddType font/woff2 .woff2
|
AddType font/woff2 .woff2
|
||||||
AddType font/woff .woff
|
AddType font/woff .woff
|
||||||
AddType image/svg+xml .svg
|
AddType image/svg+xml .svg
|
||||||
|
AddType application/javascript .mjs
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
@@ -26,43 +28,53 @@ RewriteEngine On
|
|||||||
Header always set X-Frame-Options "SAMEORIGIN"
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
Header always set X-XSS-Protection "1; mode=block"
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
Header always set X-Content-Type-Options "nosniff"
|
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 Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||||
Header always set X-Download-Options "noopen"
|
Header always set X-Download-Options "noopen"
|
||||||
Header always set Expect-CT "max-age=86400, enforce"
|
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'"
|
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>
|
</IfModule>
|
||||||
|
|
||||||
# Caching
|
# Caching
|
||||||
SetEnvIfNoCase QUERY_STRING "(^|&)v=" has_version_param=1
|
SetEnvIfNoCase QUERY_STRING "(^|&)v=" has_version_param=1
|
||||||
<IfModule mod_headers.c>
|
<IfModule mod_headers.c>
|
||||||
|
# HTML/PHP: no cache (app shell)
|
||||||
<FilesMatch "\.(html?|php)$">
|
<FilesMatch "\.(html?|php)$">
|
||||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
Header set Pragma "no-cache"
|
Header set Pragma "no-cache"
|
||||||
Header set Expires "0"
|
Header set Expires "0"
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
|
# version.js is your source-of-truth; keep it non-cacheable so dev/CI flips show up
|
||||||
<FilesMatch "^js/version\.js$">
|
<FilesMatch "^js/version\.js$">
|
||||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
Header set Pragma "no-cache"
|
Header set Pragma "no-cache"
|
||||||
Header set Expires "0"
|
Header set Expires "0"
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
<FilesMatch "\.(js|css)$">
|
# Unversioned JS/CSS (dev): 1 hour
|
||||||
Header set Cache-Control "public, max-age=3600, must-revalidate" env=!has_version_param
|
<FilesMatch "\.(?:m?js|css)$">
|
||||||
</FilesMatch>
|
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)$">
|
# Unversioned static assets (dev): 7 days
|
||||||
Header set Cache-Control "public, max-age=604800" env=!has_version_param
|
<FilesMatch "\.(?:png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$">
|
||||||
</FilesMatch>
|
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)$">
|
# Versioned assets (?v=...): 1 year + immutable
|
||||||
Header set Cache-Control "public, max-age=31536000, immutable" env=has_version_param
|
<FilesMatch "\.(?:m?js|css|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$">
|
||||||
</FilesMatch>
|
Header set Cache-Control "public, max-age=31536000, immutable" env=has_version_param
|
||||||
</IfModule>
|
</FilesMatch>
|
||||||
|
|
||||||
# Compression (only if module exists)
|
# Compression (if modules exist)
|
||||||
<IfModule mod_brotli.c>
|
<IfModule mod_brotli.c>
|
||||||
BrotliCompressionQuality 5
|
BrotliCompressionQuality 5
|
||||||
AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json image/svg+xml
|
AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json image/svg+xml
|
||||||
|
|||||||
@@ -19,13 +19,11 @@ if (isset($_GET['spec'])) {
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<title>FileRise API Docs</title>
|
<title>FileRise API Docs</title>
|
||||||
<script defer src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"
|
<script defer src="/vendor/redoc/redoc.standalone.js?v={{APP_QVER}}"></script>
|
||||||
integrity="sha384-70P5pmIdaQdVbxvjhrcTDv1uKcKqalZ3OHi7S2J+uzDl0PW8dO6L+pHOpm9EEjGJ"
|
<script defer src="/js/redoc-init.js?v={{APP_QVER}}"></script>
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<script defer src="/js/redoc-init.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<redoc spec-url="api.php?spec=1"></redoc>
|
<redoc spec-url="/api.php?spec=1"></redoc>
|
||||||
<div id="redoc-container"></div>
|
<div id="redoc-container"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
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-left: 30px !important;
|
||||||
padding-right: 30px !important;
|
padding-right: 30px !important;
|
||||||
}}
|
}}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.zones-toggle { left: 85px !important; }
|
||||||
|
}
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
HEADER & NAVIGATION
|
HEADER & NAVIGATION
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
@@ -35,7 +37,7 @@ body {
|
|||||||
/************************************************************/
|
/************************************************************/
|
||||||
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
|
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
|
||||||
/************************************************************/
|
/************************************************************/
|
||||||
|
.header-logo .logo { height: 50px; width: auto; display: block; }
|
||||||
.btn-login {
|
.btn-login {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}/* Color overrides */
|
}/* Color overrides */
|
||||||
|
|||||||
Binary file not shown.
@@ -11,29 +11,31 @@
|
|||||||
<meta name="share-url" content="">
|
<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>
|
<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=dev">
|
<link rel="stylesheet" href="/css/vendor/roboto.css?v={{APP_QVER}}">
|
||||||
<link rel="stylesheet" href="/css/vendor/material-icons.css?v=dev">
|
<link rel="stylesheet" href="/css/vendor/material-icons.css?v={{APP_QVER}}">
|
||||||
|
|
||||||
<!-- Bootstrap CSS (local) -->
|
<!-- Bootstrap CSS (local) -->
|
||||||
<link rel="stylesheet" href="/vendor/bootstrap/4.5.2/bootstrap.min.css?v=dev">
|
<link rel="stylesheet" href="/vendor/bootstrap/4.5.2/bootstrap.min.css?v={{APP_QVER}}">
|
||||||
|
|
||||||
<!-- CodeMirror CSS (local) -->
|
<!-- CodeMirror CSS (local) -->
|
||||||
<link rel="stylesheet" href="/vendor/codemirror/5.65.5/codemirror.min.css?v=dev">
|
<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=dev">
|
<link rel="stylesheet" href="/vendor/codemirror/5.65.5/theme/material-darker.min.css?v={{APP_QVER}}">
|
||||||
|
|
||||||
<!-- app CSS -->
|
<!-- app CSS -->
|
||||||
<link rel="stylesheet" href="/css/styles.css?v=dev">
|
<link rel="stylesheet" href="/css/styles.css?v={{APP_QVER}}">
|
||||||
|
|
||||||
<!-- Libraries (JS) -->
|
<!-- Libraries (JS) -->
|
||||||
<script src="/vendor/dompurify/2.4.0/purify.min.js?v=dev"></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=dev"></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=dev"></script>
|
<script src="/vendor/resumable/1.1.0/resumable.min.js?v={{APP_QVER}}"></script>
|
||||||
|
|
||||||
<!-- CodeMirror core FIRST, then modes -->
|
<!-- CodeMirror core FIRST -->
|
||||||
<script src="/vendor/codemirror/5.65.5/codemirror.min.js?v=dev"></script>
|
<script src="/vendor/codemirror/5.65.5/codemirror.min.js?v={{APP_QVER}}"></script>
|
||||||
|
|
||||||
<script src="/js/version.js?v=dev"></script>
|
<script src="/js/version.js?v={{APP_QVER}}"></script>
|
||||||
<script type="module" src="/js/main.js"></script>
|
|
||||||
|
<link rel="modulepreload" href="/js/main.js?v={{APP_QVER}}">
|
||||||
|
<script type="module" src="/js/main.js?v={{APP_QVER}}"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -42,67 +44,7 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<a href="index.html">
|
<a href="index.html">
|
||||||
<div class="header-logo">
|
<div class="header-logo">
|
||||||
<svg version="1.1" id="filingCabinetLogo" xmlns="http://www.w3.org/2000/svg"
|
<img src="/assets/logo.svg?v={{APP_QVER}}" alt="FileRise" class="logo" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// adminPanel.js
|
// adminPanel.js
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
import { loadAdminConfigFunc } from './auth.js';
|
import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { sendRequest } from './networkUtils.js';
|
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
const version = window.APP_VERSION || "dev";
|
const version = window.APP_VERSION || "dev";
|
||||||
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||||
|
|||||||
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 { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||||
import { t, applyTranslations } from './i18n.js';
|
import { t, applyTranslations } from './i18n.js?v={{APP_QVER}}';
|
||||||
import {
|
import {
|
||||||
toggleVisibility,
|
toggleVisibility,
|
||||||
showToast as originalShowToast,
|
showToast as originalShowToast,
|
||||||
attachEnterKeyListener,
|
attachEnterKeyListener,
|
||||||
showCustomConfirmModal
|
showCustomConfirmModal
|
||||||
} from './domUtils.js';
|
} from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { loadFileList } from './fileListView.js';
|
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { initFileActions } from './fileActions.js';
|
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||||
import { renderFileTable } from './fileListView.js';
|
import { renderFileTable } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { loadFolderTree } from './folderManager.js';
|
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||||
import {
|
import {
|
||||||
openTOTPLoginModal as originalOpenTOTPLoginModal,
|
openTOTPLoginModal as originalOpenTOTPLoginModal,
|
||||||
openUserPanel,
|
openUserPanel,
|
||||||
@@ -17,9 +17,9 @@ import {
|
|||||||
closeTOTPModal,
|
closeTOTPModal,
|
||||||
setLastLoginData,
|
setLastLoginData,
|
||||||
openApiModal
|
openApiModal
|
||||||
} from './authModals.js';
|
} from './authModals.js?v={{APP_QVER}}';
|
||||||
import { openAdminPanel } from './adminPanel.js';
|
import { openAdminPanel } from './adminPanel.js?v={{APP_QVER}}';
|
||||||
import { initializeApp, triggerLogout } from './main.js';
|
import { initializeApp, triggerLogout } from './appCore.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
// Production OIDC configuration (override via API as needed)
|
// Production OIDC configuration (override via API as needed)
|
||||||
const currentOIDCConfig = {
|
const currentOIDCConfig = {
|
||||||
@@ -180,7 +180,7 @@ function updateLoginOptionsUIFromStorage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function loadAdminConfigFunc() {
|
export function loadAdminConfigFunc() {
|
||||||
return fetch("/api/admin/getConfig.php", { credentials: "include" })
|
return fetch("/api/siteConfig.php", { credentials: "include" })
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
// If a proxy or some edge returns 204/empty, handle gracefully
|
// If a proxy or some edge returns 204/empty, handle gracefully
|
||||||
let config = {};
|
let config = {};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { sendRequest } from './networkUtils.js';
|
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
import { t, applyTranslations, setLocale } from './i18n.js?v={{APP_QVER}}';
|
||||||
import { loadAdminConfigFunc, updateAuthenticatedUI } from './auth.js';
|
import { loadAdminConfigFunc, updateAuthenticatedUI } from './auth.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
let lastLoginData = null;
|
let lastLoginData = null;
|
||||||
export function setLastLoginData(data) {
|
export function setLastLoginData(data) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// domUtils.js
|
// domUtils.js
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
import { openDownloadModal } from './fileActions.js';
|
import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
// Basic DOM Helpers
|
// Basic DOM Helpers
|
||||||
export function toggleVisibility(elementId, shouldShow) {
|
export function toggleVisibility(elementId, shouldShow) {
|
||||||
|
|||||||
@@ -489,6 +489,7 @@ function mountHeaderToggle(btn) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function ensureZonesToggle() {
|
function ensureZonesToggle() {
|
||||||
let btn = document.getElementById('sidebarToggleFloating');
|
let btn = document.getElementById('sidebarToggleFloating');
|
||||||
const host = getHeaderHost();
|
const host = getHeaderHost();
|
||||||
@@ -502,24 +503,25 @@ function ensureZonesToggle() {
|
|||||||
|
|
||||||
if (!btn) {
|
if (!btn) {
|
||||||
btn = document.createElement('button');
|
btn = document.createElement('button');
|
||||||
|
|
||||||
btn.id = 'sidebarToggleFloating';
|
btn.id = 'sidebarToggleFloating';
|
||||||
btn.type = 'button'; // not a submit
|
btn.type = 'button';
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation(); // don't bubble into the <a href="index.html">
|
|
||||||
setSidebarCollapsed(!isSidebarCollapsed());
|
|
||||||
updateSidebarToggleUI(); // refresh icon/title
|
|
||||||
});
|
|
||||||
['mousedown','mouseup','pointerdown','pointerup'].forEach(evt =>
|
|
||||||
btn.addEventListener(evt, (e) => e.stopPropagation())
|
|
||||||
);
|
|
||||||
btn.setAttribute('aria-label', 'Toggle panels');
|
btn.setAttribute('aria-label', 'Toggle panels');
|
||||||
|
|
||||||
|
// Prevent accidental navigations / bubbling
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setSidebarCollapsed(!isSidebarCollapsed());
|
||||||
|
updateSidebarToggleUI();
|
||||||
|
});
|
||||||
|
['mousedown','mouseup','pointerdown','pointerup'].forEach(evt =>
|
||||||
|
btn.addEventListener(evt, (e) => e.stopPropagation())
|
||||||
|
);
|
||||||
|
|
||||||
Object.assign(btn.style, {
|
Object.assign(btn.style, {
|
||||||
position: 'absolute', // <-- key change (was fixed)
|
position: 'absolute',
|
||||||
top: '8px', // adjust to line up with header content
|
top: '8px',
|
||||||
left: '65px', // place to the right of your logo; tweak as needed
|
left: '65px',
|
||||||
zIndex: '1000',
|
zIndex: '1000',
|
||||||
width: '38px',
|
width: '38px',
|
||||||
height: '38px',
|
height: '38px',
|
||||||
@@ -534,8 +536,9 @@ btn.addEventListener('click', (e) => {
|
|||||||
padding: '0',
|
padding: '0',
|
||||||
lineHeight: '0'
|
lineHeight: '0'
|
||||||
});
|
});
|
||||||
|
btn.classList.add('zones-toggle');
|
||||||
|
|
||||||
// dark-mode polish (optional)
|
// Dark mode polish
|
||||||
if (document.body.classList.contains('dark-mode')) {
|
if (document.body.classList.contains('dark-mode')) {
|
||||||
btn.style.background = '#2c2c2c';
|
btn.style.background = '#2c2c2c';
|
||||||
btn.style.border = '1px solid #555';
|
btn.style.border = '1px solid #555';
|
||||||
@@ -547,13 +550,14 @@ btn.addEventListener('click', (e) => {
|
|||||||
setZonesCollapsed(!isZonesCollapsed());
|
setZonesCollapsed(!isZonesCollapsed());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insert right after the logo if present, else just append to host
|
// Insert right after the logo if present, else append to host
|
||||||
const afterLogo = host.querySelector('.header-logo');
|
const afterLogo = host.querySelector('.header-logo');
|
||||||
if (afterLogo && afterLogo.parentNode) {
|
if (afterLogo && afterLogo.parentNode) {
|
||||||
afterLogo.parentNode.insertBefore(btn, afterLogo.nextSibling);
|
afterLogo.parentNode.insertBefore(btn, afterLogo.nextSibling);
|
||||||
} else {
|
} else {
|
||||||
host.appendChild(btn);
|
host.appendChild(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
themeToggleButton(btn);
|
themeToggleButton(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// fileActions.js
|
// fileActions.js
|
||||||
import { showToast, attachEnterKeyListener } from './domUtils.js';
|
import { showToast, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { loadFileList } from './fileListView.js';
|
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { formatFolderName } from './fileListView.js';
|
import { formatFolderName } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
export function handleDeleteSelected(e) {
|
export function handleDeleteSelected(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// fileDragDrop.js
|
// fileDragDrop.js
|
||||||
import { showToast } from './domUtils.js';
|
import { showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { loadFileList } from './fileListView.js';
|
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
export function fileDragStartHandler(event) {
|
export function fileDragStartHandler(event) {
|
||||||
const row = event.currentTarget;
|
const row = event.currentTarget;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// fileEditor.js
|
// fileEditor.js
|
||||||
import { escapeHTML, showToast } from './domUtils.js';
|
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { loadFileList } from './fileListView.js';
|
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
// thresholds for editor behavior
|
// thresholds for editor behavior
|
||||||
const EDITOR_PLAIN_THRESHOLD = 5 * 1024 * 1024; // >5 MiB => force plain text, lighter settings
|
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
|
// Which mode file to load for a given name/mime
|
||||||
const MODE_URL = {
|
const MODE_URL = {
|
||||||
// core/common
|
// core/common
|
||||||
"xml": "mode/xml/xml.min.js",
|
"xml": "mode/xml/xml.min.js?v={{APP_QVER}}",
|
||||||
"css": "mode/css/css.min.js",
|
"css": "mode/css/css.min.js?v={{APP_QVER}}",
|
||||||
"javascript": "mode/javascript/javascript.min.js",
|
"javascript": "mode/javascript/javascript.min.js?v={{APP_QVER}}",
|
||||||
|
|
||||||
// meta / combos
|
// meta / combos
|
||||||
"htmlmixed": "mode/htmlmixed/htmlmixed.min.js",
|
"htmlmixed": "mode/htmlmixed/htmlmixed.min.js?v={{APP_QVER}}",
|
||||||
"application/x-httpd-php": "mode/php/php.min.js",
|
"application/x-httpd-php": "mode/php/php.min.js?v={{APP_QVER}}",
|
||||||
|
|
||||||
// docs / data
|
// docs / data
|
||||||
"markdown": "mode/markdown/markdown.min.js",
|
"markdown": "mode/markdown/markdown.min.js?v={{APP_QVER}}",
|
||||||
"yaml": "mode/yaml/yaml.min.js",
|
"yaml": "mode/yaml/yaml.min.js?v={{APP_QVER}}",
|
||||||
"properties": "mode/properties/properties.min.js",
|
"properties": "mode/properties/properties.min.js?v={{APP_QVER}}",
|
||||||
"sql": "mode/sql/sql.min.js",
|
"sql": "mode/sql/sql.min.js?v={{APP_QVER}}",
|
||||||
|
|
||||||
// shells
|
// shells
|
||||||
"shell": "mode/shell/shell.min.js",
|
"shell": "mode/shell/shell.min.js?v={{APP_QVER}}",
|
||||||
|
|
||||||
// languages
|
// languages
|
||||||
"python": "mode/python/python.min.js",
|
"python": "mode/python/python.min.js?v={{APP_QVER}}",
|
||||||
"text/x-csrc": "mode/clike/clike.min.js",
|
"text/x-csrc": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||||
"text/x-c++src": "mode/clike/clike.min.js",
|
"text/x-c++src": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||||
"text/x-java": "mode/clike/clike.min.js",
|
"text/x-java": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||||
"text/x-csharp": "mode/clike/clike.min.js",
|
"text/x-csharp": "mode/clike/clike.min.js?v={{APP_QVER}}",
|
||||||
"text/x-kotlin": "mode/clike/clike.min.js"
|
"text/x-kotlin": "mode/clike/clike.min.js?v={{APP_QVER}}"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map any mime/alias to the key we use in MODE_URL
|
// 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) {
|
function loadScriptOnce(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ver = (window.APP_VERSION ?? 'dev').replace(/^v/, ''); // "v1.6.9" -> "1.6.9"
|
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}`;
|
const key = `cm:${withQS}`;
|
||||||
let s = document.querySelector(`script[data-key="${key}"]`);
|
let s = document.querySelector(`script[data-key="${key}"]`);
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import {
|
|||||||
updateRowHighlight,
|
updateRowHighlight,
|
||||||
toggleRowSelection,
|
toggleRowSelection,
|
||||||
attachEnterKeyListener
|
attachEnterKeyListener
|
||||||
} from './domUtils.js';
|
} from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
import { bindFileListContextMenu } from './fileMenu.js';
|
import { bindFileListContextMenu } from './fileMenu.js?v={{APP_QVER}}';
|
||||||
import { openDownloadModal } from './fileActions.js';
|
import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}';
|
||||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
|
||||||
import {
|
import {
|
||||||
getParentFolder,
|
getParentFolder,
|
||||||
updateBreadcrumbTitle,
|
updateBreadcrumbTitle,
|
||||||
@@ -24,13 +24,13 @@ import {
|
|||||||
hideFolderManagerContextMenu,
|
hideFolderManagerContextMenu,
|
||||||
openRenameFolderModal,
|
openRenameFolderModal,
|
||||||
openDeleteFolderModal
|
openDeleteFolderModal
|
||||||
} from './folderManager.js';
|
} from './folderManager.js?v={{APP_QVER}}';
|
||||||
import { openFolderShareModal } from './folderShareModal.js';
|
import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}';
|
||||||
import {
|
import {
|
||||||
folderDragOverHandler,
|
folderDragOverHandler,
|
||||||
folderDragLeaveHandler,
|
folderDragLeaveHandler,
|
||||||
folderDropHandler
|
folderDropHandler
|
||||||
} from './fileDragDrop.js';
|
} from './fileDragDrop.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
export let fileData = [];
|
export let fileData = [];
|
||||||
export let sortOrder = { column: "uploaded", ascending: true };
|
export let sortOrder = { column: "uploaded", ascending: true };
|
||||||
@@ -750,7 +750,7 @@ function wireSelectAll(fileListContent) {
|
|||||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||||
btn.addEventListener("click", async e => {
|
btn.addEventListener("click", async e => {
|
||||||
e.stopPropagation();
|
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);
|
m.editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -759,7 +759,7 @@ function wireSelectAll(fileListContent) {
|
|||||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||||
btn.addEventListener("click", async e => {
|
btn.addEventListener("click", async e => {
|
||||||
e.stopPropagation();
|
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);
|
m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -768,7 +768,7 @@ function wireSelectAll(fileListContent) {
|
|||||||
fileListContent.querySelectorAll(".preview-btn").forEach(btn => {
|
fileListContent.querySelectorAll(".preview-btn").forEach(btn => {
|
||||||
btn.addEventListener("click", async e => {
|
btn.addEventListener("click", async e => {
|
||||||
e.stopPropagation();
|
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);
|
m.previewFile(btn.dataset.previewUrl, btn.dataset.previewName);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -822,7 +822,7 @@ function wireSelectAll(fileListContent) {
|
|||||||
const fileName = this.getAttribute("data-file");
|
const fileName = this.getAttribute("data-file");
|
||||||
const file = fileData.find(f => f.name === fileName);
|
const file = fileData.find(f => f.name === fileName);
|
||||||
if (file) {
|
if (file) {
|
||||||
import('./filePreview.js').then(module => {
|
import('./filePreview.js?v={{APP_QVER}}').then(module => {
|
||||||
module.openShareModal(file, folder);
|
module.openShareModal(file, folder);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -831,7 +831,7 @@ function wireSelectAll(fileListContent) {
|
|||||||
updateFileActionButtons();
|
updateFileActionButtons();
|
||||||
document.querySelectorAll("#fileList tbody tr").forEach(row => {
|
document.querySelectorAll("#fileList tbody tr").forEach(row => {
|
||||||
row.setAttribute("draggable", "true");
|
row.setAttribute("draggable", "true");
|
||||||
import('./fileDragDrop.js').then(module => {
|
import('./fileDragDrop.js?v={{APP_QVER}}').then(module => {
|
||||||
row.addEventListener("dragstart", module.fileDragStartHandler);
|
row.addEventListener("dragstart", module.fileDragStartHandler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1085,7 +1085,7 @@ function wireSelectAll(fileListContent) {
|
|||||||
// preview clicks (dynamic import to avoid global dependency)
|
// preview clicks (dynamic import to avoid global dependency)
|
||||||
fileListContent.querySelectorAll(".gallery-preview").forEach(el => {
|
fileListContent.querySelectorAll(".gallery-preview").forEach(el => {
|
||||||
el.addEventListener("click", async () => {
|
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);
|
m.previewFile(el.dataset.previewUrl, el.dataset.previewName);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1102,7 +1102,7 @@ function wireSelectAll(fileListContent) {
|
|||||||
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
fileListContent.querySelectorAll(".edit-btn").forEach(btn => {
|
||||||
btn.addEventListener("click", async e => {
|
btn.addEventListener("click", async e => {
|
||||||
e.stopPropagation();
|
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);
|
m.editFile(btn.dataset.editName, btn.dataset.editFolder);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1111,7 +1111,7 @@ function wireSelectAll(fileListContent) {
|
|||||||
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
fileListContent.querySelectorAll(".rename-btn").forEach(btn => {
|
||||||
btn.addEventListener("click", async e => {
|
btn.addEventListener("click", async e => {
|
||||||
e.stopPropagation();
|
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);
|
m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1123,7 +1123,7 @@ function wireSelectAll(fileListContent) {
|
|||||||
const fileName = btn.dataset.file;
|
const fileName = btn.dataset.file;
|
||||||
const fileObj = fileData.find(f => f.name === fileName);
|
const fileObj = fileData.find(f => f.name === fileName);
|
||||||
if (fileObj) {
|
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
|
// fileManager.js
|
||||||
import './fileListView.js';
|
import './fileListView.js?v={{APP_QVER}}';
|
||||||
import './filePreview.js';
|
import './filePreview.js?v={{APP_QVER}}';
|
||||||
import './fileEditor.js';
|
import './fileEditor.js?v={{APP_QVER}}';
|
||||||
import './fileDragDrop.js';
|
import './fileDragDrop.js?v={{APP_QVER}}';
|
||||||
import './fileMenu.js';
|
import './fileMenu.js?v={{APP_QVER}}';
|
||||||
import { initFileActions } from './fileActions.js';
|
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
// Initialize file action buttons.
|
// Initialize file action buttons.
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
@@ -14,7 +14,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
// Attach folder drag-and-drop support for folder tree nodes.
|
// Attach folder drag-and-drop support for folder tree nodes.
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
document.querySelectorAll(".folder-option").forEach(el => {
|
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("dragover", module.folderDragOverHandler);
|
||||||
el.addEventListener("dragleave", module.folderDragLeaveHandler);
|
el.addEventListener("dragleave", module.folderDragLeaveHandler);
|
||||||
el.addEventListener("drop", module.folderDropHandler);
|
el.addEventListener("drop", module.folderDropHandler);
|
||||||
@@ -32,7 +32,7 @@ document.addEventListener("keydown", function(e) {
|
|||||||
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
||||||
if (selectedCheckboxes.length > 0) {
|
if (selectedCheckboxes.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
import('./fileActions.js').then(module => {
|
import('./fileActions.js?v={{APP_QVER}}').then(module => {
|
||||||
module.handleDeleteSelected(new Event("click"));
|
module.handleDeleteSelected(new Event("click"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// fileMenu.js
|
// fileMenu.js
|
||||||
import { updateRowHighlight, showToast } from './domUtils.js';
|
import { updateRowHighlight, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js';
|
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js?v={{APP_QVER}}';
|
||||||
import { previewFile } from './filePreview.js';
|
import { previewFile } from './filePreview.js?v={{APP_QVER}}';
|
||||||
import { editFile } from './fileEditor.js';
|
import { editFile } from './fileEditor.js?v={{APP_QVER}}';
|
||||||
import { canEditFile, fileData } from './fileListView.js';
|
import { canEditFile, fileData } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
export function showFileContextMenu(x, y, menuItems) {
|
export function showFileContextMenu(x, y, menuItems) {
|
||||||
let menu = document.getElementById("fileContextMenu");
|
let menu = document.getElementById("fileContextMenu");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// filePreview.js
|
// filePreview.js
|
||||||
import { escapeHTML, showToast } from './domUtils.js';
|
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { fileData } from './fileListView.js';
|
import { fileData } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
export function openShareModal(file, folder) {
|
export function openShareModal(file, folder) {
|
||||||
// Remove any existing modal
|
// Remove any existing modal
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
// adding tags to files (with a global tag store for reuse),
|
// adding tags to files (with a global tag store for reuse),
|
||||||
// updating the file row display with tag badges,
|
// updating the file row display with tag badges,
|
||||||
// filtering the file list by tag, and persisting tag data.
|
// filtering the file list by tag, and persisting tag data.
|
||||||
import { escapeHTML } from './domUtils.js';
|
import { escapeHTML } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
import { renderFileTable, renderGalleryView } from './fileListView.js';
|
import { renderFileTable, renderGalleryView } from './fileListView.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
export function openTagModal(file) {
|
export function openTagModal(file) {
|
||||||
// Create the modal element.
|
// Create the modal element.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// folderManager.js
|
// folderManager.js
|
||||||
|
|
||||||
import { loadFileList } from './fileListView.js';
|
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
import { openFolderShareModal } from './folderShareModal.js';
|
import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}';
|
||||||
import { fetchWithCsrf } from './auth.js';
|
import { fetchWithCsrf } from './auth.js?v={{APP_QVER}}';
|
||||||
import { loadCsrfToken } from './main.js';
|
import { loadCsrfToken } from './appCore.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
/* ----------------------
|
/* ----------------------
|
||||||
Helpers: safe JSON + state
|
Helpers: safe JSON + state
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// js/folderShareModal.js
|
// js/folderShareModal.js
|
||||||
import { escapeHTML, showToast } from './domUtils.js';
|
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
export function openFolderShareModal(folder) {
|
export function openFolderShareModal(folder) {
|
||||||
// Remove any existing modal
|
// Remove any existing modal
|
||||||
|
|||||||
@@ -1,55 +1,65 @@
|
|||||||
import { sendRequest } from './networkUtils.js';
|
// /js/main.js
|
||||||
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js';
|
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||||
import { initUpload } from './upload.js';
|
import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { initAuth, fetchWithCsrf, checkAuthentication, loadAdminConfigFunc } from './auth.js';
|
import { initUpload } from './upload.js?v={{APP_QVER}}';
|
||||||
import { loadFolderTree } from './folderManager.js';
|
import { initAuth, fetchWithCsrf, checkAuthentication, loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
import { setupTrashRestoreDelete } from './trashRestoreDelete.js?v={{APP_QVER}}';
|
||||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js?v={{APP_QVER}}';
|
||||||
import { displayFilePreview } from './filePreview.js';
|
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js?v={{APP_QVER}}';
|
||||||
import { loadFileList } from './fileListView.js';
|
import { displayFilePreview } from './filePreview.js?v={{APP_QVER}}';
|
||||||
import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload } from './fileActions.js';
|
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { editFile, saveFile } from './fileEditor.js';
|
import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload } from './fileActions.js?v={{APP_QVER}}';
|
||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
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
|
CSRF HOTFIX UTILITIES
|
||||||
========================= */
|
========================= */
|
||||||
const _nativeFetch = window.fetch; // keep the real fetch
|
// Keep a handle to the native fetch so wrappers never recurse
|
||||||
|
const _nativeFetch = window.fetch.bind(window);
|
||||||
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') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed CSRF from storage ASAP (before any requests)
|
// Seed CSRF from storage ASAP (before any requests)
|
||||||
setCsrfToken(getCsrfToken());
|
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 = {}) {
|
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 {
|
try {
|
||||||
const rotated = res.headers?.get('X-CSRF-Token');
|
const rotated = res.headers?.get('X-CSRF-Token');
|
||||||
if (rotated) setCsrfToken(rotated);
|
if (rotated) setCsrfToken(rotated);
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace global fetch with the wrapped version so *all* callers benefit.
|
// Avoid double-wrapping if this module re-evaluates for any reason
|
||||||
window.fetch = fetchWithCsrfAndRefresh;
|
if (!window.fetch || !window.fetch._frWrapped) {
|
||||||
|
const wrapped = fetchWithCsrfAndRefresh;
|
||||||
|
Object.defineProperty(wrapped, '_frWrapped', { value: true });
|
||||||
|
window.fetch = wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
SAFE API HELPERS
|
SAFE API HELPERS
|
||||||
@@ -84,6 +94,7 @@ export async function apiPOSTJSON(url, body, opts = {}) {
|
|||||||
// Optional: expose on window for legacy callers
|
// Optional: expose on window for legacy callers
|
||||||
window.apiGETJSON = apiGETJSON;
|
window.apiGETJSON = apiGETJSON;
|
||||||
window.apiPOSTJSON = apiPOSTJSON;
|
window.apiPOSTJSON = apiPOSTJSON;
|
||||||
|
window.triggerLogout = triggerLogout; // expose the moved helper
|
||||||
|
|
||||||
// Global handler to keep UX friendly if something forgets to catch
|
// Global handler to keep UX friendly if something forgets to catch
|
||||||
window.addEventListener("unhandledrejection", (ev) => {
|
window.addEventListener("unhandledrejection", (ev) => {
|
||||||
@@ -98,131 +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);
|
const params = new URLSearchParams(window.location.search);
|
||||||
if (params.get('logout') === '1') {
|
if (params.get('logout') === '1') {
|
||||||
localStorage.removeItem("username");
|
localStorage.removeItem("username");
|
||||||
localStorage.removeItem("userTOTPEnabled");
|
localStorage.removeItem("userTOTPEnabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function triggerLogout() {
|
|
||||||
_nativeFetch("/api/auth/logout.php", {
|
|
||||||
method: "POST",
|
|
||||||
credentials: "include",
|
|
||||||
headers: { "X-CSRF-Token": getCsrfToken() }
|
|
||||||
})
|
|
||||||
.then(() => 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 () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Load admin config early
|
// Load site config early (safe subset)
|
||||||
loadAdminConfigFunc();
|
loadAdminConfigFunc();
|
||||||
|
|
||||||
// i18n
|
// i18n
|
||||||
@@ -304,3 +200,16 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
// trashRestoreDelete.js
|
||||||
import { sendRequest } from './networkUtils.js';
|
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
|
||||||
import { toggleVisibility, showToast } from './domUtils.js';
|
import { toggleVisibility, showToast } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { loadFileList } from './fileListView.js';
|
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { loadFolderTree } from './folderManager.js';
|
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
function showConfirm(message, onConfirm) {
|
function showConfirm(message, onConfirm) {
|
||||||
const modal = document.getElementById("customConfirmModal");
|
const modal = document.getElementById("customConfirmModal");
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { initFileActions } from './fileActions.js';
|
import { initFileActions } from './fileActions.js?v={{APP_QVER}}';
|
||||||
import { displayFilePreview } from './filePreview.js';
|
import { displayFilePreview } from './filePreview.js?v={{APP_QVER}}';
|
||||||
import { showToast, escapeHTML } from './domUtils.js';
|
import { showToast, escapeHTML } from './domUtils.js?v={{APP_QVER}}';
|
||||||
import { loadFolderTree } from './folderManager.js';
|
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||||||
import { loadFileList } from './fileListView.js';
|
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||||
|
|
||||||
/* -----------------------------------------------------
|
/* -----------------------------------------------------
|
||||||
Helpers for Drag–and–Drop Folder Uploads (Original Code)
|
Helpers for Drag–and–Drop Folder Uploads (Original Code)
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// generated by CI
|
// generated by CI
|
||||||
window.APP_VERSION = 'v1.6.9';
|
window.APP_VERSION = 'v1.7.2';
|
||||||
|
|||||||
21
public/vendor/redoc/LICENSE
vendored
Normal file
21
public/vendor/redoc/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-present, Rebilly, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
1832
public/vendor/redoc/redoc.standalone.js
vendored
Normal file
1832
public/vendor/redoc/redoc.standalone.js
vendored
Normal file
File diff suppressed because one or more lines are too long
54
scripts/stamp-assets.sh
Normal file
54
scripts/stamp-assets.sh
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# usage: scripts/stamp-assets.sh vX.Y.Z /path/to/target/dir
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VER="${1:?usage: stamp-assets.sh vX.Y.Z target_dir}"
|
||||||
|
QVER="${VER#v}"
|
||||||
|
TARGET="${2:-.}"
|
||||||
|
|
||||||
|
echo "Stamping assets in: $TARGET"
|
||||||
|
echo "VER=${VER} QVER=${QVER}"
|
||||||
|
|
||||||
|
cd "$TARGET"
|
||||||
|
|
||||||
|
# Normalize CRLF to LF (if any files were edited on Windows)
|
||||||
|
# We only touch web assets.
|
||||||
|
find public \( -name '*.html' -o -name '*.php' -o -name '*.css' -o -name '*.js' \) -type f -print0 \
|
||||||
|
| xargs -0 -r sed -i 's/\r$//'
|
||||||
|
|
||||||
|
# --- HTML/CSS/PHP: stamp ?v=... and {{APP_VER}} ---
|
||||||
|
# (?v=...) -> ?v=<QVER>
|
||||||
|
HTML_CSS_COUNT=0
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
sed -E -i "s/(\?v=)[^\"'&<>\s]*/\1${QVER}/g" "$f"
|
||||||
|
sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f"
|
||||||
|
HTML_CSS_COUNT=$((HTML_CSS_COUNT+1))
|
||||||
|
done < <(find public -type f \( -name '*.html' -o -name '*.php' -o -name '*.css' \) -print0)
|
||||||
|
|
||||||
|
# --- JS: stamp placeholders and normalize any pre-existing ?v=... ---
|
||||||
|
JS_COUNT=0
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
# Replace placeholders
|
||||||
|
sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f"
|
||||||
|
sed -E -i "s/\{\{APP_QVER\}\}/${QVER}/g" "$f"
|
||||||
|
# Normalize any "?v=..." that appear in ESM imports or strings
|
||||||
|
# This keeps any ".js" or ".mjs" then forces ?v=<QVER>
|
||||||
|
perl -0777 -i -pe "s@(\.m?js)\?v=[^\"')]+@\1?v=${QVER}@g" "$f"
|
||||||
|
JS_COUNT=$((JS_COUNT+1))
|
||||||
|
done < <(find public -type f -name '*.js' -print0)
|
||||||
|
|
||||||
|
# Force-write version.js (source of truth in stamped output)
|
||||||
|
if [[ -f public/js/version.js ]]; then
|
||||||
|
printf "window.APP_VERSION = '%s';\n" "$VER" > public/js/version.js
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Touched files: HTML/CSS/PHP=${HTML_CSS_COUNT}, JS=${JS_COUNT}"
|
||||||
|
|
||||||
|
# Final self-check: fail if anything is left
|
||||||
|
if grep -R -n -E "{{APP_QVER}}|{{APP_VER}}" public \
|
||||||
|
--include='*.html' --include='*.php' --include='*.css' --include='*.js' 2>/dev/null; then
|
||||||
|
echo "ERROR: Placeholders remain after stamping." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Stamped to ${VER} (${QVER})"
|
||||||
@@ -99,7 +99,7 @@ class AdminController
|
|||||||
'header_title' => '',
|
'header_title' => '',
|
||||||
'loginOptions' => [
|
'loginOptions' => [
|
||||||
'disableFormLogin' => false,
|
'disableFormLogin' => false,
|
||||||
'disableBasicAuth' => false,
|
'disableBasicAuth' => true,
|
||||||
'disableOIDCLogin' => true,
|
'disableOIDCLogin' => true,
|
||||||
'authBypass' => false,
|
'authBypass' => false,
|
||||||
'authHeaderName' => 'X-Remote-User'
|
'authHeaderName' => 'X-Remote-User'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||||||
|
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserController
|
* UserController
|
||||||
@@ -665,4 +666,38 @@ class UserController
|
|||||||
echo json_encode(['success' => true, 'url' => $url]);
|
echo json_encode(['success' => true, 'url' => $url]);
|
||||||
exit;
|
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;
|
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.
|
* Updates the admin configuration file.
|
||||||
*
|
*
|
||||||
@@ -157,6 +202,14 @@ class AdminModel
|
|||||||
// Best-effort normalize perms for host visibility (user rw, group rw)
|
// Best-effort normalize perms for host visibility (user rw, group rw)
|
||||||
@chmod($configFile, 0664);
|
@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."];
|
return ["success" => "Configuration updated successfully."];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +315,7 @@ class AdminModel
|
|||||||
],
|
],
|
||||||
'loginOptions' => [
|
'loginOptions' => [
|
||||||
'disableFormLogin' => false,
|
'disableFormLogin' => false,
|
||||||
'disableBasicAuth' => false,
|
'disableBasicAuth' => true,
|
||||||
'disableOIDCLogin' => true
|
'disableOIDCLogin' => true
|
||||||
],
|
],
|
||||||
'globalOtpauthUrl' => "",
|
'globalOtpauthUrl' => "",
|
||||||
|
|||||||
Reference in New Issue
Block a user