Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4f7ec0dca | ||
|
|
5a7c4704d0 | ||
|
|
8b880738d6 | ||
|
|
06c732971f | ||
|
|
ab75381acb | ||
|
|
b1bd903072 | ||
|
|
ab327acc8a | ||
|
|
2e98ceee4c | ||
|
|
3351a11927 | ||
|
|
4dddcf0f99 | ||
|
|
35966964e7 | ||
|
|
7fe8e858ae | ||
|
|
64332211c9 | ||
|
|
3e37738e3f | ||
|
|
2ba33f40f8 | ||
|
|
badcf5c02b | ||
|
|
89976f444f | ||
|
|
9c53c37f38 | ||
|
|
a400163dfb | ||
|
|
ebe5939bf5 | ||
|
|
83757c7470 | ||
|
|
8e363ea758 | ||
|
|
2739925f0b |
44
.gitattributes
vendored
@@ -1,4 +1,40 @@
|
||||
public/api.html linguist-documentation
|
||||
public/openapi.json linguist-documentation
|
||||
resources/ export-ignore
|
||||
.github/ export-ignore
|
||||
# --- Docs that shouldn't count toward code stats
|
||||
public/api.php linguist-documentation
|
||||
public/openapi.json linguist-documentation
|
||||
openapi.json.dist linguist-documentation
|
||||
SECURITY.md linguist-documentation
|
||||
CHANGELOG.md linguist-documentation
|
||||
CONTRIBUTING.md linguist-documentation
|
||||
CODE_OF_CONDUCT.md linguist-documentation
|
||||
LICENSE linguist-documentation
|
||||
README.md linguist-documentation
|
||||
|
||||
# --- Vendored/minified stuff: exclude from Linguist
|
||||
public/vendor/** linguist-vendored
|
||||
public/css/vendor/** linguist-vendored
|
||||
public/fonts/** linguist-vendored
|
||||
public/js/**/*.min.js linguist-vendored
|
||||
public/**/*.min.css linguist-vendored
|
||||
public/**/*.map linguist-generated
|
||||
|
||||
# --- Treat assets as binary (nicer diffs)
|
||||
*.png -diff
|
||||
*.jpg -diff
|
||||
*.jpeg -diff
|
||||
*.gif -diff
|
||||
*.webp -diff
|
||||
*.svg -diff
|
||||
*.ico -diff
|
||||
*.woff -diff
|
||||
*.woff2 -diff
|
||||
*.ttf -diff
|
||||
*.otf -diff
|
||||
*.zip -diff
|
||||
|
||||
# --- Keep these out of auto-generated source archives (OK to ignore)
|
||||
# Only ignore things you *never* need in release tarballs
|
||||
.github/ export-ignore
|
||||
resources/ export-ignore
|
||||
|
||||
# --- Normalize text files
|
||||
* text=auto
|
||||
159
.github/workflows/release-on-version.yml
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: Release on version.js update
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
paths:
|
||||
- public/js/version.js
|
||||
workflow_run:
|
||||
workflows: ["Bump version and sync Changelog to Docker Repo"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}-${{ github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Ensure tags available
|
||||
run: |
|
||||
git fetch --tags --force --prune --quiet
|
||||
|
||||
- name: Read version from version.js
|
||||
id: ver
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VER=$(grep -Eo "APP_VERSION\s*=\s*['\"]v[^'\"]+['\"]" public/js/version.js | sed -E "s/.*['\"](v[^'\"]+)['\"].*/\1/")
|
||||
if [[ -z "$VER" ]]; then
|
||||
echo "Could not parse APP_VERSION from version.js" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VER" >> "$GITHUB_OUTPUT"
|
||||
echo "Parsed version: $VER"
|
||||
|
||||
- name: Skip if tag already exists
|
||||
id: tagcheck
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git rev-parse -q --verify "refs/tags/${{ steps.ver.outputs.version }}" >/dev/null; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Tag ${{ steps.ver.outputs.version }} already exists. Skipping release."
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build zip artifact
|
||||
if: steps.tagcheck.outputs.exists == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ZIP="FileRise-${{ steps.ver.outputs.version }}.zip"
|
||||
zip -r "$ZIP" . \
|
||||
-x "./.git/*" "./.github/*" \
|
||||
"./resources/*" "./resources/**" \
|
||||
"./.dockerignore" "./.gitattributes" "./.gitignore" \
|
||||
"$ZIP" "${ZIP}.sha256" >/dev/null
|
||||
|
||||
- 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'
|
||||
id: notes
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NOTES_PATH=""
|
||||
if [[ -f CHANGELOG.md ]]; then
|
||||
awk '
|
||||
BEGIN{found=0}
|
||||
/^## / && !found {found=1}
|
||||
found && /^---$/ {exit}
|
||||
found {print}
|
||||
' CHANGELOG.md > CHANGELOG_SNIPPET.md || true
|
||||
sed -i -e :a -e '/^\n*$/{$d;N;ba' -e '}' CHANGELOG_SNIPPET.md || true
|
||||
if [[ -s CHANGELOG_SNIPPET.md ]]; then
|
||||
NOTES_PATH="CHANGELOG_SNIPPET.md"
|
||||
fi
|
||||
fi
|
||||
echo "path=$NOTES_PATH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- 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'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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 }}"
|
||||
|
||||
{
|
||||
echo "## ${VER}"
|
||||
echo
|
||||
if [[ -s CHANGELOG_SNIPPET.md ]]; then
|
||||
cat CHANGELOG_SNIPPET.md
|
||||
echo
|
||||
fi
|
||||
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
|
||||
with:
|
||||
tag_name: ${{ steps.ver.outputs.version }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
name: ${{ steps.ver.outputs.version }}
|
||||
body_path: RELEASE_BODY.md
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
FileRise-${{ steps.ver.outputs.version }}.zip
|
||||
FileRise-${{ steps.ver.outputs.version }}.zip.sha256
|
||||
49
.github/workflows/sync-changelog.yml
vendored
@@ -4,7 +4,7 @@ name: Bump version and sync Changelog to Docker Repo
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'CHANGELOG.md'
|
||||
- "CHANGELOG.md"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -15,37 +15,70 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract version from commit message
|
||||
id: ver
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
MSG="${{ github.event.head_commit.message }}"
|
||||
if [[ "$MSG" =~ release\((v[0-9]+\.[0-9]+\.[0-9]+)\) ]]; then
|
||||
echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
|
||||
echo "version=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT"
|
||||
echo "Found version: ${BASH_REMATCH[1]}"
|
||||
else
|
||||
echo "version=" >> $GITHUB_OUTPUT
|
||||
echo "version=" >> "$GITHUB_OUTPUT"
|
||||
echo "No release(vX.Y.Z) tag in commit message; skipping bump."
|
||||
fi
|
||||
|
||||
- name: Update public/js/version.js
|
||||
if: steps.ver.outputs.version != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cat > public/js/version.js <<'EOF'
|
||||
// generated by CI
|
||||
window.APP_VERSION = '${{ steps.ver.outputs.version }}';
|
||||
EOF
|
||||
|
||||
- name: Commit version.js (if changed)
|
||||
- name: Stamp asset cache-busters (?v=...) in HTML/CSS and {{APP_VER}} everywhere
|
||||
if: steps.ver.outputs.version != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VER="${{ steps.ver.outputs.version }}" # e.g. v1.6.9
|
||||
QVER="${VER#v}" # e.g. 1.6.9
|
||||
echo "Stamping ?v=${QVER} and {{APP_VER}}=${VER}"
|
||||
|
||||
# 1) Only stamp ?v= in HTML/CSS (avoid JS concatenation issues)
|
||||
mapfile -t html_css < <(git ls-files -- 'public/*.html' 'public/**/*.html' 'public/*.php' 'public/**/*.css')
|
||||
for f in "${html_css[@]}"; do
|
||||
sed -E -i "s/(\?v=)[^\"'&<>\s]*/\1${QVER}/g" "$f"
|
||||
sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f"
|
||||
done
|
||||
|
||||
# 2) For JS, only replace the {{APP_VER}} placeholder (do NOT touch ?v=)
|
||||
mapfile -t jsfiles < <(git ls-files -- 'public/*.js' 'public/**/*.js')
|
||||
for f in "${jsfiles[@]}"; do
|
||||
sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f"
|
||||
done
|
||||
|
||||
echo "Changed files:"
|
||||
git status --porcelain | awk '{print $2}' | sed 's/^/ - /'
|
||||
|
||||
- name: Commit version bump + stamped assets
|
||||
if: steps.ver.outputs.version != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add public/js/version.js
|
||||
git add public/js/version.js public
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore: set APP_VERSION to ${{ steps.ver.outputs.version }}"
|
||||
git commit -m "chore(release): set APP_VERSION and stamp assets to ${{ steps.ver.outputs.version }} [skip ci]"
|
||||
git push
|
||||
fi
|
||||
|
||||
@@ -59,14 +92,18 @@ jobs:
|
||||
|
||||
- name: Copy CHANGELOG.md and write VERSION
|
||||
if: steps.ver.outputs.version != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp CHANGELOG.md docker-repo/CHANGELOG.md
|
||||
echo "${{ steps.ver.outputs.version }}" > docker-repo/VERSION
|
||||
|
||||
- name: Commit & push to docker repo
|
||||
if: steps.ver.outputs.version != ''
|
||||
working-directory: docker-repo
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add CHANGELOG.md VERSION
|
||||
|
||||
140
CHANGELOG.md
@@ -1,5 +1,145 @@
|
||||
# Changelog
|
||||
|
||||
## 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)
|
||||
|
||||
release(v1.6.9): feat(core) localize assets, harden headers, and speed up load
|
||||
|
||||
- index.html: drop all CDNs in favor of local /vendor assets
|
||||
- add versioned cache-busting query (?v=…) on CSS/JS
|
||||
- wire version.js for APP_VERSION and numeric cache key
|
||||
- public/vendor/: add pinned copies of:
|
||||
- bootstrap 4.5.2, codemirror 5.65.5 (+ themes/modes), dompurify 2.4.0,
|
||||
fuse.js 6.6.2, resumable.js 1.1.0
|
||||
- fonts: add self-hosted Material Icons + Roboto (latin + latin-ext) with
|
||||
vendor CSS (material-icons.css, roboto.css)
|
||||
|
||||
- fileEditor.js: load CodeMirror modes from local vendor with ?v=APP_VERSION_NUM,
|
||||
keep timeout/plain-text fallback, no SRI (same-origin)
|
||||
- dragAndDrop.js: nudge zonesToggle 65px left to sit tighter to the logo
|
||||
|
||||
- styles.css: prune/organize rules and add small utility classes; move 3P
|
||||
font CSS to /css/vendor/
|
||||
|
||||
- .htaccess: security + performance overhaul
|
||||
- Content-Security-Policy: default-src 'self'; img-src include data: and blob:
|
||||
- version-aware caching: HTML/version.js = no-cache; assets with ?v= = 1y immutable
|
||||
- correct MIME for fonts/SVG; enable Brotli/Gzip (if available)
|
||||
- X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS, Permissions-Policy
|
||||
- disable TRACE; deny dotfiles; prevent directory listing
|
||||
|
||||
- .gitattributes: mark vendor/minified as linguist-vendored, treat assets as
|
||||
binary in diffs, exclude CI/resources from source archives
|
||||
|
||||
- docs/licensing:
|
||||
- add licenses/ and THIRD_PARTY.md with upstream licenses/attribution
|
||||
- README: add “License & Credits” section with components and licenses
|
||||
|
||||
- CI: (sync-changelog) stamp asset cache-busters to the numeric release
|
||||
(e.g. ?v=1.6.9) and write window.APP_VERSION in version.js before Docker build
|
||||
|
||||
perf: site loads significantly faster with local assets + compression + long-lived caching
|
||||
security: CSP, strict headers, and same-origin assets reduce XSS/SRI/CORS risk
|
||||
|
||||
Refs: #performance #security
|
||||
|
||||
---
|
||||
|
||||
## Changes 10/25/2025 (v1.6.8)
|
||||
|
||||
release(v1.6.8): fix(ui) prevent Extract/Create flash on refresh; remember last folder
|
||||
|
||||
- Seed `currentFolder` from `localStorage.lastOpenedFolder` (fallback to "root")
|
||||
- Stop eager `loadFileList('root')` on boot; defer initial load to resolved folder
|
||||
- Hide capability-gated actions by default (`#extractZipBtn`, `#createBtn`) to avoid pre-auth flash
|
||||
- Eliminates transient root state when reloading inside a subfolder
|
||||
|
||||
User-visible: refreshing a non-root folder no longer flashes Root items or privileged buttons; app resumes in the last opened folder.
|
||||
|
||||
---
|
||||
|
||||
## Changes 10/25/2025 (v1.6.7)
|
||||
|
||||
release(v1.6.7): Folder Move feature, stable DnD persistence, safer uploads, and ACL/UI polish
|
||||
|
||||
### 📂 Folder Move (new major feature)
|
||||
|
||||
**Drag & Drop to move folder, use context menu or Move Folder button**
|
||||
|
||||
- Added **Move Folder** support across backend and UI.
|
||||
- New API endpoint: `public/api/folder/moveFolder.php`
|
||||
- Controller and ACL updates to validate scope, ownership, and permissions.
|
||||
- Non-admins can only move within folders they own.
|
||||
- `ACL::renameTree()` re-keys all subtree ACLs on folder rename/move.
|
||||
- Introduced new capabilities:
|
||||
- `canMoveFolder`
|
||||
- `canMove` (UI alias for backward compatibility)
|
||||
- New “Move Folder” button + modal in the UI with full i18n strings (`i18n.js`).
|
||||
- Action button styling and tooltip consistency for all folder actions.
|
||||
|
||||
### 🧱 Drag & Drop / Layout Improvements
|
||||
|
||||
- Fixed **random sidebar → top zone jumps** on refresh.
|
||||
- Cards/panels now **persist exactly where you placed them** (`userZonesSnapshot`)
|
||||
— no unwanted repositioning unless the window is resized below the small-screen threshold.
|
||||
- Added hysteresis around the 1205 px breakpoint to prevent flicker when resizing.
|
||||
- Eliminated the 50 px “ghost” gutter with `clampSidebarWhenEmpty()`:
|
||||
- Sidebar no longer reserves space when collapsed or empty.
|
||||
- Temporarily “unclamps” during drag so drop targets remain accurate and full-width.
|
||||
- Removed forced 800 px height on drag highlight; uses natural flex layout now.
|
||||
- General layout polish — smoother transitions when toggling *Hide/Show Panels*.
|
||||
|
||||
### ☁️ Uploads & UX
|
||||
|
||||
- Stronger folder sanitization and safer base-path handling.
|
||||
- Fixed subfolder creation when uploading directories (now builds under correct parent).
|
||||
- Improved chunk error handling and metadata key correctness.
|
||||
- Clearer success/failure toasts and accurate filename display from server responses.
|
||||
|
||||
### 🔐 Permissions / ACL
|
||||
|
||||
- Simplified file rename checks — now rely solely on granular `ACL::canRename()`.
|
||||
- Updated capability lists to include move/rename operations consistently.
|
||||
|
||||
### 🌐 UI / i18n Enhancements
|
||||
|
||||
- Added i18n strings for new “Move Folder” prompts, modals, and tooltips.
|
||||
- Minor UI consistency tweaks: button alignment, focus states, reduced-motion support.
|
||||
|
||||
---
|
||||
|
||||
## Changes 10/24/2025 (v1.6.6)
|
||||
|
||||
release(v1.6.6): header-mounted toggle, dark-mode polish, persistent layout, and ACL fix
|
||||
|
||||
17
README.md
@@ -23,9 +23,9 @@ With drag-and-drop uploads, in-browser editing, secure user logins (SSO & TOTP 2
|
||||
|
||||
> ⚠️ **Security fix in v1.5.0** — ACL hardening. If you’re on ≤1.4.x, please upgrade.
|
||||
|
||||
**4/3/2025 Video demo:**
|
||||
**10/25/2025 Video demo:**
|
||||
|
||||
<https://github.com/user-attachments/assets/221f6a53-85f5-48d4-9abe-89445e0af90e>
|
||||
<https://github.com/user-attachments/assets/a2240300-6348-4de7-b72f-1b85b7da3a08>
|
||||
|
||||
**Dark mode:**
|
||||

|
||||
@@ -139,7 +139,7 @@ docker run -d \
|
||||
-e DATE_TIME_FORMAT="m/d/y h:iA" \
|
||||
-e TOTAL_UPLOAD_SIZE="5G" \
|
||||
-e SECURE="false" \
|
||||
-e PERSISTENT_TOKENS_KEY="please_change_this_@@" \
|
||||
-e PERSISTENT_TOKENS_KEY="default_please_change_this_key" \
|
||||
-e PUID="1000" \
|
||||
-e PGID="1000" \
|
||||
-e CHOWN_ON_START="true" \
|
||||
@@ -175,10 +175,10 @@ docker exec -it filerise id www-data
|
||||
Save as `docker-compose.yml`, then `docker-compose up -d`:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
filerise:
|
||||
image: error311/filerise-docker:latest
|
||||
container_name: filerise
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
DATE_TIME_FORMAT: "m/d/y h:iA"
|
||||
TOTAL_UPLOAD_SIZE: "10G"
|
||||
SECURE: "false"
|
||||
PERSISTENT_TOKENS_KEY: "please_change_this_@@"
|
||||
PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
|
||||
# Ownership & indexing
|
||||
PUID: "1000" # Unraid users often use 99
|
||||
PGID: "1000" # Unraid users often use 100
|
||||
@@ -198,6 +198,7 @@ services:
|
||||
- ./uploads:/var/www/uploads
|
||||
- ./users:/var/www/users
|
||||
- ./metadata:/var/www/metadata
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Access at `http://localhost:8080` (or your server’s IP).
|
||||
@@ -423,6 +424,10 @@ Every bit helps me keep FileRise fast, polished, and well-maintained. Thank you!
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## License & Credits
|
||||
|
||||
MIT License – see [LICENSE](LICENSE).
|
||||
This project bundles third-party assets such as Bootstrap, CodeMirror, DOMPurify, Fuse.js, Resumable.js, and Google Fonts (Roboto, Material Icons).
|
||||
All third-party code and fonts remain under their original open-source licenses (MIT or Apache 2.0).
|
||||
|
||||
See THIRD_PARTY.md and the /licenses directory for full license texts and attributions.
|
||||
|
||||
47
THIRD_PARTY.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Third-Party Notices
|
||||
|
||||
FileRise bundles the following third‑party assets. Each item lists the project, version, typical on-disk location in this repo, and its license.
|
||||
|
||||
If you believe any attribution is missing or incorrect, please open an issue.
|
||||
|
||||
---
|
||||
|
||||
## Fonts
|
||||
|
||||
- **Roboto (wght 400/500)** — Google Fonts
|
||||
**License:** Apache License 2.0
|
||||
**Files:** `public/css/vendor/roboto.css`, `public/fonts/roboto/*.woff2`
|
||||
|
||||
- **Material Icons (ligature font)** — Google Fonts
|
||||
**License:** Apache License 2.0
|
||||
**Files:** `public/css/vendor/material-icons.css`, `public/fonts/material-icons/*.woff2`
|
||||
|
||||
> Google fonts/icons © Google. Licensed under Apache 2.0. See `licenses/apache-2.0.txt`.
|
||||
|
||||
---
|
||||
|
||||
## CSS / JS Libraries (vendored)
|
||||
|
||||
- **Bootstrap 4.5.2** — MIT License
|
||||
**Files:** `public/vendor/bootstrap/4.5.2/bootstrap.min.css`
|
||||
|
||||
- **CodeMirror 5.65.5** — MIT License
|
||||
**Files:** `public/vendor/codemirror/5.65.5/*`
|
||||
|
||||
- **DOMPurify 2.4.0** — Apache License 2.0
|
||||
**Files:** `public/vendor/dompurify/2.4.0/purify.min.js`
|
||||
|
||||
- **Fuse.js 6.6.2** — Apache License 2.0
|
||||
**Files:** `public/vendor/fuse/6.6.2/fuse.min.js`
|
||||
|
||||
- **Resumable.js 1.1.0** — MIT License
|
||||
**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`.
|
||||
> Apache-2.0–licensed code: see `licenses/apache-2.0.txt`.
|
||||
|
||||
---
|
||||
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
|
||||
5
licenses/NOTICE_GOOGLE_FONTS.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Google Fonts & Icons NOTICE
|
||||
|
||||
This product bundles font files from Google Fonts (Roboto, Material Icons, and/or Material Symbols).
|
||||
Copyright 2012–present Google Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (see ../apache-2.0.txt).
|
||||
202
licenses/apache-2.0.txt
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
19
licenses/mit.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
MIT License
|
||||
|
||||
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.
|
||||
@@ -1,81 +1,76 @@
|
||||
# -----------------------------
|
||||
# 1) Prevent directory listings
|
||||
# -----------------------------
|
||||
# --------------------------------
|
||||
# Base: safe in most environments
|
||||
# --------------------------------
|
||||
Options -Indexes
|
||||
|
||||
# -----------------------------
|
||||
# Default index files
|
||||
# -----------------------------
|
||||
DirectoryIndex index.html
|
||||
|
||||
# -----------------------------
|
||||
# Deny access to hidden files
|
||||
# -----------------------------
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
<IfModule mod_authz_core.c>
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
# -----------------------------
|
||||
# Enforce HTTPS (optional)
|
||||
# -----------------------------
|
||||
RewriteEngine On
|
||||
#RewriteCond %{HTTPS} off
|
||||
#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
# Allow requests from a specific origin
|
||||
#Header set Access-Control-Allow-Origin "https://demo.filerise.net"
|
||||
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
|
||||
Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, X-CSRF-Token"
|
||||
Header set Access-Control-Allow-Credentials "true"
|
||||
# MIME types for fonts/SVG
|
||||
<IfModule mod_mime.c>
|
||||
AddType font/woff2 .woff2
|
||||
AddType font/woff .woff
|
||||
AddType image/svg+xml .svg
|
||||
</IfModule>
|
||||
|
||||
# Security headers
|
||||
<IfModule mod_headers.c>
|
||||
# Prevent clickjacking
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
# Block XSS
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
# No MIME sniffing
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||
Header always set X-Download-Options "noopen"
|
||||
Header always set Expect-CT "max-age=86400, enforce"
|
||||
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; worker-src 'self' blob:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self'"
|
||||
</IfModule>
|
||||
|
||||
# Caching
|
||||
SetEnvIfNoCase QUERY_STRING "(^|&)v=" has_version_param=1
|
||||
<IfModule mod_headers.c>
|
||||
# HTML: always revalidate
|
||||
<FilesMatch "\.(html|htm)$">
|
||||
<FilesMatch "\.(html?|php)$">
|
||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
# JS/CSS: short‑term cache, revalidate regularly
|
||||
<FilesMatch "\.(js|css)$">
|
||||
Header set Cache-Control "public, max-age=3600, must-revalidate"
|
||||
</FilesMatch>
|
||||
# version.js should always revalidate (it changes on releases)
|
||||
|
||||
<FilesMatch "^js/version\.js$">
|
||||
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.(js|css)$">
|
||||
Header set Cache-Control "public, max-age=3600, must-revalidate" env=!has_version_param
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.(png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$">
|
||||
Header set Cache-Control "public, max-age=604800" env=!has_version_param
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.(js|css|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$">
|
||||
Header set Cache-Control "public, max-age=31536000, immutable" env=has_version_param
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
# -----------------------------
|
||||
# Additional Security Headers
|
||||
# -----------------------------
|
||||
<IfModule mod_headers.c>
|
||||
# Enforce HTTPS for a year with subdomains and preload option.
|
||||
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
# Set a Referrer Policy.
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
# Permissions Policy: disable features you don't need.
|
||||
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||
# IE-specific header to prevent downloads from opening in IE.
|
||||
Header always set X-Download-Options "noopen"
|
||||
# Expect-CT header for Certificate Transparency (optional).
|
||||
Header always set Expect-CT "max-age=86400, enforce"
|
||||
# Compression (only if module exists)
|
||||
<IfModule mod_brotli.c>
|
||||
BrotliCompressionQuality 5
|
||||
AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json image/svg+xml
|
||||
</IfModule>
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml
|
||||
</IfModule>
|
||||
|
||||
# -----------------------------
|
||||
# Disable TRACE method
|
||||
# -----------------------------
|
||||
# Disable TRACE
|
||||
RewriteCond %{REQUEST_METHOD} ^TRACE
|
||||
RewriteRule .* - [F]
|
||||
@@ -19,13 +19,15 @@ if (isset($_GET['spec'])) {
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>FileRise API Docs</title>
|
||||
<script defer src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"
|
||||
integrity="sha384-70P5pmIdaQdVbxvjhrcTDv1uKcKqalZ3OHi7S2J+uzDl0PW8dO6L+pHOpm9EEjGJ"
|
||||
crossorigin="anonymous"></script>
|
||||
<script defer src="/js/redoc-init.js"></script>
|
||||
|
||||
<!-- Local ReDoc bundle -->
|
||||
<script defer src="/vendor/redoc/redoc.standalone.js?v=1.6.11"></script>
|
||||
|
||||
<!-- Your init (also local) -->
|
||||
<script defer src="/js/redoc-init.js?v=1.6.11"></script>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="api.php?spec=1"></redoc>
|
||||
<redoc spec-url="/api.php?spec=1"></redoc>
|
||||
<div id="redoc-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -185,6 +185,7 @@ $canDelete = $gDeleteBase && !$readOnly && $inScope;
|
||||
$canReceive = ($gUploadBase || $gCreateBase || $canManageBase) && !$readOnly && $inScope;
|
||||
// Back-compat: expose as canMoveIn (used by toolbar/context-menu/drag&drop)
|
||||
$canMoveIn = $canReceive;
|
||||
$canMoveAlias = $canMoveIn;
|
||||
$canEdit = $gEditBase && !$readOnly && $inScope;
|
||||
$canCopy = $gCopyBase && !$readOnly && $inScope;
|
||||
$canExtract = $gExtractBase && !$readOnly && $inScope;
|
||||
@@ -200,6 +201,12 @@ if ($isRoot) {
|
||||
$canRename = false;
|
||||
$canDelete = false;
|
||||
$canShareFoldEff = false;
|
||||
$canMoveFolder = false;
|
||||
}
|
||||
|
||||
if (!$isRoot) {
|
||||
$canMoveFolder = (ACL::canManage($username, $perms, $folder) || ACL::isOwner($username, $perms, $folder))
|
||||
&& !$readOnly;
|
||||
}
|
||||
|
||||
$owner = null;
|
||||
@@ -225,6 +232,8 @@ echo json_encode([
|
||||
'canRename' => $canRename,
|
||||
'canDelete' => $canDelete,
|
||||
'canMoveIn' => $canMoveIn,
|
||||
'canMove' => $canMoveAlias,
|
||||
'canMoveFolder'=> $canMoveFolder,
|
||||
'canEdit' => $canEdit,
|
||||
'canCopy' => $canCopy,
|
||||
'canExtract' => $canExtract,
|
||||
|
||||
9
public/api/folder/moveFolder.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// public/api/folder/moveFolder.php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||
|
||||
$controller = new FolderController();
|
||||
$controller->moveFolder();
|
||||
24
public/css/vendor/material-icons.css
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/* fallback */
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/fonts/material-icons/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
44
public/css/vendor/roboto.css
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Roboto Regular 400 — latin-ext */
|
||||
@font-face{
|
||||
font-family:'Roboto';
|
||||
font-style:normal;
|
||||
font-weight:400;
|
||||
font-display:swap;
|
||||
src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2') format('woff2');
|
||||
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
/* Roboto Regular 400 — latin */
|
||||
@font-face{
|
||||
font-family:'Roboto';
|
||||
font-style:normal;
|
||||
font-weight:400;
|
||||
font-display:swap;
|
||||
src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2') format('woff2');
|
||||
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
/* Roboto Medium 500 — latin-ext */
|
||||
@font-face{
|
||||
font-family:'Roboto';
|
||||
font-style:normal;
|
||||
font-weight:500;
|
||||
font-display:swap;
|
||||
src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2') format('woff2');
|
||||
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
/* Roboto Medium 500 — latin */
|
||||
@font-face{
|
||||
font-family:'Roboto';
|
||||
font-style:normal;
|
||||
font-weight:500;
|
||||
font-display:swap;
|
||||
src:url('/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2') format('woff2');
|
||||
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
/* sensible stack so Chinese falls back cleanly */
|
||||
:root{
|
||||
--ui-font: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC",
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
}
|
||||
body{ font-family: var(--ui-font); }
|
||||
BIN
public/fonts/material-icons/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2
vendored
Normal file
BIN
public/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2
vendored
Normal file
BIN
public/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2
vendored
Normal file
@@ -9,58 +9,32 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
||||
<meta name="csrf-token" content="">
|
||||
<meta name="share-url" content="">
|
||||
<style>
|
||||
/* hide the app shell until JS says otherwise */
|
||||
.main-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* full-screen white overlay while we check auth */
|
||||
#loadingOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--bg-color, #fff);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<!-- Google Fonts and Material Icons -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
||||
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css"
|
||||
integrity="sha384-zaeBlB/vwYsDRSlFajnDd7OydJ0cWk+c2OWybl3eSUf6hW2EbhlCsQPqKr3gkznT" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css"
|
||||
integrity="sha384-eZTPTN0EvJdn23s24UDYJmUM2T7C2ZFa3qFLypeBruJv8mZeTusKUAO/j5zPAQ6l" crossorigin="anonymous">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"
|
||||
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js"
|
||||
integrity="sha384-xPpkMo5nDgD98fIcuRVYhxkZV6/9Y4L8s3p0J5c4MxgJkyKJ8BJr+xfRkq7kn6Tw"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/css/css.min.js"
|
||||
integrity="sha384-to8njsu2GAiXQnY/aLGzz0DIY/SFSeSDodtvSl869n2NmsBdHOTZNNqbEBPYh7Pa"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"
|
||||
integrity="sha384-kmQrbJf09Uo1WRLMDVGoVG3nM6F48frIhcj7f3FDUjeRzsiHwyBWDjMUIttnIeAf"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/resumable.js/1.1.0/resumable.min.js"
|
||||
integrity="sha384-EXTg7rRfdTPZWoKVCslusAAev2TYw76fm+Wox718iEtFQ+gdAdAc5Z/ndLHSo4mq"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
||||
integrity="sha384-Tsl3d5pUAO7a13enIvSsL3O0/95nsthPJiPto5NtLuY8w3+LbZOpr3Fl2MNmrh1E"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"
|
||||
integrity="sha384-zPE55eyESN+FxCWGEnlNxGyAPJud6IZ6TtJmXb56OFRGhxZPN4akj9rjA3gw5Qqa"
|
||||
crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="css/styles.css" />
|
||||
<style>.main-wrapper{display:none}#loadingOverlay{position:fixed;inset:0;background:var(--bg-color,#fff);z-index:9999;display:flex;align-items:center;justify-content:center}</style>
|
||||
<link rel="stylesheet" href="/css/vendor/roboto.css?v=1.6.11">
|
||||
<link rel="stylesheet" href="/css/vendor/material-icons.css?v=1.6.11">
|
||||
|
||||
<!-- Bootstrap CSS (local) -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap/4.5.2/bootstrap.min.css?v=1.6.11">
|
||||
|
||||
<!-- CodeMirror CSS (local) -->
|
||||
<link rel="stylesheet" href="/vendor/codemirror/5.65.5/codemirror.min.css?v=1.6.11">
|
||||
<link rel="stylesheet" href="/vendor/codemirror/5.65.5/theme/material-darker.min.css?v=1.6.11">
|
||||
|
||||
<!-- app CSS -->
|
||||
<link rel="stylesheet" href="/css/styles.css?v=1.6.11">
|
||||
|
||||
<!-- Libraries (JS) -->
|
||||
<script src="/vendor/dompurify/2.4.0/purify.min.js?v=1.6.11"></script>
|
||||
<script src="/vendor/fuse/6.6.2/fuse.min.js?v=1.6.11"></script>
|
||||
<script src="/vendor/resumable/1.1.0/resumable.min.js?v=1.6.11"></script>
|
||||
|
||||
<!-- CodeMirror core FIRST, then modes -->
|
||||
<script src="/vendor/codemirror/5.65.5/codemirror.min.js?v=1.6.11"></script>
|
||||
|
||||
<script src="/js/version.js?v=1.6.11"></script>
|
||||
<script type="module" src="/js/main.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -286,9 +260,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="moveFolderBtn" class="btn btn-warning ml-2" data-i18n-title="move_folder">
|
||||
<i class="material-icons">drive_file_move</i>
|
||||
</button>
|
||||
<!-- MOVE FOLDER MODAL (place near your other folder modals) -->
|
||||
<div id="moveFolderModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h4 data-i18n-key="move_folder_title">Move Folder</h4>
|
||||
<p data-i18n-key="move_folder_message">Select a destination folder to move the current folder
|
||||
into:</p>
|
||||
<select id="moveFolderTarget" class="form-control modal-input"></select>
|
||||
<div class="modal-footer" style="margin-top:15px; text-align:right;">
|
||||
<button id="cancelMoveFolder" class="btn btn-secondary"
|
||||
data-i18n-key="cancel">Cancel</button>
|
||||
<button id="confirmMoveFolder" class="btn btn-primary" data-i18n-key="move">Move</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="renameFolderBtn" class="btn btn-warning ml-2" data-i18n-title="rename_folder">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
|
||||
<div id="renameFolderModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4 data-i18n-key="rename_folder_title">Rename Folder</h4>
|
||||
@@ -389,16 +381,14 @@
|
||||
</div>
|
||||
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled
|
||||
data-i18n-key="download_zip">Download ZIP</button>
|
||||
<button id="extractZipBtn" class="btn action-btn btn-sm btn-info" data-i18n-title="extract_zip"
|
||||
<button id="extractZipBtn" class="btn action-btn btn-sm btn-info" style="display: none;" disabled
|
||||
data-i18n-key="extract_zip_button">Extract Zip</button>
|
||||
<div id="createDropdown" class="dropdown-container" style="position:relative; display:inline-block;">
|
||||
<button id="createBtn" class="btn action-btn" data-i18n-key="create">
|
||||
${t('create')} <span class="material-icons" style="font-size:16px;vertical-align:middle;">arrow_drop_down</span>
|
||||
</button>
|
||||
<ul
|
||||
id="createMenu"
|
||||
class="dropdown-menu"
|
||||
style="
|
||||
<div id="createDropdown" class="dropdown-container" style="position:relative; display:inline-block;">
|
||||
<button id="createBtn" class="btn action-btn" style="display: none;" data-i18n-key="create">
|
||||
${t('create')} <span class="material-icons"
|
||||
style="font-size:16px;vertical-align:middle;">arrow_drop_down</span>
|
||||
</button>
|
||||
<ul id="createMenu" class="dropdown-menu" style="
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
@@ -411,27 +401,23 @@
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
min-width: 140px;
|
||||
"
|
||||
>
|
||||
<li id="createFileOption" class="dropdown-item" data-i18n-key="create_file" style="padding:8px 12px; cursor:pointer;">
|
||||
${t('create_file')}
|
||||
</li>
|
||||
<li id="createFolderOption" class="dropdown-item" data-i18n-key="create_folder" style="padding:8px 12px; cursor:pointer;">
|
||||
${t('create_folder')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
">
|
||||
<li id="createFileOption" class="dropdown-item" data-i18n-key="create_file"
|
||||
style="padding:8px 12px; cursor:pointer;">
|
||||
${t('create_file')}
|
||||
</li>
|
||||
<li id="createFolderOption" class="dropdown-item" data-i18n-key="create_folder"
|
||||
style="padding:8px 12px; cursor:pointer;">
|
||||
${t('create_folder')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Create File Modal -->
|
||||
<div id="createFileModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h4 data-i18n-key="create_new_file">Create New File</h4>
|
||||
<input
|
||||
type="text"
|
||||
id="createFileNameInput"
|
||||
class="form-control"
|
||||
placeholder="Enter filename…"
|
||||
data-i18n-placeholder="newfile_placeholder"
|
||||
/>
|
||||
<input type="text" id="createFileNameInput" class="form-control" placeholder="Enter filename…"
|
||||
data-i18n-placeholder="newfile_placeholder" />
|
||||
<div class="modal-footer" style="margin-top:1rem; text-align:right;">
|
||||
<button id="cancelCreateFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
||||
<button id="confirmCreateFile" class="btn btn-primary" data-i18n-key="create">Create</button>
|
||||
@@ -563,8 +549,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js"></script>
|
||||
<script type="module" src="js/main.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -59,7 +59,7 @@ function onShareFileToggle(row, checked) {
|
||||
}
|
||||
|
||||
function onWriteToggle(row, checked) {
|
||||
const caps = ["create","upload","edit","rename","copy","move","delete","extract"];
|
||||
const caps = ["create","upload","edit","rename","copy","delete","extract"];
|
||||
caps.forEach(c => {
|
||||
const box = qs(row, `input[data-cap="${c}"]`);
|
||||
if (box) box.checked = checked;
|
||||
@@ -683,25 +683,32 @@ function renderFolderGrantsUI(username, container, folders, grants) {
|
||||
// toolbar
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.className = 'folder-access-toolbar';
|
||||
toolbar.innerHTML = `
|
||||
<input type="text" class="form-control" style="max-width:220px;" placeholder="${tf('search_folders', 'Search folders')}" />
|
||||
<label class="muted" title="${tf('view_all_help', 'See all files in this folder (everyone’s files)')}">
|
||||
<input type="checkbox" data-bulk="view" /> ${tf('view_all', 'View (all)')}
|
||||
</label>
|
||||
<label class="muted" title="${tf('view_own_help', 'See only files you uploaded in this folder')}">
|
||||
<input type="checkbox" data-bulk="viewOwn" /> ${tf('view_own', 'View (own)')}
|
||||
</label>
|
||||
<label class="muted" title="${tf('write_help', 'Create/upload files and edit/rename/copy/delete items in this folder')}">
|
||||
<input type="checkbox" data-bulk="write" /> ${tf('write_full', 'Write (upload/edit/delete)')}
|
||||
</label>
|
||||
<label class="muted" title="${tf('manage_help', 'Owner-level: can grant access; implies View (all)/Create Folder/Rename Folder/Move Files/Share Folder')}">
|
||||
<input type="checkbox" data-bulk="manage" /> ${tf('manage', 'Manage')}
|
||||
</label>
|
||||
<label class="muted" title="${tf('share_help', 'Create/manage share links; implies View (all)')}">
|
||||
<input type="checkbox" data-bulk="share" /> ${tf('share', 'Share')}
|
||||
</label>
|
||||
<span class="muted">(${tf('applies_to_filtered', 'applies to filtered list')})</span>
|
||||
`;
|
||||
toolbar.innerHTML = `
|
||||
<input type="text" class="form-control" style="max-width:220px;"
|
||||
placeholder="${tf('search_folders', 'Search folders')}" />
|
||||
|
||||
<label class="muted" title="${tf('view_all_help', 'See all files in this folder (everyone’s files)')}">
|
||||
<input type="checkbox" data-bulk="view" /> ${tf('view_all', 'View (all)')}
|
||||
</label>
|
||||
|
||||
<label class="muted" title="${tf('view_own_help', 'See only files you uploaded in this folder')}">
|
||||
<input type="checkbox" data-bulk="viewOwn" /> ${tf('view_own', 'View (own files)')}
|
||||
</label>
|
||||
|
||||
<label class="muted" title="${tf('write_help', 'File-level: upload, edit, rename, copy, delete, extract ZIPs')}">
|
||||
<input type="checkbox" data-bulk="write" /> ${tf('write_full', 'Write (file ops)')}
|
||||
</label>
|
||||
|
||||
<label class="muted" title="${tf('manage_help', 'Folder-level (owner): can create/rename/move folders and grant access; implies View (all)')}">
|
||||
<input type="checkbox" data-bulk="manage" /> ${tf('manage', 'Manage (folder owner)')}
|
||||
</label>
|
||||
|
||||
<label class="muted" title="${tf('share_help', 'Create/manage share links; implies View (all)')}">
|
||||
<input type="checkbox" data-bulk="share" /> ${tf('share', 'Share')}
|
||||
</label>
|
||||
|
||||
<span class="muted">(${tf('applies_to_filtered', 'applies to filtered list')})</span>
|
||||
`;
|
||||
container.appendChild(toolbar);
|
||||
|
||||
const list = document.createElement('div');
|
||||
@@ -709,30 +716,55 @@ function renderFolderGrantsUI(username, container, folders, grants) {
|
||||
container.appendChild(list);
|
||||
|
||||
const headerHtml = `
|
||||
<div class="folder-access-header">
|
||||
<div class="folder-access-header">
|
||||
<div class="folder-cell" title="${tf('folder_help','Folder path within FileRise')}">
|
||||
${tf('folder','Folder')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('view_all_help', 'See all files in this folder (everyone’s files)')}">${tf('view_all', 'View (all)')}</div>
|
||||
<div class="perm-col" title="${tf('view_own_help', 'See only files you uploaded in this folder')}">${tf('view_own', 'View (own)')}</div>
|
||||
<div class="perm-col" title="${tf('write_help', 'Meta: toggles all write operations (below) on/off for this row')}">${tf('write_full', 'Write')}</div>
|
||||
<div class="perm-col" title="${tf('manage_help', 'Owner-level: can grant access; implies View (all)/Create Folder/Rename Folder/Move Files/Share Folder')}">${tf('manage', 'Manage')}</div>
|
||||
<div class="perm-col" title="${tf('create_help', 'Create empty files')}">${tf('create', 'Create')}</div>
|
||||
<div class="perm-col" title="${tf('upload_help', 'Upload files to this folder')}">${tf('upload', 'Upload')}</div>
|
||||
<div class="perm-col" title="${tf('edit_help', 'Edit file contents')}">${tf('edit', 'Edit')}</div>
|
||||
<div class="perm-col" title="${tf('rename_help', 'Rename files')}">${tf('rename', 'Rename')}</div>
|
||||
<div class="perm-col" title="${tf('copy_help', 'Copy files')}">${tf('copy', 'Copy')}</div>
|
||||
<div class="perm-col" title="${tf('move_help', 'Move files: requires Manage')}">${tf('move', 'Move')}</div>
|
||||
<div class="perm-col" title="${tf('delete_help', 'Delete files/folders')}">${tf('delete', 'Delete')}</div>
|
||||
<div class="perm-col" title="${tf('extract_help', 'Extract ZIP archives')}">${tf('extract', 'Extract ZIP')}</div>
|
||||
<div class="perm-col" title="${tf('share_file_help', 'Create share links for files')}">${tf('share_file', 'Share File')}</div>
|
||||
<div class="perm-col" title="${tf('share_folder_help', 'Create share links for folders (requires View all)')}">${tf('share_folder', 'Share Folder')}</div>
|
||||
<div class="perm-col" title="${tf('view_all_help', 'See all files in this folder (everyone’s files)')}">
|
||||
${tf('view_all', 'View (all)')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('view_own_help', 'See only files you uploaded in this folder')}">
|
||||
${tf('view_own', 'View (own)')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('write_help', 'Meta: toggles all file-level operations below')}">
|
||||
${tf('write_full', 'Write')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('manage_help', 'Folder owner: can create/rename/move folders and grant access; implies View (all)')}">
|
||||
${tf('manage', 'Manage')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('create_help', 'Create empty file')}">
|
||||
${tf('create', 'Create File')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('upload_help', 'Upload a file into this folder')}">
|
||||
${tf('upload', 'Upload File')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('edit_help', 'Edit file contents')}">
|
||||
${tf('edit', 'Edit File')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('rename_help', 'Rename a file')}">
|
||||
${tf('rename', 'Rename File')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('copy_help', 'Copy a file')}">
|
||||
${tf('copy', 'Copy File')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('delete_help', 'Delete a file')}">
|
||||
${tf('delete', 'Delete File')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('extract_help', 'Extract ZIP archives')}">
|
||||
${tf('extract', 'Extract ZIP')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('share_file_help', 'Create share links for files')}">
|
||||
${tf('share_file', 'Share File')}
|
||||
</div>
|
||||
<div class="perm-col" title="${tf('share_folder_help', 'Create share links for folders (requires Manage + View (all))')}">
|
||||
${tf('share_folder', 'Share Folder')}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
function rowHtml(folder) {
|
||||
const g = grants[folder] || {};
|
||||
const name = folder === 'root' ? '(Root)' : folder;
|
||||
const writeMetaChecked = !!(g.create || g.upload || g.edit || g.rename || g.copy || g.move || g.delete || g.extract);
|
||||
const writeMetaChecked = !!(g.create || g.upload || g.edit || g.rename || g.copy || g.delete || g.extract);
|
||||
const shareFolderDisabled = !g.view;
|
||||
return `
|
||||
<div class="folder-access-row" data-folder="${folder}">
|
||||
@@ -752,7 +784,6 @@ function renderFolderGrantsUI(username, container, folders, grants) {
|
||||
<div class="perm-col"><input type="checkbox" data-cap="edit" ${g.edit ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="rename" ${g.rename ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="copy" ${g.copy ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="move" ${g.move ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="delete" ${g.delete ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="extract" ${g.extract ? 'checked' : ''}></div>
|
||||
<div class="perm-col"><input type="checkbox" data-cap="shareFile" ${g.shareFile ? 'checked' : ''}></div>
|
||||
@@ -788,7 +819,7 @@ function renderFolderGrantsUI(username, container, folders, grants) {
|
||||
if (v) v.checked = true;
|
||||
if (w) w.checked = true;
|
||||
if (vo) { vo.checked = false; vo.disabled = true; }
|
||||
['create','upload','edit','rename','copy','move','delete','extract','shareFile','shareFolder']
|
||||
['create','upload','edit','rename','copy','delete','extract','shareFile','shareFolder']
|
||||
.forEach(c => { const cb = qs(row, `input[data-cap="${c}"]`); if (cb) cb.checked = true; });
|
||||
setRowDisabled(row, true);
|
||||
const tag = row.querySelector('.inherited-tag');
|
||||
@@ -888,7 +919,7 @@ function renderFolderGrantsUI(username, container, folders, grants) {
|
||||
const w = r.querySelector('input[data-cap="write"]');
|
||||
const vo = r.querySelector('input[data-cap="viewOwn"]');
|
||||
const boxes = [
|
||||
'create','upload','edit','rename','copy','move','delete','extract','shareFile','shareFolder'
|
||||
'create','upload','edit','rename','copy','delete','extract','shareFile','shareFolder'
|
||||
].map(c => r.querySelector(`input[data-cap="${c}"]`));
|
||||
if (m) m.checked = checked;
|
||||
if (v) v.checked = checked;
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
// ---- responsive defaults ----
|
||||
const MEDIUM_MIN = 1205; // matches your small-screen cutoff
|
||||
const MEDIUM_MAX = 1600; // tweak as you like
|
||||
const MEDIUM_MAX = 1600;
|
||||
|
||||
const TOGGLE_TOP_PX = 10;
|
||||
const TOGGLE_LEFT_PX = 100;
|
||||
const TOGGLE_TOP_PX = 8;
|
||||
const TOGGLE_LEFT_PX = 50;
|
||||
|
||||
const TOGGLE_ICON_OPEN = 'view_sidebar';
|
||||
const TOGGLE_ICON_CLOSED = 'menu';
|
||||
@@ -19,6 +19,13 @@ const KNOWN_CARD_IDS = ['uploadCard', 'folderManagementCard'];
|
||||
|
||||
const CARD_IDS = ['uploadCard', 'folderManagementCard'];
|
||||
|
||||
// --- NEW: separate user snapshot so refresh restores *manual* placements only ---
|
||||
const USER_SNAPSHOT_KEY = 'userZonesSnapshot'; // { cardId: 'sidebarDropArea'|'leftCol'|'rightCol' }
|
||||
|
||||
function hasUserSnapshot() {
|
||||
try { const s = JSON.parse(localStorage.getItem(USER_SNAPSHOT_KEY) || '{}'); return !!s && Object.keys(s).length > 0; } catch { return false; }
|
||||
}
|
||||
|
||||
function isDarkMode() {
|
||||
return document.body.classList.contains('dark-mode');
|
||||
}
|
||||
@@ -54,6 +61,16 @@ function snapshotZoneLocations() {
|
||||
localStorage.setItem('zonesSnapshot', JSON.stringify(snap));
|
||||
}
|
||||
|
||||
// NEW: Save where the user *manually* placed cards (used on normal refresh).
|
||||
function snapshotUserZones() {
|
||||
const snap = {};
|
||||
getKnownCards().forEach(card => {
|
||||
const p = card.parentNode;
|
||||
snap[card.id] = p && p.id ? p.id : '';
|
||||
});
|
||||
localStorage.setItem(USER_SNAPSHOT_KEY, JSON.stringify(snap));
|
||||
}
|
||||
|
||||
// Move a card to default expanded spot (your request: sidebar is default).
|
||||
function moveCardToSidebarDefault(card) {
|
||||
const sidebar = getSidebar();
|
||||
@@ -75,6 +92,56 @@ function stripHeaderArtifacts(card) {
|
||||
}
|
||||
}
|
||||
|
||||
// Kill the 50px ghost gutter when the sidebar isn't participating in layout.
|
||||
function clampSidebarWhenEmpty() {
|
||||
const sidebar = getSidebar();
|
||||
if (!sidebar) return;
|
||||
|
||||
const sidebarHasCards = hasSidebarCards();
|
||||
const collapsed = isZonesCollapsed();
|
||||
|
||||
// Sidebar should not take space if it's collapsed OR has no cards.
|
||||
const shouldHideSidebarSpace = collapsed || !sidebarHasCards;
|
||||
|
||||
if (shouldHideSidebarSpace) {
|
||||
// Make sure it takes absolutely no horizontal space.
|
||||
sidebar.style.display = 'none'; // don't render at all
|
||||
sidebar.style.width = '0px';
|
||||
sidebar.style.minWidth = '0px';
|
||||
sidebar.style.margin = '0';
|
||||
sidebar.style.padding = '0';
|
||||
sidebar.style.flex = '0 0 0px';
|
||||
|
||||
// if you add/remove highlight elsewhere, also ensure it's not forcing size
|
||||
sidebar.classList.remove('active');
|
||||
} else {
|
||||
// Let your CSS control it when it's actually visible/has cards.
|
||||
sidebar.style.width = '';
|
||||
sidebar.style.minWidth = '';
|
||||
sidebar.style.margin = '';
|
||||
sidebar.style.padding = '';
|
||||
sidebar.style.flex = '';
|
||||
// display is already decided by updateSidebarVisibility/applyZonesCollapsed
|
||||
}
|
||||
}
|
||||
|
||||
// Let the sidebar become a real drop target during drag, even if empty.
|
||||
function unclampSidebarForDrag() {
|
||||
const sidebar = getSidebar();
|
||||
if (!sidebar) return;
|
||||
// only un-clamp if panels are not collapsed
|
||||
if (!isZonesCollapsed()) {
|
||||
sidebar.style.display = 'block';
|
||||
// give it a sensible min width so the highlight looks right
|
||||
sidebar.style.minWidth = '280px';
|
||||
// never force a fixed height here; let CSS layout handle it
|
||||
sidebar.style.width = '';
|
||||
sidebar.style.flex = ''; // don't lock flex while dragging
|
||||
sidebar.style.margin = '';
|
||||
sidebar.style.padding = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Restore cards after “expand” (toggle off) or after refresh.
|
||||
// - If we have a snapshot, use it.
|
||||
// - If not, put all cards in the sidebar (your default).
|
||||
@@ -152,8 +219,10 @@ function removeHeaderIconForCard(card) {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply *user* snapshot on normal load (not expand) to preserve manual placement.
|
||||
function applySnapshotIfPresent() {
|
||||
const snap = readZonesSnapshot();
|
||||
let snap = {};
|
||||
try { snap = JSON.parse(localStorage.getItem(USER_SNAPSHOT_KEY) || '{}'); } catch { snap = {}; }
|
||||
const keys = Object.keys(snap || {});
|
||||
if (!keys.length) return false;
|
||||
|
||||
@@ -231,21 +300,39 @@ function moveAllSidebarCardsToTop() {
|
||||
}
|
||||
|
||||
// New: enforce responsive behavior (sidebar disabled on small screens)
|
||||
// Add hysteresis to avoid flapping near threshold
|
||||
let __lastIsSmall = null;
|
||||
let __lastWidth = null;
|
||||
const SMALL_ENTER = MEDIUM_MIN - 16; // enter small below this
|
||||
const SMALL_EXIT = MEDIUM_MIN + 16; // leave small above this
|
||||
|
||||
function enforceResponsiveZones() {
|
||||
const isSmall = isSmallScreen();
|
||||
const w = window.innerWidth;
|
||||
const prevSmall = __lastIsSmall;
|
||||
let nowSmall;
|
||||
|
||||
if (__lastWidth == null) {
|
||||
nowSmall = w < MEDIUM_MIN;
|
||||
} else if (prevSmall === true) {
|
||||
nowSmall = !(w >= SMALL_EXIT);
|
||||
} else if (prevSmall === false) {
|
||||
nowSmall = (w < SMALL_ENTER);
|
||||
} else {
|
||||
nowSmall = w < MEDIUM_MIN;
|
||||
}
|
||||
__lastWidth = w;
|
||||
|
||||
const sidebar = getSidebar();
|
||||
const topZone = getTopZone();
|
||||
|
||||
if (isSmall && __lastIsSmall !== true) {
|
||||
if (nowSmall && prevSmall !== true) {
|
||||
// entering small: remember what was in sidebar, move them up, hide sidebar
|
||||
snapshotSidebarCardsForResponsive();
|
||||
moveAllSidebarCardsToTop();
|
||||
if (sidebar) sidebar.style.display = 'none';
|
||||
if (topZone) topZone.style.display = ''; // ensure visible
|
||||
__lastIsSmall = true;
|
||||
} else if (!isSmall && __lastIsSmall !== false) {
|
||||
} else if (!nowSmall && prevSmall !== false) {
|
||||
// leaving small: restore only what used to be in the sidebar
|
||||
const ids = readResponsiveSnapshot();
|
||||
const sb = getSidebar();
|
||||
@@ -309,10 +396,10 @@ function setZonesCollapsed(collapsed) {
|
||||
|
||||
if (collapsed) {
|
||||
// Remember where cards were, then show them as header icons
|
||||
snapshotZoneLocations();
|
||||
collapseCardsToHeader(); // your existing helper that calls insertCardInHeader(...)
|
||||
snapshotZoneLocations(); // original snapshot used only for collapse/expand
|
||||
collapseCardsToHeader(); // your existing helper that calls insertCardInHeader(...)
|
||||
} else {
|
||||
// Expand: bring cards back
|
||||
// Expand: bring cards back (from the collapse snapshot)
|
||||
restoreCardsFromSnapshot();
|
||||
|
||||
// Ensure zones are visible right away after expand
|
||||
@@ -324,6 +411,7 @@ function setZonesCollapsed(collapsed) {
|
||||
|
||||
ensureZonesToggle();
|
||||
updateZonesToggleUI();
|
||||
clampSidebarWhenEmpty();
|
||||
}
|
||||
|
||||
function applyZonesCollapsed() {
|
||||
@@ -394,18 +482,25 @@ function mountHeaderToggle(btn) {
|
||||
|
||||
Object.assign(btn.style, {
|
||||
position: 'absolute',
|
||||
left: '100px', // adjust position beside the logo
|
||||
top: '10px',
|
||||
left: TOGGLE_LEFT_PX, // adjust position beside the logo
|
||||
top: TOGGLE_TOP_PX,
|
||||
zIndex: '10010',
|
||||
pointerEvents: 'auto'
|
||||
});
|
||||
}
|
||||
|
||||
function ensureZonesToggle() {
|
||||
const isAuthed = document.body.classList.contains('authenticated');
|
||||
let btn = document.getElementById('sidebarToggleFloating');
|
||||
const host = getHeaderHost();
|
||||
if (!host) return;
|
||||
|
||||
// If not authenticated, make sure the button is gone and bail.
|
||||
if (!isAuthed) {
|
||||
if (btn) btn.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure the host is a positioning context
|
||||
const hostStyle = getComputedStyle(host);
|
||||
if (hostStyle.position === 'static') {
|
||||
@@ -414,24 +509,25 @@ function ensureZonesToggle() {
|
||||
|
||||
if (!btn) {
|
||||
btn = document.createElement('button');
|
||||
|
||||
btn.id = 'sidebarToggleFloating';
|
||||
btn.type = 'button'; // not a submit
|
||||
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.type = 'button';
|
||||
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, {
|
||||
position: 'absolute', // <-- key change (was fixed)
|
||||
top: '8px', // adjust to line up with header content
|
||||
left: '100px', // place to the right of your logo; tweak as needed
|
||||
position: 'absolute',
|
||||
top: '8px',
|
||||
left: '65px',
|
||||
zIndex: '1000',
|
||||
width: '38px',
|
||||
height: '38px',
|
||||
@@ -447,7 +543,7 @@ btn.addEventListener('click', (e) => {
|
||||
lineHeight: '0'
|
||||
});
|
||||
|
||||
// dark-mode polish (optional)
|
||||
// Dark mode polish
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
btn.style.background = '#2c2c2c';
|
||||
btn.style.border = '1px solid #555';
|
||||
@@ -459,13 +555,14 @@ btn.addEventListener('click', (e) => {
|
||||
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');
|
||||
if (afterLogo && afterLogo.parentNode) {
|
||||
afterLogo.parentNode.insertBefore(btn, afterLogo.nextSibling);
|
||||
} else {
|
||||
host.appendChild(btn);
|
||||
}
|
||||
|
||||
themeToggleButton(btn);
|
||||
}
|
||||
|
||||
@@ -570,6 +667,7 @@ export function loadSidebarOrder() {
|
||||
const orderStr = localStorage.getItem('sidebarOrder');
|
||||
const headerOrderStr = localStorage.getItem('headerOrder');
|
||||
|
||||
// Restore user's last manual placement on normal load
|
||||
if (applySnapshotIfPresent()) {
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
@@ -577,6 +675,23 @@ export function loadSidebarOrder() {
|
||||
ensureZonesToggle();
|
||||
return;
|
||||
}
|
||||
// If no user snapshot exists and we're not on small screens,
|
||||
// but the cards are currently in the top zone, default them to the sidebar once.
|
||||
if (!hasUserSnapshot() && !isSmallScreen() && hasTopZoneCards() && !hasSidebarCards()) {
|
||||
const sb = getSidebar();
|
||||
if (sb) {
|
||||
['uploadCard','folderManagementCard'].forEach(id => {
|
||||
const c = document.getElementById(id);
|
||||
if (c && !sb.contains(c)) {
|
||||
sb.appendChild(c);
|
||||
c.style.width = '100%';
|
||||
}
|
||||
});
|
||||
snapshotUserZones(); // persist this as the user's baseline
|
||||
updateSidebarVisibility();
|
||||
updateTopZoneLayout();
|
||||
}
|
||||
}
|
||||
|
||||
// Only apply the one-time default if *not* initialized yet
|
||||
if (!defaultAlready &&
|
||||
@@ -692,6 +807,7 @@ function updateSidebarVisibility() {
|
||||
localStorage.setItem('layoutDefaultApplied_v1', '1');
|
||||
|
||||
ensureZonesToggle();
|
||||
clampSidebarWhenEmpty();
|
||||
}
|
||||
|
||||
// NEW: Save header order to localStorage.
|
||||
@@ -733,6 +849,7 @@ function updateTopZoneLayout() {
|
||||
|
||||
// hide whole top row when empty (kills the gap)
|
||||
if (topZone) topZone.style.display = (hasUpload || hasFolder) ? '' : 'none';
|
||||
clampSidebarWhenEmpty();
|
||||
}
|
||||
|
||||
// When a card is being dragged, if the top drop zone is empty, set its min-height.
|
||||
@@ -799,6 +916,10 @@ function insertCardInSidebar(card, event) {
|
||||
|
||||
// SAVE order & refresh minimal UI, but DO NOT collapse/restore here:
|
||||
saveSidebarOrder();
|
||||
|
||||
// NEW: persist user manual placement (used on normal refresh)
|
||||
snapshotUserZones();
|
||||
|
||||
updateSidebarVisibility();
|
||||
ensureZonesToggle();
|
||||
updateZonesToggleUI();
|
||||
@@ -1081,13 +1202,15 @@ export function initDragAndDrop() {
|
||||
showTopZoneWhileDragging();
|
||||
|
||||
const sidebar = getSidebar();
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
|
||||
sidebar.classList.add('highlight');
|
||||
sidebar.style.height = '800px';
|
||||
sidebar.style.minWidth = '280px';
|
||||
}
|
||||
if (sidebar) {
|
||||
unclampSidebarForDrag(); // <— NEW
|
||||
sidebar.classList.add('active');
|
||||
sidebar.classList.add('highlight');
|
||||
// keep it visible, but don't force a fixed height
|
||||
sidebar.style.removeProperty('height'); // <— no 800px box
|
||||
// ensure it's actually visible if not collapsed
|
||||
if (!isZonesCollapsed()) sidebar.style.display = 'block';
|
||||
}
|
||||
|
||||
showHeaderDropZone();
|
||||
const topZone = getTopZone();
|
||||
@@ -1169,7 +1292,6 @@ export function initDragAndDrop() {
|
||||
const sidebarElem = getSidebar();
|
||||
if (sidebarElem) {
|
||||
const rect = sidebarElem.getBoundingClientRect();
|
||||
const dropZoneBottom = rect.top + 800; // Virtual drop zone height.
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
@@ -1208,6 +1330,9 @@ export function initDragAndDrop() {
|
||||
setTimeout(() => {
|
||||
card.style.removeProperty('width');
|
||||
}, 210);
|
||||
|
||||
// NEW: persist user manual placement
|
||||
snapshotUserZones();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1224,6 +1349,7 @@ export function initDragAndDrop() {
|
||||
) {
|
||||
insertCardInHeader(card, e);
|
||||
droppedInHeader = true;
|
||||
// header mode is transient; do not overwrite userZones here
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1257,10 +1383,16 @@ export function initDragAndDrop() {
|
||||
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
clampSidebarWhenEmpty();
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('highlight');
|
||||
sidebar.style.height = '';
|
||||
sidebar.style.minWidth = '';
|
||||
}
|
||||
hideHeaderDropZone();
|
||||
|
||||
cleanupTopZoneAfterDrop();
|
||||
snapshotZoneLocations();
|
||||
snapshotZoneLocations(); // keep original (collapse/expand)
|
||||
const tz = getTopZone();
|
||||
if (tz) tz.style.minHeight = '';
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ const EDITOR_PLAIN_THRESHOLD = 5 * 1024 * 1024; // >5 MiB => force plain text,
|
||||
const EDITOR_BLOCK_THRESHOLD = 10 * 1024 * 1024; // >10 MiB => block editing
|
||||
|
||||
// Lazy-load CodeMirror modes on demand
|
||||
const CM_CDN = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/";
|
||||
//const CM_CDN = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/";
|
||||
const CM_LOCAL = "/vendor/codemirror/5.65.5/";
|
||||
|
||||
// Which mode file to load for a given name/mime
|
||||
const MODE_URL = {
|
||||
@@ -48,50 +49,27 @@ function normalizeModeName(modeOption) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const MODE_SRI = {
|
||||
"mode/xml/xml.min.js": "sha512-LarNmzVokUmcA7aUDtqZ6oTS+YXmUKzpGdm8DxC46A6AHu+PQiYCUlwEGWidjVYMo/QXZMFMIadZtrkfApYp/g==",
|
||||
"mode/css/css.min.js": "sha512-oikhYLgIKf0zWtVTOXh101BWoSacgv4UTJHQOHU+iUQ1Dol3Xjz/o9Jh0U33MPoT/d4aQruvjNvcYxvkTQd0nA==",
|
||||
"mode/javascript/javascript.min.js": "sha512-I6CdJdruzGtvDyvdO4YsiAq+pkWf2efgd1ZUSK2FnM/u2VuRASPC7GowWQrWyjxCZn6CT89s3ddGI+be0Ak9Fg==",
|
||||
"mode/htmlmixed/htmlmixed.min.js": "sha512-HN6cn6mIWeFJFwRN9yetDAMSh+AK9myHF1X9GlSlKmThaat65342Yw8wL7ITuaJnPioG0SYG09gy0qd5+s777w==",
|
||||
"mode/php/php.min.js": "sha512-jZGz5n9AVTuQGhKTL0QzOm6bxxIQjaSbins+vD3OIdI7mtnmYE6h/L+UBGIp/SssLggbkxRzp9XkQNA4AyjFBw==",
|
||||
"mode/markdown/markdown.min.js": "sha512-DmMao0nRIbyDjbaHc8fNd3kxGsZj9PCU6Iu/CeidLQT9Py8nYVA5n0PqXYmvqNdU+lCiTHOM/4E7bM/G8BttJg==",
|
||||
"mode/python/python.min.js": "sha512-2M0GdbU5OxkGYMhakED69bw0c1pW3Nb0PeF3+9d+SnwN1ryPx3wiDdNqK3gSM7KAU/pEV+2tFJFbMKjKAahOkQ==",
|
||||
"mode/sql/sql.min.js": "sha512-u8r8NUnG9B9L2dDmsfvs9ohQ0SO/Z7MB8bkdLxV7fE0Q8bOeP7/qft1D4KyE8HhVrpH3ihSrRoDiMbYR1VQBWQ==",
|
||||
"mode/shell/shell.min.js": "sha512-HoC6JXgjHHevWAYqww37Gfu2c1G7SxAOv42wOakjR8csbTUfTB7OhVzSJ95LL62nII0RCyImp+7nR9zGmJ1wRQ==",
|
||||
"mode/yaml/yaml.min.js": "sha512-+aXDZ93WyextRiAZpsRuJyiAZ38ztttUyO/H3FZx4gOAOv4/k9C6Um1CvHVtaowHZ2h7kH0d+orWvdBLPVwb4g==",
|
||||
"mode/properties/properties.min.js": "sha512-P4OaO+QWj1wPRsdkEHlrgkx+a7qp6nUC8rI6dS/0/HPjHtlEmYfiambxowYa/UfqTxyNUnwTyPt5U6l1GO76yw==",
|
||||
"mode/clike/clike.min.js": "sha512-l8ZIWnQ3XHPRG3MQ8+hT1OffRSTrFwrph1j1oc1Fzc9UKVGef5XN9fdO0vm3nW0PRgQ9LJgck6ciG59m69rvfg=="
|
||||
};
|
||||
|
||||
const MODE_LOAD_TIMEOUT_MS = 2500; // allow closing immediately; don't wait forever
|
||||
|
||||
function loadScriptOnce(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const key = `cm:${url}`;
|
||||
const ver = (window.APP_VERSION ?? 'dev').replace(/^v/, ''); // "v1.6.9" -> "1.6.9"
|
||||
const withQS = url + '?v=' + ver;
|
||||
|
||||
const key = `cm:${withQS}`;
|
||||
let s = document.querySelector(`script[data-key="${key}"]`);
|
||||
if (s) {
|
||||
if (s.dataset.loaded === "1") return resolve();
|
||||
s.addEventListener("load", () => resolve());
|
||||
s.addEventListener("error", () => reject(new Error(`Load failed: ${url}`)));
|
||||
s.addEventListener("load", resolve);
|
||||
s.addEventListener("error", () => reject(new Error(`Load failed: ${withQS}`)));
|
||||
return;
|
||||
}
|
||||
s = document.createElement("script");
|
||||
s.src = url;
|
||||
s.src = withQS;
|
||||
s.async = true;
|
||||
s.dataset.key = key;
|
||||
|
||||
// 🔒 Add SRI if we have it
|
||||
const relPath = url.replace(/^https:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/codemirror\/5\.65\.5\//, "");
|
||||
const sri = MODE_SRI[relPath];
|
||||
if (sri) {
|
||||
s.integrity = sri;
|
||||
s.crossOrigin = "anonymous";
|
||||
// (Optional) further tighten referrer behavior:
|
||||
// s.referrerPolicy = "no-referrer";
|
||||
}
|
||||
|
||||
s.addEventListener("load", () => { s.dataset.loaded = "1"; resolve(); });
|
||||
s.addEventListener("error", () => reject(new Error(`Load failed: ${url}`)));
|
||||
s.addEventListener("error", () => reject(new Error(`Load failed: ${withQS}`)));
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
@@ -124,7 +102,7 @@ async function ensureModeLoaded(modeOption) {
|
||||
await ensureModeLoaded("htmlmixed");
|
||||
}
|
||||
|
||||
await loadScriptOnce(CM_CDN + url);
|
||||
await loadScriptOnce(CM_LOCAL + url);
|
||||
}
|
||||
|
||||
function getModeForFile(fileName) {
|
||||
|
||||
@@ -103,6 +103,7 @@ async function applyFolderCapabilities(folder) {
|
||||
|
||||
const isRoot = (folder === 'root');
|
||||
setControlEnabled(document.getElementById('createFolderBtn'), !!caps.canCreate);
|
||||
setControlEnabled(document.getElementById('moveFolderBtn'), !!caps.canMoveFolder);
|
||||
setControlEnabled(document.getElementById('renameFolderBtn'), !isRoot && !!caps.canRename);
|
||||
setControlEnabled(document.getElementById('deleteFolderBtn'), !isRoot && !!caps.canDelete);
|
||||
setControlEnabled(document.getElementById('shareFolderBtn'), !isRoot && !!caps.canShareFolder);
|
||||
@@ -180,6 +181,49 @@ function breadcrumbDropHandler(e) {
|
||||
console.error("Invalid drag data on breadcrumb:", err);
|
||||
return;
|
||||
}
|
||||
/* FOLDER MOVE FALLBACK */
|
||||
if (!dragData) {
|
||||
const plain = (event.dataTransfer && event.dataTransfer.getData("application/x-filerise-folder")) ||
|
||||
(event.dataTransfer && event.dataTransfer.getData("text/plain")) || "";
|
||||
if (plain) {
|
||||
const sourceFolder = String(plain).trim();
|
||||
if (sourceFolder && sourceFolder !== "root") {
|
||||
if (dropFolder === sourceFolder || (dropFolder + "/").startsWith(sourceFolder + "/")) {
|
||||
showToast("Invalid destination.", 4000);
|
||||
return;
|
||||
}
|
||||
fetchWithCsrf("/api/folder/moveFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ source: sourceFolder, destination: dropFolder })
|
||||
})
|
||||
.then(safeJson)
|
||||
.then(data => {
|
||||
if (data && !data.error) {
|
||||
showToast(`Folder moved to ${dropFolder}!`);
|
||||
if (window.currentFolder && (window.currentFolder === sourceFolder || window.currentFolder.startsWith(sourceFolder + "/"))) {
|
||||
const base = sourceFolder.split("/").pop();
|
||||
const newPath = (dropFolder === "root" ? "" : dropFolder + "/") + base;
|
||||
window.currentFolder = newPath;
|
||||
}
|
||||
return loadFolderTree().then(() => {
|
||||
try { expandTreePath(window.currentFolder || "root"); } catch (_) {}
|
||||
loadFileList(window.currentFolder || "root");
|
||||
});
|
||||
} else {
|
||||
showToast("Error: " + (data && data.error || "Could not move folder"), 5000);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error moving folder:", err);
|
||||
showToast("Error moving folder", 5000);
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
||||
if (filesToMove.length === 0) return;
|
||||
|
||||
@@ -262,7 +306,7 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
||||
} else {
|
||||
html += `<span class="folder-indent-placeholder"></span>`;
|
||||
}
|
||||
html += `<span class="folder-option" data-folder="${fullPath}">${escapeHTML(folder)}</span>`;
|
||||
html += `<span class="folder-option" draggable="true" data-folder="${fullPath}">${escapeHTML(folder)}</span>`;
|
||||
if (hasChildren) {
|
||||
html += renderFolderTree(tree[folder], fullPath, displayState);
|
||||
}
|
||||
@@ -312,13 +356,58 @@ function folderDropHandler(event) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.classList.remove("drop-hover");
|
||||
const dropFolder = event.currentTarget.getAttribute("data-folder");
|
||||
let dragData;
|
||||
let dragData = null;
|
||||
try {
|
||||
dragData = JSON.parse(event.dataTransfer.getData("application/json"));
|
||||
} catch (e) {
|
||||
const jsonStr = event.dataTransfer.getData("application/json") || "";
|
||||
if (jsonStr) dragData = JSON.parse(jsonStr);
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Invalid drag data", e);
|
||||
return;
|
||||
}
|
||||
/* FOLDER MOVE FALLBACK */
|
||||
if (!dragData) {
|
||||
const plain = (event.dataTransfer && event.dataTransfer.getData("application/x-filerise-folder")) ||
|
||||
(event.dataTransfer && event.dataTransfer.getData("text/plain")) || "";
|
||||
if (plain) {
|
||||
const sourceFolder = String(plain).trim();
|
||||
if (sourceFolder && sourceFolder !== "root") {
|
||||
if (dropFolder === sourceFolder || (dropFolder + "/").startsWith(sourceFolder + "/")) {
|
||||
showToast("Invalid destination.", 4000);
|
||||
return;
|
||||
}
|
||||
fetchWithCsrf("/api/folder/moveFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ source: sourceFolder, destination: dropFolder })
|
||||
})
|
||||
.then(safeJson)
|
||||
.then(data => {
|
||||
if (data && !data.error) {
|
||||
showToast(`Folder moved to ${dropFolder}!`);
|
||||
if (window.currentFolder && (window.currentFolder === sourceFolder || window.currentFolder.startsWith(sourceFolder + "/"))) {
|
||||
const base = sourceFolder.split("/").pop();
|
||||
const newPath = (dropFolder === "root" ? "" : dropFolder + "/") + base;
|
||||
window.currentFolder = newPath;
|
||||
}
|
||||
return loadFolderTree().then(() => {
|
||||
try { expandTreePath(window.currentFolder || "root"); } catch (_) {}
|
||||
loadFileList(window.currentFolder || "root");
|
||||
});
|
||||
} else {
|
||||
showToast("Error: " + (data && data.error || "Could not move folder"), 5000);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error moving folder:", err);
|
||||
showToast("Error moving folder", 5000);
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
||||
if (filesToMove.length === 0) return;
|
||||
|
||||
@@ -459,6 +548,14 @@ export async function loadFolderTree(selectedFolder) {
|
||||
|
||||
// Attach drag/drop event listeners.
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
// Provide folder path payload for folder->folder DnD
|
||||
el.addEventListener("dragstart", (ev) => {
|
||||
const src = el.getAttribute("data-folder");
|
||||
try { ev.dataTransfer.setData("application/x-filerise-folder", src); } catch (e) {}
|
||||
try { ev.dataTransfer.setData("text/plain", src); } catch (e) {}
|
||||
ev.dataTransfer.effectAllowed = "move";
|
||||
});
|
||||
|
||||
el.addEventListener("dragover", folderDragOverHandler);
|
||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||
el.addEventListener("drop", folderDropHandler);
|
||||
@@ -487,6 +584,14 @@ export async function loadFolderTree(selectedFolder) {
|
||||
|
||||
// Folder-option click: update selection, breadcrumbs, and file list
|
||||
container.querySelectorAll(".folder-option").forEach(el => {
|
||||
// Provide folder path payload for folder->folder DnD
|
||||
el.addEventListener("dragstart", (ev) => {
|
||||
const src = el.getAttribute("data-folder");
|
||||
try { ev.dataTransfer.setData("application/x-filerise-folder", src); } catch (e) {}
|
||||
try { ev.dataTransfer.setData("text/plain", src); } catch (e) {}
|
||||
ev.dataTransfer.effectAllowed = "move";
|
||||
});
|
||||
|
||||
el.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||
@@ -642,6 +747,44 @@ if (submitRename) {
|
||||
});
|
||||
}
|
||||
|
||||
// === Move Folder Modal helper (shared by button + context menu) ===
|
||||
function openMoveFolderUI(sourceFolder) {
|
||||
const modal = document.getElementById('moveFolderModal');
|
||||
const targetSel = document.getElementById('moveFolderTarget');
|
||||
|
||||
// If you right-clicked a different folder than currently selected, use that
|
||||
if (sourceFolder && sourceFolder !== 'root') {
|
||||
window.currentFolder = sourceFolder;
|
||||
}
|
||||
|
||||
// Fill target dropdown
|
||||
if (targetSel) {
|
||||
targetSel.innerHTML = '';
|
||||
fetch('/api/folder/getFolderList.php', { credentials: 'include' })
|
||||
.then(r => r.json())
|
||||
.then(list => {
|
||||
if (Array.isArray(list) && list.length && typeof list[0] === 'object' && list[0].folder) {
|
||||
list = list.map(it => it.folder);
|
||||
}
|
||||
// Root option
|
||||
const rootOpt = document.createElement('option');
|
||||
rootOpt.value = 'root'; rootOpt.textContent = '(Root)';
|
||||
targetSel.appendChild(rootOpt);
|
||||
|
||||
(list || [])
|
||||
.filter(f => f && f !== 'trash' && f !== (window.currentFolder || ''))
|
||||
.forEach(f => {
|
||||
const o = document.createElement('option');
|
||||
o.value = f; o.textContent = f;
|
||||
targetSel.appendChild(o);
|
||||
});
|
||||
})
|
||||
.catch(()=>{ /* no-op */ });
|
||||
}
|
||||
|
||||
if (modal) modal.style.display = 'block';
|
||||
}
|
||||
|
||||
export function openDeleteFolderModal() {
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
@@ -841,6 +984,10 @@ function folderManagerContextMenuHandler(e) {
|
||||
if (input) input.focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t("move_folder"),
|
||||
action: () => { openMoveFolderUI(folder); }
|
||||
},
|
||||
{
|
||||
label: t("rename_folder"),
|
||||
action: () => { openRenameFolderModal(); }
|
||||
@@ -923,4 +1070,53 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
|
||||
// Initial context menu delegation bind
|
||||
bindFolderManagerContextMenu();
|
||||
bindFolderManagerContextMenu();
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const moveBtn = document.getElementById('moveFolderBtn');
|
||||
const modal = document.getElementById('moveFolderModal');
|
||||
const targetSel = document.getElementById('moveFolderTarget');
|
||||
const cancelBtn = document.getElementById('cancelMoveFolder');
|
||||
const confirmBtn= document.getElementById('confirmMoveFolder');
|
||||
|
||||
if (moveBtn) {
|
||||
moveBtn.addEventListener('click', () => {
|
||||
const cf = window.currentFolder || 'root';
|
||||
if (!cf || cf === 'root') { showToast('Select a non-root folder to move.'); return; }
|
||||
openMoveFolderUI(cf);
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelBtn) cancelBtn.addEventListener('click', () => { if (modal) modal.style.display = 'none'; });
|
||||
|
||||
if (confirmBtn) confirmBtn.addEventListener('click', async () => {
|
||||
if (!targetSel) return;
|
||||
const destination = targetSel.value;
|
||||
const source = window.currentFolder;
|
||||
|
||||
if (!destination) { showToast('Pick a destination'); return; }
|
||||
if (destination === source || (destination + '/').startsWith(source + '/')) {
|
||||
showToast('Invalid destination'); return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/folder/moveFolder.php', {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.csrfToken },
|
||||
body: JSON.stringify({ source, destination })
|
||||
});
|
||||
const data = await safeJson(res);
|
||||
if (res.ok && data && !data.error) {
|
||||
showToast('Folder moved');
|
||||
if (modal) modal.style.display='none';
|
||||
await loadFolderTree();
|
||||
const base = source.split('/').pop();
|
||||
const newPath = (destination === 'root' ? '' : destination + '/') + base;
|
||||
window.currentFolder = newPath;
|
||||
loadFileList(window.currentFolder || 'root');
|
||||
} else {
|
||||
showToast('Error: ' + (data && data.error || 'Move failed'));
|
||||
}
|
||||
} catch (e) { console.error(e); showToast('Move failed'); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,7 +282,27 @@ const translations = {
|
||||
"bypass_ownership": "Bypass Ownership",
|
||||
"error_loading_user_grants": "Error loading user grants",
|
||||
"click_to_edit": "Click to edit",
|
||||
"folder_access": "Folder Access"
|
||||
"folder_access": "Folder Access",
|
||||
"move_folder": "Move Folder",
|
||||
"move_folder_message": "Select a destination folder to move this folder to:",
|
||||
"move_folder_title": "Move this folder",
|
||||
"move_folder_success": "Folder moved successfully.",
|
||||
"move_folder_error": "Error moving folder.",
|
||||
"move_folder_invalid": "Invalid source or destination folder.",
|
||||
"move_folder_denied": "You do not have permission to move this folder.",
|
||||
"move_folder_same_dest": "Destination cannot be the source or one of its subfolders.",
|
||||
"move_folder_same_owner": "Source and destination must have the same owner.",
|
||||
"move_folder_confirm": "Are you sure you want to move this folder?",
|
||||
"move_folder_select_dest": "Select a destination folder",
|
||||
"move_folder_select_dest_help": "Choose where this folder should be moved to.",
|
||||
"acl_move_folder_label": "Move Folder (source)",
|
||||
"acl_move_folder_help": "Allows moving this folder to a different parent. Requires Manage or Ownership on the folder.",
|
||||
"acl_move_in_label": "Allow Moves Into This Folder (destination)",
|
||||
"acl_move_in_help": "Allows items or folders from elsewhere to be moved into this folder. Requires Manage on the destination folder.",
|
||||
"acl_move_folder_info": "Moving folders is restricted to folder owners or managers. Destination folders must also allow moves in.",
|
||||
"context_move_folder": "Move Folder...",
|
||||
"context_move_here": "Move Here",
|
||||
"context_move_cancel": "Cancel Move"
|
||||
},
|
||||
es: {
|
||||
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
||||
|
||||
@@ -105,12 +105,14 @@ export function initializeApp() {
|
||||
const saved = parseInt(localStorage.getItem('rowHeight') || '48', 10);
|
||||
document.documentElement.style.setProperty('--file-row-height', saved + 'px');
|
||||
|
||||
window.currentFolder = "root";
|
||||
//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);
|
||||
//loadFileList(window.currentFolder);
|
||||
|
||||
const fileListArea = document.getElementById('fileListContainer');
|
||||
const uploadArea = document.getElementById('uploadDropArea');
|
||||
@@ -202,8 +204,11 @@ export function triggerLogout() {
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": getCsrfToken() }
|
||||
})
|
||||
.then(() => window.location.reload(true))
|
||||
.catch(() => { });
|
||||
.then(() => {
|
||||
document.body.classList.remove('authenticated');
|
||||
window.location.reload(true);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// Expose functions for inline handlers.
|
||||
@@ -237,9 +242,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// 3) If authenticated, start app
|
||||
checkAuthentication().then(authenticated => {
|
||||
if (authenticated) {
|
||||
document.body.classList.add('authenticated');
|
||||
const overlay = document.getElementById('loadingOverlay');
|
||||
if (overlay) overlay.remove();
|
||||
initializeApp();
|
||||
} else {
|
||||
document.body.classList.remove('authenticated');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -161,91 +161,91 @@ function createFileEntry(file) {
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.classList.add("remove-file-btn");
|
||||
removeBtn.textContent = "×";
|
||||
// In your remove button event listener, replace the fetch call with:
|
||||
removeBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
const uploadIndex = file.uploadIndex;
|
||||
window.selectedFiles = window.selectedFiles.filter(f => f.uploadIndex !== uploadIndex);
|
||||
|
||||
// Cancel the file upload if possible.
|
||||
if (typeof file.cancel === "function") {
|
||||
file.cancel();
|
||||
console.log("Canceled file upload:", file.fileName);
|
||||
}
|
||||
|
||||
// Remove file from the resumable queue.
|
||||
if (resumableInstance && typeof resumableInstance.removeFile === "function") {
|
||||
resumableInstance.removeFile(file);
|
||||
}
|
||||
|
||||
// Call our helper repeatedly to remove the chunk folder.
|
||||
if (file.uniqueIdentifier) {
|
||||
removeChunkFolderRepeatedly(file.uniqueIdentifier, window.csrfToken, 3, 1000);
|
||||
}
|
||||
|
||||
li.remove();
|
||||
updateFileInfoCount();
|
||||
});
|
||||
// In your remove button event listener, replace the fetch call with:
|
||||
removeBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
const uploadIndex = file.uploadIndex;
|
||||
window.selectedFiles = window.selectedFiles.filter(f => f.uploadIndex !== uploadIndex);
|
||||
|
||||
// Cancel the file upload if possible.
|
||||
if (typeof file.cancel === "function") {
|
||||
file.cancel();
|
||||
console.log("Canceled file upload:", file.fileName);
|
||||
}
|
||||
|
||||
// Remove file from the resumable queue.
|
||||
if (resumableInstance && typeof resumableInstance.removeFile === "function") {
|
||||
resumableInstance.removeFile(file);
|
||||
}
|
||||
|
||||
// Call our helper repeatedly to remove the chunk folder.
|
||||
if (file.uniqueIdentifier) {
|
||||
removeChunkFolderRepeatedly(file.uniqueIdentifier, window.csrfToken, 3, 1000);
|
||||
}
|
||||
|
||||
li.remove();
|
||||
updateFileInfoCount();
|
||||
});
|
||||
li.removeBtn = removeBtn;
|
||||
li.appendChild(removeBtn);
|
||||
|
||||
// Add pause/resume/restart button if the file supports pause/resume.
|
||||
// Conditionally add the pause/resume button only if file.pause is available
|
||||
// Pause/Resume button (for resumable file–picker uploads)
|
||||
if (typeof file.pause === "function") {
|
||||
const pauseResumeBtn = document.createElement("button");
|
||||
pauseResumeBtn.setAttribute("type", "button"); // not a submit button
|
||||
pauseResumeBtn.classList.add("pause-resume-btn");
|
||||
// Start with pause icon and disable button until upload starts
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
|
||||
pauseResumeBtn.disabled = true;
|
||||
pauseResumeBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
if (file.isError) {
|
||||
// If the file previously failed, try restarting upload.
|
||||
if (typeof file.retry === "function") {
|
||||
file.retry();
|
||||
file.isError = false;
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
|
||||
}
|
||||
} else if (!file.paused) {
|
||||
// Pause the upload (if possible)
|
||||
if (typeof file.pause === "function") {
|
||||
file.pause();
|
||||
file.paused = true;
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">play_circle_outline</span>';
|
||||
} else {
|
||||
}
|
||||
} else if (file.paused) {
|
||||
// Resume sequence: first call to resume (or upload() fallback)
|
||||
if (typeof file.resume === "function") {
|
||||
file.resume();
|
||||
} else {
|
||||
resumableInstance.upload();
|
||||
}
|
||||
// After a short delay, pause again then resume
|
||||
setTimeout(() => {
|
||||
// Conditionally add the pause/resume button only if file.pause is available
|
||||
// Pause/Resume button (for resumable file–picker uploads)
|
||||
if (typeof file.pause === "function") {
|
||||
const pauseResumeBtn = document.createElement("button");
|
||||
pauseResumeBtn.setAttribute("type", "button"); // not a submit button
|
||||
pauseResumeBtn.classList.add("pause-resume-btn");
|
||||
// Start with pause icon and disable button until upload starts
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
|
||||
pauseResumeBtn.disabled = true;
|
||||
pauseResumeBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
if (file.isError) {
|
||||
// If the file previously failed, try restarting upload.
|
||||
if (typeof file.retry === "function") {
|
||||
file.retry();
|
||||
file.isError = false;
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
|
||||
}
|
||||
} else if (!file.paused) {
|
||||
// Pause the upload (if possible)
|
||||
if (typeof file.pause === "function") {
|
||||
file.pause();
|
||||
file.paused = true;
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">play_circle_outline</span>';
|
||||
} else {
|
||||
}
|
||||
} else if (file.paused) {
|
||||
// Resume sequence: first call to resume (or upload() fallback)
|
||||
if (typeof file.resume === "function") {
|
||||
file.resume();
|
||||
} else {
|
||||
resumableInstance.upload();
|
||||
}
|
||||
// After a short delay, pause again then resume
|
||||
setTimeout(() => {
|
||||
if (typeof file.resume === "function") {
|
||||
file.resume();
|
||||
if (typeof file.pause === "function") {
|
||||
file.pause();
|
||||
} else {
|
||||
resumableInstance.upload();
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (typeof file.resume === "function") {
|
||||
file.resume();
|
||||
} else {
|
||||
resumableInstance.upload();
|
||||
}
|
||||
}, 100);
|
||||
}, 100);
|
||||
}, 100);
|
||||
file.paused = false;
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
|
||||
} else {
|
||||
console.error("Pause/resume function not available for file", file);
|
||||
}
|
||||
});
|
||||
li.appendChild(pauseResumeBtn);
|
||||
}
|
||||
file.paused = false;
|
||||
pauseResumeBtn.innerHTML = '<span class="material-icons pauseResumeBtn">pause_circle_outline</span>';
|
||||
} else {
|
||||
console.error("Pause/resume function not available for file", file);
|
||||
}
|
||||
});
|
||||
li.appendChild(pauseResumeBtn);
|
||||
}
|
||||
|
||||
// Preview element
|
||||
const preview = document.createElement("div");
|
||||
@@ -406,20 +406,27 @@ let resumableInstance;
|
||||
function initResumableUpload() {
|
||||
resumableInstance = new Resumable({
|
||||
target: "/api/upload/upload.php",
|
||||
query: { folder: window.currentFolder || "root", upload_token: window.csrfToken },
|
||||
chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks
|
||||
chunkSize: 1.5 * 1024 * 1024,
|
||||
simultaneousUploads: 3,
|
||||
forceChunkSize: true,
|
||||
testChunks: false,
|
||||
throttleProgressCallbacks: 1,
|
||||
withCredentials: true,
|
||||
headers: { 'X-CSRF-Token': window.csrfToken },
|
||||
query: {
|
||||
query: () => ({
|
||||
folder: window.currentFolder || "root",
|
||||
upload_token: window.csrfToken // still as a fallback
|
||||
}
|
||||
upload_token: window.csrfToken
|
||||
})
|
||||
});
|
||||
|
||||
// keep query fresh when folder changes (call this from your folder nav code)
|
||||
function updateResumableQuery() {
|
||||
if (!resumableInstance) return;
|
||||
resumableInstance.opts.headers['X-CSRF-Token'] = window.csrfToken;
|
||||
// if you're not using a function for query, do:
|
||||
resumableInstance.opts.query.folder = window.currentFolder || 'root';
|
||||
resumableInstance.opts.query.upload_token = window.csrfToken;
|
||||
}
|
||||
|
||||
const fileInput = document.getElementById("file");
|
||||
if (fileInput) {
|
||||
// Assign Resumable to file input for file picker uploads.
|
||||
@@ -432,6 +439,7 @@ function initResumableUpload() {
|
||||
}
|
||||
|
||||
resumableInstance.on("fileAdded", function (file) {
|
||||
|
||||
// Initialize custom paused flag
|
||||
file.paused = false;
|
||||
file.uploadIndex = file.uniqueIdentifier;
|
||||
@@ -461,16 +469,17 @@ function initResumableUpload() {
|
||||
li.dataset.uploadIndex = file.uniqueIdentifier;
|
||||
list.appendChild(li);
|
||||
updateFileInfoCount();
|
||||
updateResumableQuery();
|
||||
});
|
||||
|
||||
resumableInstance.on("fileProgress", function(file) {
|
||||
resumableInstance.on("fileProgress", function (file) {
|
||||
const progress = file.progress(); // value between 0 and 1
|
||||
const percent = Math.floor(progress * 100);
|
||||
const li = document.querySelector(`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`);
|
||||
if (li && li.progressBar) {
|
||||
if (percent < 99) {
|
||||
li.progressBar.style.width = percent + "%";
|
||||
|
||||
|
||||
// Calculate elapsed time and speed.
|
||||
const elapsed = (Date.now() - li.startTime) / 1000;
|
||||
let speed = "";
|
||||
@@ -491,7 +500,7 @@ function initResumableUpload() {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerHTML = '<i class="material-icons spinning" style="vertical-align: middle;">autorenew</i>';
|
||||
}
|
||||
|
||||
|
||||
// Enable the pause/resume button once progress starts.
|
||||
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
||||
if (pauseResumeBtn) {
|
||||
@@ -499,8 +508,8 @@ function initResumableUpload() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resumableInstance.on("fileSuccess", function(file, message) {
|
||||
|
||||
resumableInstance.on("fileSuccess", function (file, message) {
|
||||
// Try to parse JSON response
|
||||
let data;
|
||||
try {
|
||||
@@ -508,18 +517,18 @@ function initResumableUpload() {
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
|
||||
// 1) Soft‐fail CSRF? then update token & retry this file
|
||||
if (data && data.csrf_expired) {
|
||||
// Update global and Resumable headers
|
||||
window.csrfToken = data.csrf_token;
|
||||
resumableInstance.opts.headers['X-CSRF-Token'] = data.csrf_token;
|
||||
resumableInstance.opts.query.upload_token = data.csrf_token;
|
||||
resumableInstance.opts.query.upload_token = data.csrf_token;
|
||||
// Retry this chunk/file
|
||||
file.retry();
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 2) Otherwise treat as real success:
|
||||
const li = document.querySelector(
|
||||
`li.upload-progress-item[data-upload-index="${file.uniqueIdentifier}"]`
|
||||
@@ -531,13 +540,13 @@ function initResumableUpload() {
|
||||
const pauseResumeBtn = li.querySelector(".pause-resume-btn");
|
||||
if (pauseResumeBtn) pauseResumeBtn.style.display = "none";
|
||||
const removeBtn = li.querySelector(".remove-file-btn");
|
||||
if (removeBtn) removeBtn.style.display = "none";
|
||||
if (removeBtn) removeBtn.style.display = "none";
|
||||
setTimeout(() => li.remove(), 5000);
|
||||
}
|
||||
|
||||
|
||||
loadFileList(window.currentFolder);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
resumableInstance.on("fileError", function (file, message) {
|
||||
@@ -637,7 +646,7 @@ function submitFiles(allFiles) {
|
||||
} catch (e) {
|
||||
jsonResponse = null;
|
||||
}
|
||||
|
||||
|
||||
// ─── Soft-fail CSRF: retry this upload ───────────────────────
|
||||
if (jsonResponse && jsonResponse.csrf_expired) {
|
||||
console.warn("CSRF expired during upload, retrying chunk", file.uploadIndex);
|
||||
@@ -650,10 +659,10 @@ function submitFiles(allFiles) {
|
||||
xhr.send(formData);
|
||||
return; // skip the "finishedCount++" and error/success logic for now
|
||||
}
|
||||
|
||||
|
||||
// ─── Normal success/error handling ────────────────────────────
|
||||
const li = progressElements[file.uploadIndex];
|
||||
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
||||
// real success
|
||||
if (li) {
|
||||
@@ -662,6 +671,7 @@ function submitFiles(allFiles) {
|
||||
if (li.removeBtn) li.removeBtn.style.display = "none";
|
||||
}
|
||||
uploadResults[file.uploadIndex] = true;
|
||||
|
||||
} else {
|
||||
// real failure
|
||||
if (li) {
|
||||
@@ -681,12 +691,17 @@ function submitFiles(allFiles) {
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
|
||||
// ─── Only now count this chunk as finished ───────────────────
|
||||
finishedCount++;
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList(allFiles, uploadResults, progressElements);
|
||||
}
|
||||
if (finishedCount === allFiles.length) {
|
||||
const succeededCount = uploadResults.filter(Boolean).length;
|
||||
const failedCount = allFiles.length - succeededCount;
|
||||
|
||||
setTimeout(() => {
|
||||
refreshFileList(allFiles, uploadResults, progressElements);
|
||||
}, 250);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("error", function () {
|
||||
@@ -699,6 +714,9 @@ function submitFiles(allFiles) {
|
||||
finishedCount++;
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList(allFiles, uploadResults, progressElements);
|
||||
// Immediate summary toast based on actual XHR outcomes
|
||||
const succeededCount = uploadResults.filter(Boolean).length;
|
||||
const failedCount = allFiles.length - succeededCount;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -725,17 +743,30 @@ function submitFiles(allFiles) {
|
||||
loadFileList(folderToUse)
|
||||
.then(serverFiles => {
|
||||
initFileActions();
|
||||
serverFiles = (serverFiles || []).map(item => item.name.trim().toLowerCase());
|
||||
// Be tolerant to API shapes: string or object with name/fileName/filename
|
||||
serverFiles = (serverFiles || [])
|
||||
.map(item => {
|
||||
if (typeof item === 'string') return item;
|
||||
const n = item?.name ?? item?.fileName ?? item?.filename ?? '';
|
||||
return String(n);
|
||||
})
|
||||
.map(s => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
let overallSuccess = true;
|
||||
let succeeded = 0;
|
||||
allFiles.forEach(file => {
|
||||
const clientFileName = file.name.trim().toLowerCase();
|
||||
const li = progressElements[file.uploadIndex];
|
||||
if (!uploadResults[file.uploadIndex] || !serverFiles.includes(clientFileName)) {
|
||||
const hadRelative = !!(file.webkitRelativePath || file.customRelativePath);
|
||||
if (!uploadResults[file.uploadIndex] || (!hadRelative && !serverFiles.includes(clientFileName))) {
|
||||
if (li) {
|
||||
li.progressBar.innerText = "Error";
|
||||
}
|
||||
overallSuccess = false;
|
||||
|
||||
} else if (li) {
|
||||
succeeded++;
|
||||
|
||||
// Schedule removal of successful file entry after 5 seconds.
|
||||
setTimeout(() => {
|
||||
li.remove();
|
||||
@@ -757,9 +788,12 @@ function submitFiles(allFiles) {
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!overallSuccess) {
|
||||
showToast("Some files failed to upload. Please check the list.");
|
||||
const failed = allFiles.length - succeeded;
|
||||
showToast(`${failed} file(s) failed, ${succeeded} succeeded. Please check the list.`);
|
||||
} else {
|
||||
showToast(`${succeeded} file succeeded. Please check the list.`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -768,6 +802,7 @@ function submitFiles(allFiles) {
|
||||
})
|
||||
.finally(() => {
|
||||
loadFolderTree(window.currentFolder);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// generated by CI
|
||||
window.APP_VERSION = 'v1.6.6';
|
||||
window.APP_VERSION = 'v1.6.11';
|
||||
|
||||
21
public/vendor/bootstrap/4.5.2/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
Bootstrap 4.5.2 — MIT
|
||||
|
||||
MIT License
|
||||
|
||||
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.
|
||||
6
public/vendor/bootstrap/4.5.2/bootstrap.min.css
vendored
Normal file
1
public/vendor/bootstrap/4.5.2/bootstrap.min.css.map.json
vendored
Normal file
21
public/vendor/codemirror/5.65.5/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
CodeMirror 5.65.5 — MIT
|
||||
|
||||
MIT License
|
||||
|
||||
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.
|
||||
1
public/vendor/codemirror/5.65.5/codemirror.min.css
vendored
Normal file
1
public/vendor/codemirror/5.65.5/codemirror.min.js
vendored
Normal file
1
public/vendor/codemirror/5.65.5/mode/clike/clike.min.js
vendored
Normal file
1
public/vendor/codemirror/5.65.5/mode/css/css.min.js
vendored
Normal file
1
public/vendor/codemirror/5.65.5/mode/htmlmixed/htmlmixed.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],t):t(CodeMirror)}(function(m){"use strict";var l={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};var a={};function d(t,e){e=t.match(a[t=e]||(a[t]=new RegExp("\\s+"+t+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*")));return e?/^\s*(.*?)\s*$/.exec(e[2])[1]:""}function g(t,e){return new RegExp((e?"^":"")+"</\\s*"+t+"\\s*>","i")}function o(t,e){for(var a in t)for(var n=e[a]||(e[a]=[]),l=t[a],o=l.length-1;0<=o;o--)n.unshift(l[o])}m.defineMode("htmlmixed",function(i,t){var c=m.getMode(i,{name:"xml",htmlMode:!0,multilineTagIndentFactor:t.multilineTagIndentFactor,multilineTagIndentPastTag:t.multilineTagIndentPastTag,allowMissingTagName:t.allowMissingTagName}),s={},e=t&&t.tags,a=t&&t.scriptTypes;if(o(l,s),e&&o(e,s),a)for(var n=a.length-1;0<=n;n--)s.script.unshift(["type",a[n].matches,a[n].mode]);function u(t,e){var a,o,r,n=c.token(t,e.htmlState),l=/\btag\b/.test(n);return l&&!/[<>\s\/]/.test(t.current())&&(a=e.htmlState.tagName&&e.htmlState.tagName.toLowerCase())&&s.hasOwnProperty(a)?e.inTag=a+" ":e.inTag&&l&&/>$/.test(t.current())?(a=/^([\S]+) (.*)/.exec(e.inTag),e.inTag=null,l=">"==t.current()&&function(t,e){for(var a=0;a<t.length;a++){var n=t[a];if(!n[0]||n[1].test(d(e,n[0])))return n[2]}}(s[a[1]],a[2]),l=m.getMode(i,l),o=g(a[1],!0),r=g(a[1],!1),e.token=function(t,e){return t.match(o,!1)?(e.token=u,e.localState=e.localMode=null):(a=t,n=r,t=e.localMode.token(t,e.localState),e=a.current(),-1<(l=e.search(n))?a.backUp(e.length-l):e.match(/<\/?$/)&&(a.backUp(e.length),a.match(n,!1)||a.match(e)),t);var a,n,l},e.localMode=l,e.localState=m.startState(l,c.indent(e.htmlState,"",""))):e.inTag&&(e.inTag+=t.current(),t.eol()&&(e.inTag+=" ")),n}return{startState:function(){return{token:u,inTag:null,localMode:null,localState:null,htmlState:m.startState(c)}},copyState:function(t){var e;return t.localState&&(e=m.copyState(t.localMode,t.localState)),{token:t.token,inTag:t.inTag,localMode:t.localMode,localState:e,htmlState:m.copyState(c,t.htmlState)}},token:function(t,e){return e.token(t,e)},indent:function(t,e,a){return!t.localMode||/^\s*<\//.test(e)?c.indent(t.htmlState,e,a):t.localMode.indent?t.localMode.indent(t.localState,e,a):m.Pass},innerMode:function(t){return{state:t.localState||t.htmlState,mode:t.localMode||c}}}},"xml","javascript","css"),m.defineMIME("text/html","htmlmixed")});
|
||||
1
public/vendor/codemirror/5.65.5/mode/javascript/javascript.min.js
vendored
Normal file
1
public/vendor/codemirror/5.65.5/mode/markdown/markdown.min.js
vendored
Normal file
1
public/vendor/codemirror/5.65.5/mode/php/php.min.js
vendored
Normal file
1
public/vendor/codemirror/5.65.5/mode/properties/properties.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.defineMode("properties",function(){return{token:function(e,i){var t=e.sol()||i.afterSection,n=e.eol();if(i.afterSection=!1,t&&(i.nextMultiline?(i.inMultiline=!0,i.nextMultiline=!1):i.position="def"),n&&!i.nextMultiline&&(i.inMultiline=!1,i.position="def"),t)for(;e.eatSpace(););n=e.next();return!t||"#"!==n&&"!"!==n&&";"!==n?t&&"["===n?(i.afterSection=!0,e.skipTo("]"),e.eat("]"),"header"):"="===n||":"===n?(i.position="quote",null):("\\"===n&&"quote"===i.position&&e.eol()&&(i.nextMultiline=!0),i.position):(i.position="comment",e.skipToEnd(),"comment")},startState:function(){return{position:"def",nextMultiline:!1,inMultiline:!1,afterSection:!1}}}}),e.defineMIME("text/x-properties","properties"),e.defineMIME("text/x-ini","properties")});
|
||||
1
public/vendor/codemirror/5.65.5/mode/python/python.min.js
vendored
Normal file
1
public/vendor/codemirror/5.65.5/mode/shell/shell.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(s){"use strict";s.defineMode("shell",function(){var o={};function e(e,t){for(var n=0;n<t.length;n++)o[t[n]]=e}var t=["true","false"],n=["if","then","do","else","elif","while","until","for","in","esac","fi","fin","fil","done","exit","set","unset","export","function"],r=["ab","awk","bash","beep","cat","cc","cd","chown","chmod","chroot","clear","cp","curl","cut","diff","echo","find","gawk","gcc","get","git","grep","hg","kill","killall","ln","ls","make","mkdir","openssl","mv","nc","nl","node","npm","ping","ps","restart","rm","rmdir","sed","service","sh","shopt","shred","source","sort","sleep","ssh","start","stop","su","sudo","svn","tee","telnet","top","touch","vi","vim","wall","wc","wget","who","write","yes","zsh"];function i(e,t){if(e.eatSpace())return null;var n,r=e.sol(),i=e.next();if("\\"===i)return e.next(),null;if("'"===i||'"'===i||"`"===i)return t.tokens.unshift(f(i,"`"===i?"quote":"string")),l(e,t);if("#"===i)return r&&e.eat("!")?(e.skipToEnd(),"meta"):(e.skipToEnd(),"comment");if("$"===i)return t.tokens.unshift(u),l(e,t);if("+"===i||"="===i)return"operator";if("-"===i)return e.eat("-"),e.eatWhile(/\w/),"attribute";if("<"==i){if(e.match("<<"))return"operator";r=e.match(/^<-?\s*['"]?([^'"]*)['"]?/);if(r)return t.tokens.unshift((n=r[1],function(e,t){return e.sol()&&e.string==n&&t.tokens.shift(),e.skipToEnd(),"string-2"})),"string-2"}if(/\d/.test(i)&&(e.eatWhile(/\d/),e.eol()||!/\w/.test(e.peek())))return"number";e.eatWhile(/[\w-]/);t=e.current();return"="===e.peek()&&/\w+/.test(t)?"def":o.hasOwnProperty(t)?o[t]:null}function f(i,o){var s="("==i?")":"{"==i?"}":i;return function(e,t){for(var n,r=!1;null!=(n=e.next());){if(n===s&&!r){t.tokens.shift();break}if("$"===n&&!r&&"'"!==i&&e.peek()!=s){r=!0,e.backUp(1),t.tokens.unshift(u);break}if(!r&&i!==s&&n===i)return t.tokens.unshift(f(i,o)),l(e,t);if(!r&&/['"]/.test(n)&&!/['"]/.test(i)){t.tokens.unshift(function(n,r){return function(e,t){return t.tokens[0]=f(n,r),e.next(),l(e,t)}}(n,"string")),e.backUp(1);break}r=!r&&"\\"===n}return o}}s.registerHelper("hintWords","shell",t.concat(n,r)),e("atom",t),e("keyword",n),e("builtin",r);var u=function(e,t){1<t.tokens.length&&e.eat("$");var n=e.next();return/['"({]/.test(n)?(t.tokens[0]=f(n,"("==n?"quote":"{"==n?"def":"string"),l(e,t)):(/\d/.test(n)||e.eatWhile(/\w/),t.tokens.shift(),"def")};function l(e,t){return(t.tokens[0]||i)(e,t)}return{startState:function(){return{tokens:[]}},token:l,closeBrackets:"()[]{}''\"\"``",lineComment:"#",fold:"brace"}}),s.defineMIME("text/x-sh","shell"),s.defineMIME("application/x-sh","shell")});
|
||||
1
public/vendor/codemirror/5.65.5/mode/sql/sql.min.js
vendored
Normal file
1
public/vendor/codemirror/5.65.5/mode/xml/xml.min.js
vendored
Normal file
1
public/vendor/codemirror/5.65.5/mode/yaml/yaml.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.defineMode("yaml",function(){var n=new RegExp("\\b(("+["true","false","on","off","yes","no"].join(")|(")+"))$","i");return{token:function(e,i){var t=e.peek(),r=i.escaped;if(i.escaped=!1,"#"==t&&(0==e.pos||/\s/.test(e.string.charAt(e.pos-1))))return e.skipToEnd(),"comment";if(e.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))return"string";if(i.literal&&e.indentation()>i.keyCol)return e.skipToEnd(),"string";if(i.literal&&(i.literal=!1),e.sol()){if(i.keyCol=0,i.pair=!1,i.pairStart=!1,e.match("---"))return"def";if(e.match("..."))return"def";if(e.match(/\s*-\s+/))return"meta"}if(e.match(/^(\{|\}|\[|\])/))return"{"==t?i.inlinePairs++:"}"==t?i.inlinePairs--:"["==t?i.inlineList++:i.inlineList--,"meta";if(0<i.inlineList&&!r&&","==t)return e.next(),"meta";if(0<i.inlinePairs&&!r&&","==t)return i.keyCol=0,i.pair=!1,i.pairStart=!1,e.next(),"meta";if(i.pairStart){if(e.match(/^\s*(\||\>)\s*/))return i.literal=!0,"meta";if(e.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i))return"variable-2";if(0==i.inlinePairs&&e.match(/^\s*-?[0-9\.\,]+\s?$/))return"number";if(0<i.inlinePairs&&e.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/))return"number";if(e.match(n))return"keyword"}return!i.pair&&e.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)?(i.pair=!0,i.keyCol=e.indentation(),"atom"):i.pair&&e.match(/^:\s*/)?(i.pairStart=!0,"meta"):(i.pairStart=!1,i.escaped="\\"==t,e.next(),null)},startState:function(){return{pair:!1,pairStart:!1,keyCol:0,inlinePairs:0,inlineList:0,literal:!1,escaped:!1}},lineComment:"#",fold:"indent"}}),e.defineMIME("text/x-yaml","yaml"),e.defineMIME("text/yaml","yaml")});
|
||||
1
public/vendor/codemirror/5.65.5/theme/material-darker.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.cm-s-material-darker.CodeMirror{background-color:#212121;color:#eff}.cm-s-material-darker .CodeMirror-gutters{background:#212121;color:#545454;border:none}.cm-s-material-darker .CodeMirror-guttermarker,.cm-s-material-darker .CodeMirror-guttermarker-subtle,.cm-s-material-darker .CodeMirror-linenumber{color:#545454}.cm-s-material-darker .CodeMirror-cursor{border-left:1px solid #fc0}.cm-s-material-darker div.CodeMirror-selected{background:rgba(97,97,97,.2)}.cm-s-material-darker.CodeMirror-focused div.CodeMirror-selected{background:rgba(97,97,97,.2)}.cm-s-material-darker .CodeMirror-line::selection,.cm-s-material-darker .CodeMirror-line>span::selection,.cm-s-material-darker .CodeMirror-line>span>span::selection{background:rgba(128,203,196,.2)}.cm-s-material-darker .CodeMirror-line::-moz-selection,.cm-s-material-darker .CodeMirror-line>span::-moz-selection,.cm-s-material-darker .CodeMirror-line>span>span::-moz-selection{background:rgba(128,203,196,.2)}.cm-s-material-darker .CodeMirror-activeline-background{background:rgba(0,0,0,.5)}.cm-s-material-darker .cm-keyword{color:#c792ea}.cm-s-material-darker .cm-operator{color:#89ddff}.cm-s-material-darker .cm-variable-2{color:#eff}.cm-s-material-darker .cm-type,.cm-s-material-darker .cm-variable-3{color:#f07178}.cm-s-material-darker .cm-builtin{color:#ffcb6b}.cm-s-material-darker .cm-atom{color:#f78c6c}.cm-s-material-darker .cm-number{color:#ff5370}.cm-s-material-darker .cm-def{color:#82aaff}.cm-s-material-darker .cm-string{color:#c3e88d}.cm-s-material-darker .cm-string-2{color:#f07178}.cm-s-material-darker .cm-comment{color:#545454}.cm-s-material-darker .cm-variable{color:#f07178}.cm-s-material-darker .cm-tag{color:#ff5370}.cm-s-material-darker .cm-meta{color:#ffcb6b}.cm-s-material-darker .cm-attribute{color:#c792ea}.cm-s-material-darker .cm-property{color:#c792ea}.cm-s-material-darker .cm-qualifier{color:#decb6b}.cm-s-material-darker .cm-type,.cm-s-material-darker .cm-variable-3{color:#decb6b}.cm-s-material-darker .cm-error{color:#fff;background-color:#ff5370}.cm-s-material-darker .CodeMirror-matchingbracket{text-decoration:underline;color:#fff!important}
|
||||
180
public/vendor/dompurify/2.4.0/LICENSE
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
DOMPurify 2.4.0 — Apache-2.0
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor
|
||||
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable copyright license to reproduce, prepare
|
||||
Derivative Works of, publicly display, publicly perform, sublicense,
|
||||
and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor
|
||||
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable (except as stated in this section) patent
|
||||
license to make, have made, use, offer to sell, sell, import, and
|
||||
otherwise transfer the Work, where such license applies only to those
|
||||
patent claims licensable by such Contributor that are necessarily
|
||||
infringed by their Contribution(s) alone or by combination of their
|
||||
Contribution(s) with the Work to which such Contribution(s) was submitted.
|
||||
If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works
|
||||
thereof in any medium, with or without modifications, and in Source or
|
||||
Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works
|
||||
a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating
|
||||
that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You
|
||||
distribute, all copyright, patent, trademark, and attribution notices
|
||||
from the Source form of the Work, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution,
|
||||
then any Derivative Works that You distribute must include a readable
|
||||
copy of the attribution notices contained within such NOTICE file,
|
||||
excluding those notices that do not pertain to any part of the
|
||||
Derivative Works, in at least one of the following places: within a
|
||||
NOTICE text file distributed as part of the Derivative Works; within
|
||||
the Source form or documentation, if provided along with the Derivative
|
||||
Works; or, within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents of the
|
||||
NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works
|
||||
that You distribute, alongside or as an addendum to the NOTICE text from
|
||||
the Work, provided that such additional attribution notices cannot be
|
||||
construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may
|
||||
provide additional or different license terms and conditions for use,
|
||||
reproduction, or distribution of Your modifications, or for any such
|
||||
Derivative Works as a whole, provided Your use, reproduction, and
|
||||
distribution of the Work otherwise complies with the conditions
|
||||
stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally
|
||||
submitted for inclusion in the Work by You to the Licensor shall be
|
||||
under the terms and conditions of this License, without any additional
|
||||
terms or conditions. Notwithstanding the above, nothing herein shall
|
||||
supersede or modify the terms of any separate license agreement you
|
||||
may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides
|
||||
the Work (and each Contributor provides its Contributions) on an "AS IS"
|
||||
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions of
|
||||
TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR
|
||||
PURPOSE. You are solely responsible for determining the appropriateness
|
||||
of using or redistributing the Work and assume any risks associated with
|
||||
Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License
|
||||
or out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction,
|
||||
or any and all other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this License.
|
||||
However, in accepting such obligations, You may act only on Your own behalf
|
||||
and on Your sole responsibility, not on behalf of any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against, such Contributor
|
||||
by reason of your accepting any such warranty or additional liability.
|
||||
2
public/vendor/dompurify/2.4.0/purify.min.js
vendored
Normal file
1
public/vendor/dompurify/2.4.0/purify.min.js-2.map
generated
vendored
Normal file
180
public/vendor/fuse/6.6.2/LICENSE
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
Fuse.js 6.6.2 — Apache-2.0
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor
|
||||
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable copyright license to reproduce, prepare
|
||||
Derivative Works of, publicly display, publicly perform, sublicense,
|
||||
and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor
|
||||
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable (except as stated in this section) patent
|
||||
license to make, have made, use, offer to sell, sell, import, and
|
||||
otherwise transfer the Work, where such license applies only to those
|
||||
patent claims licensable by such Contributor that are necessarily
|
||||
infringed by their Contribution(s) alone or by combination of their
|
||||
Contribution(s) with the Work to which such Contribution(s) was submitted.
|
||||
If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works
|
||||
thereof in any medium, with or without modifications, and in Source or
|
||||
Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works
|
||||
a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating
|
||||
that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You
|
||||
distribute, all copyright, patent, trademark, and attribution notices
|
||||
from the Source form of the Work, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution,
|
||||
then any Derivative Works that You distribute must include a readable
|
||||
copy of the attribution notices contained within such NOTICE file,
|
||||
excluding those notices that do not pertain to any part of the
|
||||
Derivative Works, in at least one of the following places: within a
|
||||
NOTICE text file distributed as part of the Derivative Works; within
|
||||
the Source form or documentation, if provided along with the Derivative
|
||||
Works; or, within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents of the
|
||||
NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works
|
||||
that You distribute, alongside or as an addendum to the NOTICE text from
|
||||
the Work, provided that such additional attribution notices cannot be
|
||||
construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may
|
||||
provide additional or different license terms and conditions for use,
|
||||
reproduction, or distribution of Your modifications, or for any such
|
||||
Derivative Works as a whole, provided Your use, reproduction, and
|
||||
distribution of the Work otherwise complies with the conditions
|
||||
stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally
|
||||
submitted for inclusion in the Work by You to the Licensor shall be
|
||||
under the terms and conditions of this License, without any additional
|
||||
terms or conditions. Notwithstanding the above, nothing herein shall
|
||||
supersede or modify the terms of any separate license agreement you
|
||||
may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides
|
||||
the Work (and each Contributor provides its Contributions) on an "AS IS"
|
||||
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions of
|
||||
TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR
|
||||
PURPOSE. You are solely responsible for determining the appropriateness
|
||||
of using or redistributing the Work and assume any risks associated with
|
||||
Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License
|
||||
or out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction,
|
||||
or any and all other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this License.
|
||||
However, in accepting such obligations, You may act only on Your own behalf
|
||||
and on Your sole responsibility, not on behalf of any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against, such Contributor
|
||||
by reason of your accepting any such warranty or additional liability.
|
||||
9
public/vendor/fuse/6.6.2/fuse.min.js
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
21
public/vendor/resumable/1.1.0/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
Resumable.js 1.1.0 — MIT
|
||||
|
||||
MIT License
|
||||
|
||||
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.
|
||||
1
public/vendor/resumable/1.1.0/resumable.min.js
vendored
Normal file
|
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 500 KiB |
|
Before Width: | Height: | Size: 764 KiB After Width: | Height: | Size: 470 KiB |
BIN
resources/dark-folder-access.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 736 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 392 KiB After Width: | Height: | Size: 623 KiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 687 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 521 KiB |
|
Before Width: | Height: | Size: 378 KiB After Width: | Height: | Size: 552 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 397 KiB After Width: | Height: | Size: 538 KiB |
|
Before Width: | Height: | Size: 504 KiB After Width: | Height: | Size: 610 KiB |
|
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 554 KiB |
@@ -501,7 +501,7 @@ public function deleteFiles()
|
||||
$userPermissions = $this->loadPerms($username);
|
||||
|
||||
// Need granular rename (or ancestor-owner)
|
||||
if (!(ACL::canRename($username, $userPermissions, $folder) || $this->ownsFolderOrAncestor($folder, $username, $userPermissions))) {
|
||||
if (!(ACL::canRename($username, $userPermissions, $folder))) {
|
||||
$this->_jsonOut(["error"=>"Forbidden: no rename rights"], 403); return;
|
||||
}
|
||||
|
||||
|
||||
@@ -695,4 +695,79 @@ for ($i = $startPage; $i <= $endPage; $i++): ?>
|
||||
echo json_encode(['success' => false, 'error' => 'Not found']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- API: Move Folder -------------------- */
|
||||
public function moveFolder(): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
self::requireAuth();
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { http_response_code(405); echo json_encode(['error'=>'Method not allowed']); return; }
|
||||
// CSRF: accept header or form field
|
||||
$hdr = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
$tok = $_SESSION['csrf_token'] ?? '';
|
||||
if (!$hdr || !$tok || !hash_equals((string)$tok, (string)$hdr)) { http_response_code(403); echo json_encode(['error'=>'Invalid CSRF token']); return; }
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$input = json_decode($raw ?: "{}", true);
|
||||
$source = trim((string)($input['source'] ?? ''));
|
||||
$destination = trim((string)($input['destination'] ?? ''));
|
||||
|
||||
if ($source === '' || strcasecmp($source,'root')===0) { http_response_code(400); echo json_encode(['error'=>'Invalid source folder']); return; }
|
||||
if ($destination === '') $destination = 'root';
|
||||
|
||||
// basic segment validation
|
||||
foreach ([$source,$destination] as $f) {
|
||||
if ($f==='root') continue;
|
||||
$parts = array_filter(explode('/', trim($f, "/\\ ")), fn($p)=>$p!=='');
|
||||
foreach ($parts as $seg) {
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $seg)) { http_response_code(400); echo json_encode(['error'=>'Invalid folder segment']); return; }
|
||||
}
|
||||
}
|
||||
|
||||
$srcNorm = trim($source, "/\\ ");
|
||||
$dstNorm = $destination==='root' ? '' : trim($destination, "/\\ ");
|
||||
|
||||
// prevent move into self/descendant
|
||||
if ($dstNorm !== '' && (strcasecmp($dstNorm,$srcNorm)===0 || strpos($dstNorm.'/', $srcNorm.'/')===0)) {
|
||||
http_response_code(400); echo json_encode(['error'=>'Destination cannot be the source or its descendant']); return;
|
||||
}
|
||||
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$perms = $this->loadPerms($username);
|
||||
|
||||
// enforce scopes (source manage-ish, dest write-ish)
|
||||
if ($msg = self::enforceFolderScope($source, $username, $perms, 'manage')) { http_response_code(403); echo json_encode(['error'=>$msg]); return; }
|
||||
if ($msg = self::enforceFolderScope($destination, $username, $perms, 'write')) { http_response_code(403); echo json_encode(['error'=>$msg]); return; }
|
||||
|
||||
// Check capabilities using ACL helpers
|
||||
$canManageSource = ACL::canManage($username, $perms, $source) || ACL::isOwner($username, $perms, $source);
|
||||
$canMoveIntoDest = ACL::canMove($username, $perms, $destination) || ($destination==='root' ? self::isAdmin($perms) : ACL::isOwner($username, $perms, $destination));
|
||||
if (!$canManageSource) { http_response_code(403); echo json_encode(['error'=>'Forbidden: manage rights required on source']); return; }
|
||||
if (!$canMoveIntoDest) { http_response_code(403); echo json_encode(['error'=>'Forbidden: move rights required on destination']); return; }
|
||||
|
||||
// Non-admin: enforce same owner between source and destination tree (if any)
|
||||
$isAdmin = self::isAdmin($perms);
|
||||
if (!$isAdmin) {
|
||||
try {
|
||||
$ownerSrc = FolderModel::getOwnerFor($source) ?? '';
|
||||
$ownerDst = $destination==='root' ? '' : (FolderModel::getOwnerFor($destination) ?? '');
|
||||
if ($ownerSrc !== $ownerDst) {
|
||||
http_response_code(403); echo json_encode(['error'=>'Source and destination must have the same owner']); return;
|
||||
}
|
||||
} catch (\Throwable $e) { /* ignore – fall through */ }
|
||||
}
|
||||
|
||||
// Compute final target "destination/basename(source)"
|
||||
$baseName = basename(str_replace('\\','/', $srcNorm));
|
||||
$target = $destination==='root' ? $baseName : rtrim($destination, "/\\ ") . '/' . $baseName;
|
||||
|
||||
try {
|
||||
$result = FolderModel::renameFolder($source, $target);
|
||||
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (\Throwable $e) {
|
||||
error_log('moveFolder error: '.$e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['error'=>'Internal error moving folder']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,48 @@ class ACL
|
||||
unset($rec);
|
||||
return $changed ? self::save($acl) : true;
|
||||
}
|
||||
public static function ownsFolderOrAncestor(string $user, array $perms, string $folder): bool
|
||||
{
|
||||
$folder = self::normalizeFolder($folder);
|
||||
if (self::isAdmin($perms)) return true;
|
||||
if (self::hasGrant($user, $folder, 'owners')) return true;
|
||||
|
||||
$folder = trim($folder, "/\\ ");
|
||||
if ($folder === '' || $folder === 'root') return false;
|
||||
|
||||
$parts = explode('/', $folder);
|
||||
while (count($parts) > 1) {
|
||||
array_pop($parts);
|
||||
$parent = implode('/', $parts);
|
||||
if (self::hasGrant($user, $parent, 'owners')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Re-key explicit ACL entries for an entire subtree: old/... → new/... */
|
||||
public static function renameTree(string $oldFolder, string $newFolder): void
|
||||
{
|
||||
$old = self::normalizeFolder($oldFolder);
|
||||
$new = self::normalizeFolder($newFolder);
|
||||
if ($old === '' || $old === 'root') return; // nothing to re-key for root
|
||||
|
||||
$acl = self::$cache ?? self::loadFresh();
|
||||
if (!isset($acl['folders']) || !is_array($acl['folders'])) return;
|
||||
|
||||
$rebased = [];
|
||||
foreach ($acl['folders'] as $k => $rec) {
|
||||
if ($k === $old || strpos($k, $old . '/') === 0) {
|
||||
$suffix = substr($k, strlen($old));
|
||||
$suffix = ltrim((string)$suffix, '/');
|
||||
$newKey = $new . ($suffix !== '' ? '/' . $suffix : '');
|
||||
$rebased[$newKey] = $rec;
|
||||
} else {
|
||||
$rebased[$k] = $rec;
|
||||
}
|
||||
}
|
||||
$acl['folders'] = $rebased;
|
||||
self::save($acl);
|
||||
}
|
||||
|
||||
private static function loadFresh(): array {
|
||||
$path = self::path();
|
||||
@@ -323,10 +365,10 @@ class ACL
|
||||
$sf = !empty($caps['shareFile']) || !empty($caps['share_file']);
|
||||
$sfo = !empty($caps['shareFolder']) || !empty($caps['share_folder']);
|
||||
|
||||
if ($m) { $v = true; $w = true; $u = $c = $ed = $rn = $cp = $mv = $dl = $ex = $sf = $sfo = true; }
|
||||
if ($m) { $v = true; $w = true; $u = $c = $ed = $rn = $cp = $dl = $ex = $sf = $sfo = true; }
|
||||
if ($u && !$v && !$vo) $vo = true;
|
||||
//if ($s && !$v) $v = true;
|
||||
if ($w) { $c = $u = $ed = $rn = $cp = $mv = $dl = $ex = true; }
|
||||
if ($w) { $c = $u = $ed = $rn = $cp = $dl = $ex = true; }
|
||||
|
||||
if ($m) $rec['owners'][] = $user;
|
||||
if ($v) $rec['read'][] = $user;
|
||||
@@ -419,9 +461,13 @@ public static function canCopy(string $user, array $perms, string $folder): bool
|
||||
public static function canMove(string $user, array $perms, string $folder): bool {
|
||||
$folder = self::normalizeFolder($folder);
|
||||
if (self::isAdmin($perms)) return true;
|
||||
return self::hasGrant($user, $folder, 'owners')
|
||||
|| self::hasGrant($user, $folder, 'move')
|
||||
|| self::hasGrant($user, $folder, 'write');
|
||||
return self::ownsFolderOrAncestor($user, $perms, $folder);
|
||||
}
|
||||
|
||||
public static function canMoveFolder(string $user, array $perms, string $folder): bool {
|
||||
$folder = self::normalizeFolder($folder);
|
||||
if (self::isAdmin($perms)) return true;
|
||||
return self::ownsFolderOrAncestor($user, $perms, $folder);
|
||||
}
|
||||
|
||||
public static function canDelete(string $user, array $perms, string $folder): bool {
|
||||
|
||||
@@ -326,6 +326,8 @@ class FolderModel
|
||||
|
||||
// Update ownership mapping for the entire subtree.
|
||||
self::renameOwnersForTree($oldRel, $newRel);
|
||||
// Re-key explicit ACLs for the moved subtree
|
||||
ACL::renameTree($oldRel, $newRel);
|
||||
|
||||
return ["success" => true];
|
||||
}
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
require_once PROJECT_ROOT . '/config/config.php';
|
||||
|
||||
class UploadModel {
|
||||
|
||||
private static function sanitizeFolder(string $folder): string {
|
||||
$folder = trim($folder);
|
||||
if ($folder === '' || strtolower($folder) === 'root') return '';
|
||||
// no traversal
|
||||
if (strpos($folder, '..') !== false) return '';
|
||||
// only safe chars + forward slashes
|
||||
if (!preg_match('/^[A-Za-z0-9_\-\/]+$/', $folder)) return '';
|
||||
// normalize: strip leading slashes
|
||||
return ltrim($folder, '/');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles file uploads – supports both chunked uploads and full (non-chunked) uploads.
|
||||
*
|
||||
@@ -38,15 +51,19 @@ class UploadModel {
|
||||
return ["error" => "Invalid file name: $resumableFilename"];
|
||||
}
|
||||
|
||||
$folder = isset($post['folder']) ? trim($post['folder']) : 'root';
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name"];
|
||||
}
|
||||
$folderRaw = $post['folder'] ?? 'root';
|
||||
$folderSan = self::sanitizeFolder((string)$folderRaw);
|
||||
|
||||
|
||||
if (empty($files['file']) || !isset($files['file']['name'])) {
|
||||
return ["error" => "No files received"];
|
||||
}
|
||||
|
||||
$baseUploadDir = UPLOAD_DIR;
|
||||
if ($folder !== 'root') {
|
||||
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
if ($folderSan !== '') {
|
||||
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
|
||||
. str_replace('/', DIRECTORY_SEPARATOR, $folderSan) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) {
|
||||
return ["error" => "Failed to create upload directory"];
|
||||
}
|
||||
@@ -56,12 +73,14 @@ class UploadModel {
|
||||
return ["error" => "Failed to create temporary chunk directory"];
|
||||
}
|
||||
|
||||
if (!isset($files["file"]) || $files["file"]["error"] !== UPLOAD_ERR_OK) {
|
||||
$chunkErr = $files['file']['error'] ?? UPLOAD_ERR_NO_FILE;
|
||||
if ($chunkErr !== UPLOAD_ERR_OK) {
|
||||
return ["error" => "Upload error on chunk $chunkNumber"];
|
||||
}
|
||||
|
||||
$chunkFile = $tempDir . $chunkNumber;
|
||||
if (!move_uploaded_file($files["file"]["tmp_name"], $chunkFile)) {
|
||||
$tmpName = $files['file']['tmp_name'] ?? null;
|
||||
if (!$tmpName || !move_uploaded_file($tmpName, $chunkFile)) {
|
||||
return ["error" => "Failed to move uploaded chunk $chunkNumber"];
|
||||
}
|
||||
|
||||
@@ -100,8 +119,7 @@ class UploadModel {
|
||||
fclose($out);
|
||||
|
||||
// Update metadata.
|
||||
$relativeFolder = $folder;
|
||||
$metadataKey = ($relativeFolder === '' || strtolower($relativeFolder) === 'root') ? "root" : $relativeFolder;
|
||||
$metadataKey = ($folderSan === '') ? "root" : $folderSan;
|
||||
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
|
||||
$metadataFile = META_DIR . $metadataFileName;
|
||||
$uploadedDate = date(DATE_TIME_FORMAT);
|
||||
@@ -134,16 +152,16 @@ class UploadModel {
|
||||
|
||||
return ["success" => "File uploaded successfully"];
|
||||
} else {
|
||||
// Handle full upload (non-chunked).
|
||||
$folder = isset($post['folder']) ? trim($post['folder']) : 'root';
|
||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||
return ["error" => "Invalid folder name"];
|
||||
// Handle full upload (non-chunked)
|
||||
$folderRaw = $post['folder'] ?? 'root';
|
||||
$folderSan = self::sanitizeFolder((string)$folderRaw);
|
||||
}
|
||||
|
||||
$baseUploadDir = UPLOAD_DIR;
|
||||
if ($folder !== 'root') {
|
||||
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
if ($folderSan !== '') {
|
||||
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
|
||||
. str_replace('/', DIRECTORY_SEPARATOR, $folderSan) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) {
|
||||
return ["error" => "Failed to create upload directory"];
|
||||
}
|
||||
@@ -153,6 +171,10 @@ class UploadModel {
|
||||
$metadataChanged = [];
|
||||
|
||||
foreach ($files["file"]["name"] as $index => $fileName) {
|
||||
// Basic PHP upload error check per file
|
||||
if (($files['file']['error'][$index] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
|
||||
return ["error" => "Error uploading file"];
|
||||
}
|
||||
$safeFileName = trim(urldecode(basename($fileName)));
|
||||
if (!preg_match($safeFileNamePattern, $safeFileName)) {
|
||||
return ["error" => "Invalid file name: " . $fileName];
|
||||
@@ -161,21 +183,22 @@ class UploadModel {
|
||||
if (isset($post['relativePath'])) {
|
||||
$relativePath = is_array($post['relativePath']) ? $post['relativePath'][$index] ?? '' : $post['relativePath'];
|
||||
}
|
||||
$uploadDir = $baseUploadDir;
|
||||
$uploadDir = rtrim($baseUploadDir, '/\\') . DIRECTORY_SEPARATOR;
|
||||
if (!empty($relativePath)) {
|
||||
$subDir = dirname($relativePath);
|
||||
if ($subDir !== '.' && $subDir !== '') {
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $subDir) . DIRECTORY_SEPARATOR;
|
||||
// IMPORTANT: build the subfolder under the *current* base folder
|
||||
$uploadDir = rtrim($baseUploadDir, '/\\') . DIRECTORY_SEPARATOR .
|
||||
str_replace('/', DIRECTORY_SEPARATOR, $subDir) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
$safeFileName = basename($relativePath);
|
||||
}
|
||||
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0775, true)) {
|
||||
return ["error" => "Failed to create subfolder"];
|
||||
if (!is_dir($uploadDir) && !@mkdir($uploadDir, 0775, true)) {
|
||||
return ["error" => "Failed to create subfolder: " . $uploadDir];
|
||||
}
|
||||
$targetPath = $uploadDir . $safeFileName;
|
||||
if (move_uploaded_file($files["file"]["tmp_name"][$index], $targetPath)) {
|
||||
$folderPath = $folder;
|
||||
$metadataKey = ($folderPath === '' || strtolower($folderPath) === 'root') ? "root" : $folderPath;
|
||||
$metadataKey = ($folderSan === '') ? "root" : $folderSan;
|
||||
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
|
||||
$metadataFile = META_DIR . $metadataFileName;
|
||||
if (!isset($metadataCollection[$metadataKey])) {
|
||||
@@ -208,7 +231,7 @@ class UploadModel {
|
||||
}
|
||||
return ["success" => "Files uploaded successfully"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively removes a directory and its contents.
|
||||
|
||||